运维开发网
广告位招商联系QQ:123077622
 
广告位招商联系QQ:123077622

Unix网络编程—— shutdown与close

运维开发网 https://www.qedev.com 2021-05-18 09:32 出处:51CTO 作者:Annnnnnnnd
Unix网络编程—— shutdown与close,TCP的连接终止序列:TCP建立一个连接需要三次握手,但是终止一个连接需要四次挥手: 1. 当某个应用进程主动调用close时,它向对端发送一个FIN分节,表示这端需要关闭连接 2. 当对端接收到FIN分节时,read函数返回0,它的TCP发送一个ACK,表示接收到了主动close端的FIN分节。主动关闭端的TCP在接受到ACK后处于FIN_WAIT状态,表示需要等待对端的FIN分节到达。

TCP的连接终止序列:

TCP建立一个连接需要三次握手,但是终止一个连接需要四次挥手:

1. 当某个应用进程主动调用close时,它向对端发送一个FIN分节,表示这端需要关闭连接

2. 当对端接收到FIN分节时,read函数返回0,它的TCP发送一个ACK,表示接收到了主动close端的FIN分节。主动关闭端的TCP在接受到ACK后处于FIN_WAIT状态,表示需要等待对端的FIN分节到达。

3. 被动关闭端发送FIN分节给主动关闭端,主动关闭端收到FIN后,发送ACK给对端,处于TIME_WAIT状态,表示等待被动关闭端的ACK确认

4. 被动关闭端接收到ACK后四次挥手完成,两端套接字关闭完成

Unix网络编程—— shutdown与close

close函数

#includeint close(int fd);// 成功返回0,失败返回-1,并且置位errno

close在TCP中的默认行为是把该套接字标记为已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是不能再作为read,write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后再发送TCP连接终止序列。

close的错误返回三种情况:

- EBADF:表示参数fd为非法描述符

- EINTR:close调用被信号中断

- EIO:I/O时出现错误

一个常见的TCP并发服务器模型:

#include#include#include#include#include#includeint main(){    int sockfd = socket(AF_INET,SOCK_STREAM,0);    struct sockaddr_in serv_addr;    struct sockaddr_in client_addr;    int client_len;
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(LISTEN_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    int ret = bind(sockfd,(struct sockaddr* )&serv_addr,sizeof(serv_addr));
    ret = listen(sockfd,LISTENQ);    while(1){
        client_len = sizeof(client_addr);        int client_fd = accept(sockfd,(struct sockaddr *)&client_addr,&client_len);
        pid_t pid = fort();        if(pid < 0){            continue;
        }        else if(pid == 0){
            do_serv();
            close();    
        }        else close(client_fd);
    }    return 0;
}

上述代码中close被调用2次,因为子进程和父进程共享了client_fd,其引用计数为2,如果子进程只调用一次,则会导致client_fd的套接字永远不能关闭,导致进程描述符耗尽而无法为其他请求提供服务。并且当close调用时,TCP的读写2端都被关闭。

close在网络编程中的局限:

- 当一个描述符被共享使用时,调用close函数只是将其引用计数减一,仅仅在引用计数为0的时候,该套接字才被关闭。

- close终止读和写2个方向的数据传输。TCP为全双工协议,有时候当我们完成数据发送后,可能需要等待对端发送数据,此时可以调用shutdown来实现此功能。

shutdown函数

int shutdown(int sockfd,int howto); 
//返回:成功返回0,失败返回-1

howto的可选项:

SHUT_RD:关闭连接的读——套接字中不再读取数据,而且套接字接受缓冲区中的数据也会被丢弃。进程不能再对这个套接字调用任何读取函数。

SHUT_WR:关闭连接的写——对于TCP套接字,这成为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,然后TCP的正常连接终止序列。无论这个套接字描述符的引用计数是否为0,shutdown都会激发TCP终止序列,以后进程不能再对这个套接字调用任何写函数。

SHUT_RDWR:关闭读、写——相当于分别调取了shutdown两次并传递参数SHUT_RD和SHUT_WR。shutdown使用此选项与close的区别是,shutdown立马关闭套接字的读写通道,但是close只会在引用计数为0的情况才关闭读写通道。

close和shutdown的对比示例

主程序

#include#include#include#include#include#include#includeextern void client_echo(FILE *fp,int sockfd);int main(){ 
    int sockfd = socket(AF_INET,SOCK_STREAM,0);    struct sockaddr_in serv_addr;    struct sockaddr_in client_addr;    int client_len;
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(LISTEN_PORT);    char *strIp = inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr);    int ret = connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    client_echo(stdin,sockfd);//从stdin读取数据发送到sockfd并回写
    return 0;
}

使用close的实现

void client_echo(FILE *fp,int sockfd){    if(fp == NULL) return;
    int fd = fineno(fp);
    fd_set read_set;
    FD_ZERO(&read_set);
    int stdin_eof = 0;
    int nread = 0;
    char recvbuf[1024];    while(1){
        FD_SET(fd,&read_set);
        FD_SET(sockfd,&read_set);       
        int maxfd = max(fd,sockfd);
        int ret = select(maxfd + 1,&read_set,NULL,NULL,NULL);        if(ret < 0){            if(errno == EINTR) continue; // 由于中断引发的失败,重试            else {                       // 其他错误,退出
                perror("select");                return;
            }
        }        if(FD_ISSET(sockfd,&read_set)){            if( (nread = read(sockfd,recvbuf,sizelf(recvbuf))) == 0){                printf("read EOF from serv\n");
                close(sockfd);                exit(1);
            }
            fputs(recvbuf,stdout);
        }        if(FD_ISSET(fd,&read_set)){            if(fgets(recvbuf,sizeof(recvbuf),fd) == 0){                printf("read EOF from terminate\n");
                close(sockfd);                exit(1);
            }
            write(sockfd,recvbuf,strlen(recvbuf));
        }
    }
}

在上面的程序中,当客户端从终端读取到CTRL+D的时候将使fgets函数返回0,此时引发客户端主动关闭close,退出客户端的连接处理函数后,将立即退出程序(main函数结束)。此时服务端如果有数据正在发送,则将会丢失。处理这种问题的方式是,当客户端不再write时,只关闭TCP的write,但是任然保留其read通道。可以通过shutdown来实现。

使用shutdown的实现

void client_echo(FILE *fp,int sockfd){    if(fp == NULL) return;
    int fd = fineno(fp);
    fd_set read_set;
    FD_ZERO(&read_set);
    int stdin_eof = 0;
    int nread = 0;
    char recvbuf[1024];
    int maxfd;    while(1){       
        FD_SET(fd,&read_set);
        FD_SET(sockfd,&read_set);               
        if(stdin_eof != 0){
            FD_CLR(fd,&read_set);
            maxfd = sockfd;
        }        else maxfd = max(fd,sockfd);
        int ret = select(maxfd + 1,&read_set,NULL,NULL,NULL);        if(ret < 0){            if(errno == EINTR) continue; // 由于中断引发的失败,重试            else {                       // 其他错误,退出
                perror("select");                return;
            }
        }        if(FD_ISSET(sockfd,&read_set)){            if( (nread = read(sockfd,recvbuf,sizelf(recvbuf))) == 0){                if(stdin_eof == 1){                    return;
                }                else{                    printf("read EOF from serv\n");
                    close(sockfd);                    exit(1);
                }
            }
            fputs(recvbuf,stdout);
        }        if(FD_ISSET(fd,&read_set)){            if(fgets(recvbuf,sizeof(recvbuf),fd) == 0){
                stdin_eof = 1;
                shutdown(sockfd,SHUT_WR);   // send FIN
                FD_CLR(fd,&read_set);
            }            else write(sockfd,recvbuf,strlen(recvbuf));
        }
    }
}

在shutdown_client中,当从终端读取到EOF时,将调用shutdown关闭套接字的写通道,但是此套接字任然可以从服务端读取数据,保证了数据不会丢失。

扫码领视频副本.gif

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号