close和shutdown、套接字超时、timeout

close函数:

终止数据传送的两个方向。。。。

1,客户端A调用close函数,也就是说:socket(A)不能向socket(B)发送数据,也不能接收从B发送来的据。。。

      当客户端A调用了close函数时(一般情况下:会发送一个FIN分节),对端read返回为0,但是这并不意味着对端

      socket(B)不能向A发送数据,,,这也是允许的,,,(因为B收到FIN只能说明:A->B的数据不能传输了,但

      是B->A还是可以传输数据的,,,)

     

不用质疑的是:A不接受B发送来的数据,完全不影响B向A发送数据,只是A不接受罢了(关闭了),A关闭了,TCP

                           会向B响应一个RST分节,表示连接重置。。。。如果B再次调用write函数,那么就会产生一个

                            SIGPIPE信号,,,


这里我们说明一个问题:调用close函数(对于一个客户端和一个服务端而言:会发送一个FIN分节;

                                            对于多个客户端而言(多进程并发,select的I/O复用的并发机制),close会把描述符的引用

                                            计数减1,仅在该计数变为0时才关闭套接字)



接下来,我们可以在看一个问题,我们可以把TCP看成是全双工的管道:

                      

            我们以简单理解一下这个图:首先TCP报文端A,发送过去,然后B,C等,紧接着在还未收到A的确认的时候,

            我们客户端发送了FIN分节,导致我们客户端不能在次发送数据,严重的是我们的确认也就收不到了,,,,

            这个情况的产生是很正常的,,,,


为了避免这个尴尬的问题,,,,我们引入了shutdown,,,

#include <sys/socket.h>

int shutdown(int sockfd, int how);         //若成功则为0,若出错则为-1
该函数的行为依赖于how的值:

SHUT_RD(0):关闭连接的读这一半——套接字中不能在从管道中读取数据

SHUT_WR(1):不能再向管道当中写入数据,,,

SHUT_RDWR(2):相当于前面两者的组合。。。。。


如果how = 1,就可以保证发送一个FIN分节,保证对端接收到一个EOF字符,不用管套接字的引用计数是不是等于0,


1,shutdown可以有选择的终止某个方向上的数据传送,,,或者终止数据传送的两个方向,,,,我们既可以终止

      数据的发送方向,也可以终止数据的接收方向。。。。。。

2,close要发送FIN分节,必须等到引用计数等于0,而shutdown不用,,,,



看一下代码:

客户端处理代码,,,

 while(1)       //需要检测标准输入/套接口输入是不是产生了可读事件,
        {
                FD_SET(fd_stdin, &rset);
                FD_SET(sock, &rset);
                nready = select(maxfd + 1, &rset, NULL , NULL, NULL);
                if(nready == -1)
                        ERR_EXIT("select");
                if(nready == 0)
                        continue;
                
                if(FD_ISSET(sock, &rset))
                {
                         int ret = readline(sock, recvbuf, sizeof(recvbuf));
                         if(ret == -1)
                                 ERR_EXIT("readline");  
                         else if(ret == 0)
                         {
                                 printf("server close\n");
                                 break;  
                         }

                         fputs(recvbuf, stdout);
                         memset(recvbuf, 0, sizeof(recvbuf));
                }

                if(FD_ISSET(fd_stdin, &rset))
                {
                        if(fgets( sendbuf, sizeof(sendbuf), stdin) == NULL)     
                        {
                                close(sock);            //这里是重点
                        }
                        else
                        {       writen(sock, sendbuf, strlen(sendbuf));
                                memset(sendbuf, 0, sizeof(sendbuf));
                        }
                }
        }

最后一个if是select检测缓冲区,如果缓冲区中有数据,调用else中的writen()函数,如果出现信号中断(或者其他

方式使得fgets返回为-1,好像ctrl + b可以),然后就会调用close(sock)函数,,,,


服务端程序:

        //这里是对已完成的套接字进行继续检测
                for(i = 0; i< FD_SETSIZE; i++)
                {
                        conn = client[i];
                        if(conn == -1)
                                continue;
                        if(FD_ISSET(conn, &rset))
                        {
                                char recvbuf[1024] = {0};
                                int ret = readline(conn, recvbuf, 1024);
                                if(ret == -1)
                                        ERR_EXIT("readline");
                                if(ret == 0)
                                {
                                        printf("client close\n");
                                        FD_CLR(conn, &allset);
                                }
                                fputs(recvbuf, stdout);
                                sleep(4);        //同样,这里也是重点,,,,
                                writen(conn, recvbuf, strlen(recvbuf));

                                if(--nready <= 0)
                                        break;
                        }
                }

这里所加的延时,目的很简单,就是向客户端在发送完数据后,还没有来得及收到回射(响应)就调用close函数,,


调用close函数,就意味着不能向管道里面写入数据,也不能从管道中读取数据。。。。


这里如果,客户端,快速的输入(“aaa”,“bbb”),即使的按下ctrl+b,那么服务端是会收到aaa,bbb,但是我们

在客户端看到到回射会来的数据,因为,刚才按下ctrl+b,就已经导致程序执行到了close,close使得套接口不能

接收数据,也不能发送数据,所以很正常的收不到回射回来的数据,,,,


这里,可能会产生一个RST错误,然后接着产生sigpipe,所以我们应该对这个信号进行处理。。。。。。。。



那么,如果,我们调用shutdown呢????


    if(FD_ISSET(fd_stdin, &rset))
                {
                        if(fgets( sendbuf, sizeof(sendbuf), stdin) == NULL)     
                        {
                               // close(sock);            //这里是重点
                                   shutdown(sock, SHUT_WR);     //表示关闭了写
                        }
                        else
                        {       writen(sock, sendbuf, strlen(sendbuf));
                                memset(sendbuf, 0, sizeof(sendbuf));
                        }
                }

如此,我们的程序还是可以回射回来的,因为,此刻表示套接口可以接受数据,只是不能发送而已。。。。。。。。



套接字超时:

1,调用alarm,它在指定超时期满时产生SIGALRM信号

void handler(int sig)
{
        return 0;                 //不做多余的操作,仅仅只是打断read    
}
signal(SIGALRM, handler);         //对下面信号的处理
alarm(5);                         //我们设置一个闹钟,5秒钟
int ret = read(fd, buf, sizeof(buf));       //再一个I/O上读操作
//由于上面设置了alarm,那么如果5秒的时间到来了,还没有产生读操作,那么
//就会产生一个SIGALRM信号。。。。将read函数打断,不再阻塞
if(ret == -1 && errno == EINTR)
{
        errno = ETIMEDOUT;      
}
else if(ret >= 0)
{
        alarm(0);   //5秒之前就已经读操作了,必须关闭闹钟       
}
       但是,我们一般不会通过这种方式来实现超时,因为闹钟在程序里经常另作它用,而如果我们通过这种方式来实现

       超时,闹钟直接会产生冲突,但是,冲突的解决比较麻烦,,,,


2,使用较新的套接字选项SO_RCVTIMEO和SO_SNDTIMEO

setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, 5);
//设置读的超时时间
int ret = read(sock, buf, sizeof(buf));
if(ret == -1 && errno == EWOULDBLOCK)
{
        //超时之后,直接打断read,read返回-1,同时errno等于EWOULDBLOCK
        errno = ETIMEDOUT;    //更改错误                                        
}

       我们也不经常用这两个套接字选项来处理超时问题,很简单,因为并非所有的TCP实现都支持这两个套接字。。。


3,select实现超时,在select中阻塞等待I/O(select有内置的时间限制),以此代替直接阻塞在read或write调用上。。

 

实现简单的read_timeout()(下面是实现的一些伪代码,,,)

//成功(未超时,返回0),失败返回-1,超时返回-1并且errno = ETIMEDOUT
read_timeout(int fd, unsigned int wait_seconds)//wait_seconds要等待的秒数
{
        int ret = 0;    
        if(wait_seconds > 0) //要等待的秒数大于0,执行如下的操作
        {
                fd_set read_fdset;//定义一个读的集合
                struct timeval timeout;  //定义超时时间
                FD_ZERO(&read_fdset);    //清空该集合
                FD_SET(fd, &read_fdset); //将该套接口加入读集合

                timeout.tv_sec = wait_seconds;  //初始化秒数
                timeout.tv_usec = 0;            //毫秒
                do
                {
                        //select超时检测
                        ret = select(fd + 1, &read_fdset, NULL,NULL, &time_out);                
                }while(ret < 0 && errno == EINTR);
                        
                if(ret == 0)  //超时返回
                {
                        ret = -1;
                        errno = ETIMEDOUT;
                }
                else if(ret == 1) //产生可读事件
                {
                        ret = 0;
                }
        }
        return ret;
}


int ret;
ret = read_timeout(fd, 5);
if(ret == 0)
{
        read(fd, ,,,,,); //没有超时,可以进行读操作     
}
else if(ret == -1 && errno == ETIMEDOUT)
{
        timeout = ETIMEDOUT;   //已经超时,重新设置超传时间     
}
else
{
        ERR_EXIT("read_timeout");//read_timeout函数内部执行出错                 
}
  


实现简单的write_timeout(),这里跟上面唯一不一样的地方就在于:

   将fd加入到写的集合中去了,,,

do
                {
                        //select超时检测
                        ret = select(fd + 1,NULL, &write_fdset,NULL, &time_out);                                                                
                }while(ret < 0 && errno == EINTR);


这里要强调的一点就是,我们这里的timeout函数,都只是简单的一个检测,并没有读操作/写操作。。。。。。。。。。





实现简单的accept_timeout(),包括检测和accept函数的执行


//fd表示相应的套接字,addr表示返回的对端的地址,wait_seconds设置秒数
int accept(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
        int ret= 0;
        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
                {
                        ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
                        //此刻如果fd可读事件,说明已经有三次握手握手了
                        //如果在timeout时间内,那么会返回1,如果超时会返回0
                        //如果函数执行出错,那么返回-1
                        //如果返回-1,并且errno 等于EINTR,那么重复执行
                }while(ret < 0 && errno == EINTR);
                if(ret == -1)
                        return -1;
                else if(ret == 0)
                {
                        errno = ETIMEDOUT;
                        return -1;      
                }
        }
        //此刻说明已经不再阻塞了
        if(addr != NULL)
                ret = accept(fd, (struct sockaddr*)addr, &addrlen);
        else 
                ret = accept(fd, NULL, NULL); //如果地址为空,那么长度也要为空
        if(ret == -1)
                ERR_EXIT("accept");
        
        return ret;
}


      下面就是connect_timeout()函数,那么为什么要对连接进行超时检测呢???????????


      大家肯定还记得我们当时的三次握手吧,,,,,由客户端发起的连接请求,然后等待服务端的响应,

      然后这个过程就是一个RTT(数据的往返时间),而对于服务端而言,就是1.5RTT,在一般的情况下,

      请求的响应时间都是极短的,但是在广域网而言,传输的过程网络中极有可能发生网络拥塞,在拥塞

      的情况下,这个响应时间是很大的,一般socket,内核默认是75s,这对于我们而言,是不能容忍的

      ,仅仅光一个请求的确认就要花费这么长时间,这个时间足够我们去喝杯咖啡了,,,,,


      所以,我们这里要设置超时,????


对于connect超时时间:我们不能直接调用connect,如果直接调用那么就会产生阻塞,,,为了不阻塞,我们

首先应该将socket(fd)设置为非阻塞的,,,,



void activate nonblock(int fd)
{
        int ret;
        int flags = fcntl(fd, F_GETFL);
        
        if(flags == -1)
                ERR_EXIT("fcntl");
        flags |= O_NONBLOCK;
        ret = fcntl(fd, F_SETFL, flags);
        if(ret == -1)
                ERR_EXIT(fcntl);        
}

//设置相应的I/O为非阻塞模式,,,

//同样的,设置为阻塞模式,
//仅仅是相对于上面的   flags |= O_NONBLOCK;      改为flag &= ~O_NONBLOCK;


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_SETFL, flags);
        if(ret == -1)
                ERR_EXIT(fcntl);        
}

int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)

fd表示I/O或套接口,,,addr表示对方的地址,,,wait_seconds表示设置的时间


int connect_timeout(int fd, struct sockaddr_in *addr , unsigned int wait_seconds)
{
        int ret;
        socklen_t addrlen = sizeof(struct sockaddr_in);
        
        if(wait_seconds > 0)
                activate_nonblock(fd);      //设置为阻塞模式
        //再设置为非阻塞模式之后,我们就可以调用accept了
        ret = connect(fd, (struct sockaddr *)addr, addrlen);
        //当然此刻,极有可能返回失败,也有可能返回成功 
        if(ret < 0 && errno == EINPROGRESS)  //小于0,表示失败,表示正在处理当中,,,,
        {
                fd_set connect_fdset;
                struct tineval timeout;
                FD_ZERO(&connect_fdset);
                FD_ZET(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 == EINTR)
                if(ret == 0)
                {
                        ret == -1;
                        errno = ETIMEDOUT;      
                } //表示已然超时

                else if(ret < 0)
                {
                        return -1;      
                }
                else if(ret == 1)  //返回1,可能有两种情况,一种是连接建立成功,一种是产生错,,,,
                                   //此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。。。

               {
                        int err;
                        socklen_t socklen = sizeof(err);
                        int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, err, &socklen);
                        if(sockoptret == -1)
                                ret = -1;
                        if(err = 0)
                                ret = 0;   //说明没有错误,属于连接建立成功的那一种,,,,
                        else 
                        {
                                errno = err;    //产生错误,我们赋值错误给errno
                                ret = -1;       
                        }
                }

                if(wait_seconds > 0)
                {
                        deactivate_nonblock(fd);        
                }
                return ret;
        }
}

这里面可能会产生错误情况下的1,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值