13-select重写客户端和tcp优雅关闭

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82989004

在阅读此篇之前,请先看完第11篇的一小节,传送门:11-服务端进程终止和SIGPIPE信号

1. 服务器进程终止的问题

在第11篇中的第1小节中,tcp客户端需要同时处理两个输入:标准输入和tcp套接字,tcp客户端代码如下所示:

//循环读写数据
while (1) {
        //tcp客户端阻塞于fgets上     
        fgets(buf, sizeof(buf), stdin);
        write(sfd, buf, strlen(buf));
        //此时read到EOF
        len = read(sfd, buf, sizeof(buf));
        if(len == 0){
            puts("peer close");
            break;
        }else if(len < 0){
             perror("read error: ");
        }
        write(STDOUT_FILENO, buf, len);
    }

对于上面的代码是存在问题的,服务器tcp虽然正确的发送了FIN,但由于tcp客户端阻塞在标准输入(fget调用)上等待读取数据,这会导致tcp客户端无法及时看到这个EOF,直到tcp客户端调用read读取套接字才会看到这个EOF,tcp客户端什么时候看到这个EOF是不确定的,这取决于tcp客户端在fgets调用处阻塞的时间长短。

因此我们希望当服务器关闭tcp连接发送FIN时,tcp客户端可以马上检测到对端tcp连接已关闭,从而作出相应的处理动作,而能帮助客户端实现这种能力的就是I/O多路复用技术。

 

除了以上这种情况,还有其他使用I/O多路复用技术的场景,这里主要讨论在网络中的应用:

                1. 客户端同时处理多个套接字(客户端确实是有可能有这种需求的)

                2. 服务端既要监听套接字,又要处理已连接的套接字

                3. 服务端需要处理多个协议,既要处理tcp,又要处理udp

2. 文件描述符何时就绪

select就是一种多路IO复用技术,根据之前我们学习可知,select会检测监听的文件描述符是否有事件发生(有数据可读或可写),因此我们需要了解文件描述符什么时候准备好,但是对于不同的文件类型,文件描述符就绪的条件也不一样,这里我们主要讨论套接字文件描述符就绪的条件。

 

对于读操作,文件描述符就绪条件:

  1. tcp连接的对端关闭(半关闭),此时对套接字进行读操作将返回EOF
  2. 监听的套接字上有新的已完成“三次握手”的tcp连接
  3. 套接字发生了异常或错误,对这样的套接字进行读操作将会返回errno错误,也可以通过SO_ERROR套接字选项或getsockop清除。

对于写操作,文件描述符就绪条件:

  1. tcp连接的对端关闭(半关闭),此时对套接字进行写操作将会收到SIGPIPE信号
  2. 使用非阻塞connect建立tcp连接成功或失败
  3. 套接字发生了异常或错误,对这样的套接字进行写操作将会返回errno错误

 

 

下面将通过select重写客户端实验,进一步验证select检测文件描述符就绪的条件。

 

3. 使用select重写客户端

原来的tcp客户端是阻塞与fgets调用处,使用select重写tcp客户端后,改为阻塞于select调用处,下面是改写后的tcp客户端程序:

        fd_set set;
        //循环读写数据
        while (1) {
                FD_ZERO(&set);
                //设置套接字,标准输入文件描述符
                FD_SET(sfd , &set);
                FD_SET(STDIN_FILENO , &set);
                int maxfd = sfd + 1;
                //检测文件描述符
                select(maxfd , &set , NULL , NULL , NULL);

               //套接字文件描述符是否有事件发生
                if(FD_ISSET(sfd , &set)){
                        //通过select改写后,如果对端关闭会立即检测到
                        len = read(sfd , buf , sizeof(buf));
                        if(len == 0){
                                puts("peer close");
                                break;
                        }else if(len < 0){
                                perror("read error: ");
                        }
                        write(STDOUT_FILENO, buf, len);
                }

              //标准输入是否有事件发生
                if(FD_ISSET(STDIN_FILENO , &set)){
                        fgets(buf , sizeof(buf) , stdin);
                        write(sfd , buf , strlen(buf));
                }
        }
        //关闭连接
        close(sfd);

先运行./server,然后再运行./client

 

通过kill命令把服务端子进程终止掉,然后服务端子进程终止时会发送FIN:

 

 

通过select改写后,根据之前的文件描述符就绪条件,当服务器这端的tcp连接关闭时(调用close关闭后,服务端通常会发送一个文件结束标记EOF,然后才发送FIN关闭tcp连接),select就会检测到套接字文件描述符发生了事件,接着客户端调用read读取套接字就会读到EOF标志并返回0,然后关闭连接,从tcpdump工具抓到的数据包来看,这是一个正常的tcp连接释放过程:

 

4. tcp优雅关闭

这里简单提一下,调用close跟tcp连接释放实际上是没有什么关系的,因为close是属于文件系统的一个系统调用,用于关闭文件描述符的,并保证没有进程会读取,当有多个进程引用套接字时(引用计数大于1),调用close不会发生tcp连接释放,因为只有当引用计数为0时,才会触发tcp连接关闭。

对于shutdown调用来说,它才是专门用于关闭一个tcp连接的,shutdown没有文件系统的语义,它是专门针对内核的tcp套接字的,因此调用shutdown才是真正关闭了通信的套接字。

重点来了!!!注意体会

再说回UNP中所说的优雅关闭,所谓的优雅关闭实则是调用close之前,先调用shutdown,这样的顺序才是关闭tcp连接的正确方式,只有调用shutdown(SHUT_WR)才会发送一个FIN。

 

 

5. shutdown函数

再次强调一下close关闭网络连接的问题:

由于调用close关闭套接字会终止两个方向的数据传输,但是tcp是支持双向通信的,有时候我们只想关闭一端的数据传输,比如客户端没有数据要发送了,就调用shutdown关闭了写端,另一端仍然可以继续发送数据,如下图所示:

                                                                              图1-调用shutdown关闭tcp连接

 

shutdown函数原型:

#include <sys/socket.h>
int shutdown(int sockfd, int how);

参数sockfd用于指定socket的描述符

 

shutdown函数可选择中止一个方向的连接(不用考虑描述符的引用计数),而参数how是指定关闭哪一方向的tcp连接。

SHUT_RD:关闭连接的读端,即套接字不再接收数据,并且该套接字的接收缓冲区已有的数据会丢弃,也就是说进程不能再对该套接字进行读操作了,但是可以对套接字进行写操作

UT_WR:关闭连接的写端,即半关闭(half-close),进程不能对套接字进行写操作,并且套接字发送缓冲区中剩下的数据将会发送完,但是进程依然可以对套接字进行读操作

SHUT_RDWR:相当于关闭了连接的读写两端,不能再对该套接字进行读写操作

简单提一下:套接字本质上是由内核维护的一个伪文件,该文件内部有2个缓冲区,一个读缓冲区,一个写缓冲区。因此在调用shutdown函数传递SHUT_RD参数表示不能对该套接字的读缓冲区读取数据,但是依然可以对缓冲区写数据,SHUT_WR参数表示不能对该套接字的写缓冲区写数据,但是依然可以对读缓冲区读数据。

 

当客户端和服务端建立tcp连接后,服务端不会主动断开连接,一般是由客户端主动发起关闭连接,所以我们对客户端进行改写,在调用close之前,调用shutdown指定SHUT_WR:

        fgets(buf, sizeof(buf), stdin);
        //将数据写给服务器
        write(sfd, buf, strlen(buf)); 

        //关闭写端,指定SHUT_WR后给会对方发送一个FIN
        shutdown(sfd , SHUT_WR);

        //依然可以读
        len = read(sfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);
        //最后关闭套接字
        close(sfd);
        return 0;

 

启动客户端和服务端:

 

tcpdump抓取到的数据报如下:

调用shutdown优雅关闭tcp连接

 

第5个数据报是客户端调用shutdown关闭写端后发送的FIN,第6个是对数据和FIN的确认,第7和第8两个是服务端回写的数据包和发送的FIN,客户端收到后回复了确认,其中第10个是对FIN的确认。

 

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82989004