close与shutdown
阅读原文时间:2023年07月10日阅读:4

  首先看一个例子,如下图所示:

当我们客户端发送ABCD再close套接字的时候,服务器端的接收通道也被关闭了,将无法接收ABCD的数据。如果想要仅仅关闭发送通道,保留接收通道,可以使用shutdown。

一、close与shutdown 的区别:
  1、close终止了数据传送的两个方向
2、shutdown 可以有选择的终止某个方向的数据传送或者数据传送的两个方向、

二、shutdown 如果howto=1(SHUT_WR),就可以保证对等方接收到一个EOF字符,而不管其他进程是否已经打开了套接字(不管引用计数是否为1都激发TCP的正常终止连接)。而close不能保证(详见下面三中的例子,conn引用计数减为0才关闭),直到套接字引用计数减
位0时才发送。也就是说直到所有的进程都关闭了套接字。

三、
  int shutdown(int sockfd,int howto)
    howto=SHUT_RD (0)关闭连接的读的一半,不再接收数据
    howto=SHUT_WR (1)关闭连接的写的一半,
    howto=SHUT_RDWR(2)
例子:
int conn;
pid_t pid=fork();
if(pid==-1)
ERR_EXIT("fork");
if(pid==0)     //子进程
{
   close(sock);

  ….       //通信
  close(conn);//子进程使用完conn,close conn 引用计数减为0,这时才会向双方发送FIN段。
}else if(pid>0)   //父进程
{
  close(conn);//父进程不会向客户端发送FIN.要考虑到引用计数。close(conn) 父进程用不到conn,将conn引用计数减一 。 shutdown(conn,SHUT_WR) 的话不理会引用计数,直接向对方发送FIN段
}

下面程序对比说明close与shutdown的区别:

客户端程序:

/*
一、close与shutdown 的区别:
1、close终止了数据传送的两个方向
2、shutdown 可以有选择的终止某个方向的数据传送或者数据传送的两个方向、
二、shutdown 如果howto=1,就可以保证对等方接收到一个EOF字符,而不管其他进程是否已经打开了
套接字(不管引用计数是否为1都激发TCP的正常终止连接)。而close不能保证,直到套接字引用计数减
位0时才发送。也就是说直到所有的进程都关闭了套接字。
三、
int shutdown(int sockfd,int howto)
howto=SHUT_RD 关闭连接的读的一半,不再接收数据
howto=SHUT_WR 关闭连接的写的一半,
howto=SHUT_RDWR
例子:
int conn;
pid_t pid=fork();
if(pid==-1)
ERR_EXIT("fork");
if(pid==0)
{
close(sock);
close(conn);//这时才会向双方发送FIN段。
}else if(pid>0)
{
close(conn);//不会向客户端发送FIN
}
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nread;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0) { if(errno==EINTR) continue; else return -1; } else if(nread==0) return (count-nleft); bufp+=nread; nleft-=nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft=count; ssize_t nwritten; char *bufp=(char*)buf; while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<=0)
{
if(errno==EINTR)
continue;
return -1;
}else if(nwritten==0)
continue;
bufp+=nwritten;
nleft-=nwritten;
}
return count;

}
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
int ret=recv(sockfd,buf,len,MSG_PEEK);//从sockfd读取内容到buf,但不去清空sockfd,偷窥
if(ret==-1&&errno==EINTR)
continue;
return ret;
}
}
//偷窥方案实现readline避免一次读取一个字符
ssize_t readline(int sockfd,void * buf,size_t maxline)
{
int ret;
int nread;
size_t nleft=maxline;
char *bufp=(char*)buf;
while(1)
{
ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窥看
if(ret<0) return ret; else if(ret==0) return ret; nread=ret; int i; for(i=0;inleft)
exit(EXIT_FAILURE);
nleft-=nread;
ret=readn(sockfd,bufp,nread);
if(ret!=nread)
exit(EXIT_FAILURE);
bufp+=nread;//移动指针继续窥看
}
return -1;
}
void echo_cli(int sock)
{
/*
char sendbuf[1024]={0};
char recvbuf[1024]={0};
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)//默认有换行符
{

    writen(sock,sendbuf,strlen(sendbuf));  
    int ret=readline(sock,recvbuf,1024);  
    if(ret==-1)  
        ERR\_EXIT("readline");  
    else if(ret==0)  
    {  
        printf("service closed\\n");  
        break;  
    }  
    fputs(recvbuf,stdout);  
    memset(sendbuf,0,sizeof(sendbuf));  
    memset(recvbuf,0,sizeof(recvbuf));  
}  

*/
char sendbuf[1024]={0};
char recvbuf[1024]={0};
fd_set rset;
FD_ZERO(&rset);//初始化
int nready;//准备好的个数
int maxfd;
int fd=fileno(stdin);//防止STDIN_FILLENO被重定向
if(fd>sock)
maxfd=fd;
else
maxfd=sock;
int stdineof=0;//标准输入是否被终止了(Ctrl+D)
while(1)
{
if(stdineof==0) FD_SET(fd,&rset);//循环中,shutdown之后继续循环的时候,stdin就已经要被清除不能再放在select监听
FD_SET(sock,&rset);
nready=select(maxfd+1,&rset,NULL,NULL,NULL);
if(nready==-1)
ERR_EXIT("select error");
if(nready==0)
continue;
if(FD_ISSET(sock,&rset))
{
int ret=readline(sock,recvbuf,sizeof(recvbuf));
if(ret==-1)
ERR_EXIT("readline error");
else if(ret==0)
{
ERR_EXIT("serve closed");
break;
}
fputs(recvbuf,stdout);
memset(recvbuf,0,sizeof(recvbuf));
}
if(FD_ISSET(fd,&rset))
{
if(fgets(sendbuf,sizeof(sendbuf),stdin)==NULL)//输入两行再按下ctrl+D后,服务器收到消息4秒之后才能回射
//但套接字被close了
{
/*
close(sock);//实验,一旦收到EOF,关闭套接字。既不能接收也不能发送。而且服务器端也会崩溃??? (服务器端有无SIGPIPE信号处理的话)
sleep(5);
exit(EXIT_FAILURE);
*/
shutdown(sock,SHUT_WR);//shutdown关闭可以产生回射,可以继续读数据和对面的关闭通知
stdineof=1;
}
else
{
writen(sock,sendbuf,strlen(sendbuf));
memset(sendbuf,0,sizeof(sendbuf));
}
}
}

}
void handle_sigpipe(int sig)
{
printf("recive a signal=%d\n",sig);

}
int main(void)
{
signal(SIGPIPE,handle_sigpipe);//捕捉第二次write的SIGPIPE信号,默认终止进程
int sock;
if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
ERR_EXIT("socket error");

    struct sockaddr\_in servaddr;//本地协议地址赋给一个套接字  
    memset(&servaddr,0,sizeof(servaddr));  
    servaddr.sin\_family=AF\_INET;  
    servaddr.sin\_port=htons(5188);

    servaddr.sin\_addr.s\_addr=inet\_addr("127.0.0.1");//服务器段地址  
    //inet\_aton("127.0.0.1",&servaddr.sin\_addr);

    if(connect(sock,(struct sockaddr\*)&servaddr,sizeof(servaddr))<0)  
        ERR\_EXIT("connect");

    //利用getsockname获取客户端本身地址和端口,即为对方accept中的对方套接口  
    struct sockaddr\_in localaddr;  
    socklen\_t addrlen=sizeof(localaddr);  
    if(getsockname(sock,(struct sockaddr \*)&localaddr,&addrlen)<0)  
        ERR\_EXIT("getsockname error");  
    printf("local IP=%s, local port=%d\\n",inet\_ntoa(localaddr.sin\_addr),ntohs(localaddr.sin\_port));  
    //使用getpeername获取对方地址  
echo\_cli(sock);//选择一个与服务器通信  
return 0;  

}

  服务器端程序:

/*
服务器进程要处理SIGPIPE信号,避免被该信号终止进程。利用shutdown函数我们还能够使得
服务器能够将接收到的内容回射回去
*/

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nread;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0) { if(errno==EINTR) continue; else return -1; } else if(nread==0) return (count-nleft); bufp+=nread; nleft-=nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft=count; ssize_t nwritten; char *bufp=(char*)buf; while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<=0)
{
if(errno==EINTR)
continue;
return -1;
}else if(nwritten==0)
continue;
bufp+=nwritten;
nleft-=nwritten;
}
return count;

}
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
int ret=recv(sockfd,buf,len,MSG_PEEK);//从sockfd读取内容到buf,但不去清空sockfd,偷窥
if(ret==-1&&errno==EINTR)
continue;
return ret;
}
}
//偷窥方案实现readline避免一次读取一个字符
ssize_t readline(int sockfd,void * buf,size_t maxline)
{
int ret;
int nread;
size_t nleft=maxline;
char *bufp=(char*)buf;
while(1)
{
ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窥看
if(ret<0) return ret; else if(ret==0) return ret; nread=ret; int i; for(i=0;inleft)
exit(EXIT_FAILURE);
nleft-=nread;
ret=readn(sockfd,bufp,nread);
if(ret!=nread)
exit(EXIT_FAILURE);
bufp+=nread;//移动指针继续窥看
}
return -1;
}
void handle_sigchld(int sig)
{

while(waitpid(-1,NULL, WNOHANG)>0)  
    ;

}
void handle_sigpipe(int sig)
{
printf("recevie a sig=%d\n",sig);//打印,不退出服务器进程
}
int main(void)
{

signal(SIGCHLD,handle\_sigchld);  
//signal(SIGPIPE,SIG\_IGN);//忽略sigpipe信号  
signal(SIGPIPE,handle\_sigpipe);  
int listenfd;  
if((listenfd=socket(PF\_INET,SOCK\_STREAM,IPPROTO\_TCP))<0)  
    ERR\_EXIT("socket error");  
//if((listenfd=socket(PF\_INET,SOCK\_STREAM,0))<0)

//本地协议地址赋给一个套接字  
struct sockaddr\_in servaddr;  
memset(&servaddr,0,sizeof(servaddr));  
servaddr.sin\_family=AF\_INET;  
servaddr.sin\_port=htons(5188);  
servaddr.sin\_addr.s\_addr=htonl(INADDR\_ANY);//表示本机地址

//开启地址重复使用,关闭服务器再打开不用等待TIME\_WAIT  
int on=1;  
if(setsockopt(listenfd,SOL\_SOCKET,SO\_REUSEADDR,&on,sizeof(on))<0)  
    ERR\_EXIT("setsockopt error");  
//绑定本地套接字  
if(bind(listenfd,(struct sockaddr\*)&servaddr,sizeof(servaddr))<0)  
    ERR\_EXIT("bind error");  
if(listen(listenfd,SOMAXCONN)<0)//设置监听套接字(被动套接字)  
    ERR\_EXIT("listen error");

struct sockaddr\_in peeraddr;//对方套接字地址  
socklen\_t peerlen=sizeof(peeraddr);

/*
pid_t pid;
while(1){
if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0) ERR_EXIT("accept error"); //连接好之后就构成连接,端口是客户端的。peeraddr是对端 printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); pid=fork(); if(pid==-1) ERR_EXIT("fork"); if(pid==0){ close(listenfd); echo_srv(conn); //某个客户端关闭,结束该子进程,否则子进程也去接受连接 //虽然结束了exit退出,但是内核还保留了其信息,父进程并未为其收尸。 exit(EXIT_SUCCESS); }else close(conn); } */ int client[FD_SETSIZE];//select最大文件描述符,用来保存已连接文件描述符。 int i=0; for(i=0;imaxi)
maxi=i;//更新最大不空闲位置
break;
}

        }  
        if(i==FD\_SETSIZE)  
        {  
            fprintf(stderr,"too many clents\\n");  
            exit(EXIT\_FAILURE);  
        }  
        printf("ip=%s port=%d\\n",inet\_ntoa(peeraddr.sin\_addr),ntohs(peeraddr.sin\_port));

        FD\_SET(conn,&allset);//将已连接套接字描述符放入allset,用于监测已连接套接口是否有客户端数据到来  
        if(conn>maxfd)  
            maxfd=conn;//更新maxfd  
        if(--nready<=0)  
            continue;//如果事件已经处理完,就继续循环监听,不再执行以下代码

    }  
    for(i=0;i<=maxi;i++)//小于等于  
    {  
        conn=client\[i\];  
        if(conn==-1)  
            continue;  
        if(FD\_ISSET(conn,&rset))//已经连接套接字是否有事件,不用while(1)循环处理客户端发送,有select监听。  
        {  
            int ret;  
            char recvbuf\[1024\];  
            memset(&recvbuf,0,sizeof(recvbuf));  
            ret=readline(conn,recvbuf,1024);  
            if(ret==-1)  
                ERR\_EXIT("readline");  
            else if(ret==0)  
            {  
                printf("client close\\n");  
                FD\_CLR(conn,&allset);//客户端清理,select就不用去监听  
                client\[i\]=-1;  
                close(conn);//前面程序BUG,对方关闭之后,我们服务器也要关闭套接口。让客户端接收到通知  
            }  
            fputs(recvbuf,stdout);  
            sleep(4);//服务器睡眠4秒。再回射数据,不马上回射过去。服务器收到客户端的数据等待4秒才回射。  
                //如果客户端用close关闭套接字,则不能回射回去。  
//由于对方关闭,返回RST字段,再次写的时候遇到RST段产生SIGPIPE信号会终止服务器,所以服务器需要加一个信号处理程序处理SIGPIPE信号。  
            writen(conn,recvbuf,strlen(recvbuf));//write :aaa  bbb ,RST,写aaa接收bbb,再bbb,有了SIGPIPE  
            if(--nready==0)  
                break;  
        }  
    }  
}  
return 0;  

}