Unix网络编程—— shutdown与close

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后四次挥手完成,两端套接字关闭完成
TCP四次挥手

close函数

#include <unistd.h>
int close(int fd);
// 成功返回0,失败返回-1,并且置位errno

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

close的错误返回三种情况:
- EBADF:表示参数fd为非法描述符
- EINTR:close调用被信号中断
- EIO:I/O时出现错误

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

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>


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);
    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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>

extern 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关闭套接字的写通道,但是此套接字任然可以从服务端读取数据,保证了数据不会丢失。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值