(P16)socket編程(十一)

1.套接字I/O超時設置方法

  • alarm
    因爲該鬧鐘可能會被其它使用,所以一般不使用鬧鐘來實現超時
SIGALRM
void handle(int sig)
{
	return 3;
}

signal(SIGALARM, handle);
alarm(5);
在read之前設置一個鬧鐘,若5s鍾內沒有返回數據(到達5s鍾),則會產生一個SIGALRM信號將read打斷
int ret = read(fd, buf, sizeof(buf));
if (ret == -1 && errno == EINTR)//被EINTR打斷
{
	errno =ETIMEDOUT;
}
else if (ret >=0 )
{
	alarm(0);
}

  • 套接字選項
    SO_SNDTIMEO發送超時時間
    SO_RCVTIMEO接收超時時間
    不好移植,因爲一些TCP選項不支持這兩個選項
setsockopt(sock,SOL_SOCKET, SO_RCVTIMEO, 5)

int ret = read(fd, buf, sizeof(buf));
if (ret == -1 && errno == EWOULDBLOCK)//超時的錯誤碼是EWOULDBLOCK
{
	errno =ETIMEDOUT;
}

-select

2.用select實現超時

  • 同select實現超時
    read_timeout函數封裝
    write_timeout函數封裝
    accept_timeout函數封裝
    connect_timeout函數封裝

  • connect_timeout的存在的價值?
    (1)RTT:一次往返的時間,
    (2)connect要停RTT這麼長的時間才返回,系統默認是75s,在廣域網上可能出現網絡擁塞,connect可能要停比較長的時間才能返回,系統默認75s,對於用於來說是不能容忍的,所以需要編寫connect_timeout函數,用於連接超時的時間
    在這裏插入圖片描述

  • eg:NetworkProgramming-master (1)\LinuxNetworkProgramming\P16Timeout

=================NetworkProgramming-master (1)\LinuxNetworkProgramming\P16Timeout\sysutil.c==============
#include <sysutil.h>

/*
read_timeout使用方法
int ret;
ret  = read_timeout(fd, 5);
if (ret == 0)//成功返回0,進行讀操作
{
    read(fd,....);若wait_seconds=0,則阻塞在read
}
else if (ret == -1 && errno == ETIMEOUT)
{
    timeout...
}
else
{
    ERR_EXIT("read_timeout");
}
*/


/*
    * read timeout - 讀超時檢測函數,不含讀操作
    * @fd:文件描述符
    * @wait_seconds:等待超時秒數,如果爲0表示不檢測超時
    * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT 
*/
//若檢測到超時,就不進行讀操作,若返回成功未超時,將做讀操作
//read_timeout作用:檢測IO是否超時
int read_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set read_fdset;//讀集合
        struct  timeval timeout;//超時時間

        FD_ZERO(&read_timeout);
        FD_SET(fd, &read_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        //超時時間到了,沒有發生可讀事件,那麼返回值爲0
        //在超時時間之內,發生可讀事件,返回值爲1
        //select調用失敗返回小於0,若小於0是由信號中斷的話(errno == EINTR),則繼續調用select
        do 
        {
            ret = select(fd +1, &read_fdset, NULL, NULL, &timeout);
        }while(ret < 0 && errno == EINTR);


        if (ret == 0 )
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)//因爲當前只將一個fd放入到讀集合fdset中
            ret = 0;
        
        //若ret= -1,並且errno不等於EINTR,則不做任何處理,直接return
        //若wait_seconds=0,則不按照超時的方式進行處理
    }

    //wait_seconds=0,則直接返回ret=0
    return ret;
}

/*
    * read timeout - 讀超時檢測函數,不含寫操作
    * @fd:文件描述符
    * @wait_seconds:等待超時秒數,如果爲0表示不檢測超時
    * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT 
*/
//若檢測到超時,就不進行寫操作,若返回成功未超時,將做寫操作
//write_timeout作用:檢測IO是否超時
int write_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set write_fdset;//寫集合
        struct  timeval timeout;//超時時間

        FD_ZERO(&write_timeout);
        FD_SET(fd, &write_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        do 
        {
            ret = select(fd +1, &write_timeout, NULL, NULL, &timeout);
        }while(ret < 0 && errno == EINTR);

        if (ret == 0 )
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)//因爲當前只將一個fd放入到讀集合fdset中
            ret = 0;
    }

    return ret;
}

/*
 * accept_timeout 帶超時的accept
 * fd:套接字,檢測的IO
 * addr:輸出參數,返回對方地址
 * wait_seconds:等待超時秒數,如果爲0表示正常模式
 * 成功(未超時)返回已連接套接字,超時返回-1並且errno = ETIMEDOUT
*/
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
    {
        fd_set accept_fdset;
        struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);
        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            //三次握手已完成,已完成連接隊列有一個條目,accept不阻塞,
            //就說明accept_fdset集合中產生了可讀事件
            ret = select(fd+1, &accept_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == -1)//這裏是ret=-1,errno不等於EINTR
            return -1;
        else if (ret == 0)//ret=0表示超時了
        {
            errno = ETIMEDOUT;
            return -1;
        }
    }

    //ret=1表示:表示檢測到事件
    //wait_seconds=0,則直接調用accept,會阻塞
    if (addr != NULL)
        ret = accept(fd, (struct sockaddr*)addr, &addrlen);//返回已連接套接字
    else
        ret = accept(fd, NULL, NULL);

    if (ret == -1)
        ERR_EXIT("accept");    
    return ret;
}

/*
 * activate_noblock:設置I/O爲非阻塞模式
 * @fd:文件描述符
*/
void activate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);//獲取fd的標記
    if (flags == -1)
        ERR_EXIT("fcntl");

    flags |= O_NONBLOCK;//添加非阻塞模式
    ret = fcntl(fd, F_SETTFL, flags);
    if (ret == -1)
        ERR_EXIT("fcntl");
}

/*
 * activate_noblock:設置I/O爲阻塞模式
 * @fd:文件描述符
*/
void deactivate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        ERR_EXIT("fcntl");

    flags &= ~O_NONBLOCK;//去掉非阻塞模式
    ret = fcntl(fd, F_SETTFL, flags);
    if (ret == -1)
        ERR_EXIT("fcntl");
}

/*
 * connect_timeout  connect
 * @fd:套接字
 * @addr:要連接的對方地址
 * @wait_seconds等待超時秒數,如果爲0表示正常模式
 * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno=ETIMEDOUT
*/
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    //不能直接調用connect,直接調用就會阻塞,希望以非阻塞的方式調用,所以fd
    //設置爲非阻塞模式
    if (wait_seconds > 0)
        activate_nonblock(fd);

    ret = connect(fd, (struct sockaddr*)addr, addrlen);
    if (ret <0 && errno == EINPROGRESS)//非阻塞fd返回一定是EINPROGRESS,表示正在處理中
    {
        printf("AAAAA");
        fd_set connect_fdset;//已連接fd集合
        struct timeval timeout;//超時時間
        FD_ZERO(connect_fdset);
        FD_SET(fd, &connect_fdset);
        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            /* 一旦連接建立,套接字就可寫 */
            ret = select(fd+1, NULL, &connect_fdset, NULL, &timeout);
        } while (ret <0 && errno == EINTER);
        
        if (ret == 0)//超時時間到了,還沒有產生可寫事件,意味着連接沒成功
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret < 0)
            return -1;
        else if (ret == 1)//檢測到可寫事件
        {
            printf("BBBBB");
            //ret返回1,可能有2種情況,一種是連接建立成功,一種是套接字產生錯誤
            //此時錯誤信息不會保存至errno變量中,因此,需要調用getsockopt來獲取
            int err;
            socklen_t socklen = sizeof(err);
            int sockoptret = getsockopt(fd, SOL_SOCKET , SO_ERROR, &err, &socklen);//獲取錯誤到err中
            if (sockoptret == -1)
            {
                return -1;
            }
            if (err == 0)//表示沒有錯誤,連接建立成功
            {
                printf("DDDDD");
                ret = 0;
            }
            else
            {
                printf("CCCCC");
                errno =err;
                ret = -1;
            }
            
        }
        
    }

     if (wait_seconds >0)
     {
         deactivate_nonblock(fd);//重新設置爲阻塞模式
     }
     return ret;
}


========================NetworkProgramming-master (1)\LinuxNetworkProgramming\P16Timeout\sysutil.h=========
#ifndef _SYSUTIL_H
#define _SYSUTIL_H

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
            perror(m); \
            exit(EXIT_FAILURE); \
        }while(0)

//設置fd爲阻塞或者非阻塞
void activate_nonblock(int fd);
void deactivate_nonblock(int fd);

//超時IO函數
int read_timeout(int fd, unsigned int wait_seconds);
int write_timeout(int fd, unsigned int wait_seconds);
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);


#endif
=======================NetworkProgramming-master (1)\LinuxNetworkProgramming\P16Timeout\srv.c==========
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
            perror(m); \
            exit(EXIT_FAILURE); \
        }while(0)

int main(void)
{
    int listenfd;
    if ((listen = socket(PF_INET, SOCK_STREAM, 0)) <0)
        ERR_EXIT("socket");
    
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sinport = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on ,sizeof(on)) < 0)
        ERR_EXIT("setsockopt");
    
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");

    if (listen(listenfd, SOMAXCONN) <0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen;
    int conn;

    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen))< 0)
        ERR_EXIT("accept");
    
    printf("ip=%s port =%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

    return 0;

}

=======================NetworkProgramming-master (1)\LinuxNetworkProgramming\P16Timeout\cli.c======================
#include <sysutil.h>

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");
    
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sinport = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = connect_timout(sock, &servaddr, 5);
    if (ret == -1 && errno == ETIMEDOUT)
    {
        printf("timeout...\n");
        return 1;
    }
    else if (ret == -1)
        ERR_EXIT("connect_timeout");

    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);

    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);
    if (getsockname(sock, (struct sockaddr*)localaddr, &addrlen) < 0)
        ERR_EXIT("getsockname");

    printf("ip=%s port =%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));//打印本地端口和ip

    return 0;
}
  • Makefile
    NetworkProgramming-master (1)\LinuxNetworkProgramming\P16Timeout\Makefile
.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=cli srv
all:$(BIN)
%.o:%.c
	$(cc) $(CFLAGS) -c $< -o $@
cli:cli.o sysutil.o
	$(cc) $(CFLAGS) $^ -o $@
srv:srv.o sysutil.o
	$(cc) $(CFLAGS) $^ -o $@
clean:
	rm -f *.o $(BIN)
  • 測試1:
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 測試2:
    若只執行客戶端,可以看一下connect_timeout是如何調用的,在裏面增加了相關標記代碼
    在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章