首先看一个例子,如下图所示:
当我们客户端发送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<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include <sys/time.h>
#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;i<nread;i++)
{
if(bufp[i]=='
')
{
ret=readn(sockfd,bufp,i+1);//读出sockfd中的一行并且清空
if(ret!=i+1)
exit(EXIT_FAILURE);
return ret;
}
}
if(nread>nleft)
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
");
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
",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
",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port));
//使用getpeername获取对方地址
echo_cli(sock);//选择一个与服务器通信
return 0;
}
服务器端程序:
/*
服务器进程要处理SIGPIPE信号,避免被该信号终止进程。利用shutdown函数我们还能够使得
服务器能够将接收到的内容回射回去
*/
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#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;i<nread;i++)
{
if(bufp[i]=='
')
{
ret=readn(sockfd,bufp,i+1);//读出sockfd中的一行并且清空
if(ret!=i+1)
exit(EXIT_FAILURE);
return ret;
}
}
if(nread>nleft)
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
",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
",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;i<FD_SETSIZE;i++)
{
client[i]=-1;
}
int conn;//已连接套接字(主动套接字)
int nready;
int maxi=0;//最大不空闲位置
int maxfd=listenfd;
fd_set rset,allset;
FD_ZERO(&rset);
FD_ZERO(&allset);
FD_SET(listenfd,&allset);
while(1)
{
rset=allset;
nready=select(maxfd+1,&rset,NULL,NULL,NULL);//如果是监听套接口(服务器),已完成连接队列不为空时,accept不再阻塞;
if(nready==-1)
{
if(errno==EINTR)
continue;
ERR_EXIT("select error");
}
if(nready==0)
continue;
if(FD_ISSET(listenfd,&rset))//监听口有事件,已完成队列不为空
{
conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);
if(conn==-1)
ERR_EXIT("accept error");
for(i=0;i<FD_SETSIZE;i++)
{
if(client[i]<0)
{
client[i]=conn;
if(i>maxi)
maxi=i;//更新最大不空闲位置
break;
}
}
if(i==FD_SETSIZE)
{
fprintf(stderr,"too many clents
");
exit(EXIT_FAILURE);
}
printf("ip=%s port=%d
",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
");
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;
}