【网络】(九)close与shutdown
2015-09-14 16:40
337 查看
1、两者的区别
close函数会终止数据传送的两个方向,包括套接口的读取和写入;
shutdown函数可以有选择的终止某个方向的数据传送,或者终止两个方向;
shutdown函数的how参数如果为SHUT_RDWR,表示关闭了套接口的写端,可以保证此时对方会收到一个FIN段,并导致对方read函数读取到EOF并返回0,shutdown函数关闭写端时总会这么做,
它不理会套接字引用计数;而close函数不会保证,调用close函数时,直到套接字引用计数为0时才会发送FIN段!
2、改进代码
假设:客户端发送数据给服务端,服务端收到数据后,等待一段事件再将数据会送给客户端!
如果客户端将数据全部发送出去后,客户端调用了close函数关闭了套接字,那么服务器在回送数据时,客户端已经无法再接收数据,第一次回送后,服务端会收到RST段,第二次回送后,服务端会引发SIGPIPE信号,从而导致服务端进程终止。
解决方法就是不调用close函数,改用shutdown函数,当客户端将数据全部发送完毕后,只关闭发送端口,接收端口还可正常接收数据,关闭发送端口会导致客户端向服务端发送一个FIN段,服务端收到该段后,可再调用close关闭套接字,此时数据已经回送完毕,等到客户端接收到服务端发送的FIN段,数据也已经接收完成!这时再完全关闭套接字即可。
服务端代码
server.c
编译命令:
客户端代码
client.c
编译命令
close函数会终止数据传送的两个方向,包括套接口的读取和写入;
shutdown函数可以有选择的终止某个方向的数据传送,或者终止两个方向;
shutdown函数的how参数如果为SHUT_RDWR,表示关闭了套接口的写端,可以保证此时对方会收到一个FIN段,并导致对方read函数读取到EOF并返回0,shutdown函数关闭写端时总会这么做,
它不理会套接字引用计数;而close函数不会保证,调用close函数时,直到套接字引用计数为0时才会发送FIN段!
2、改进代码
假设:客户端发送数据给服务端,服务端收到数据后,等待一段事件再将数据会送给客户端!
如果客户端将数据全部发送出去后,客户端调用了close函数关闭了套接字,那么服务器在回送数据时,客户端已经无法再接收数据,第一次回送后,服务端会收到RST段,第二次回送后,服务端会引发SIGPIPE信号,从而导致服务端进程终止。
解决方法就是不调用close函数,改用shutdown函数,当客户端将数据全部发送完毕后,只关闭发送端口,接收端口还可正常接收数据,关闭发送端口会导致客户端向服务端发送一个FIN段,服务端收到该段后,可再调用close关闭套接字,此时数据已经回送完毕,等到客户端接收到服务端发送的FIN段,数据也已经接收完成!这时再完全关闭套接字即可。
服务端代码
server.c
编译命令:
gcc -Wall -g -std=gnu99 server.c -o server
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <stdbool.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <string.h> #include <sys/select.h> #include <signal.h> #define handle_error(msg) \ do{perror(msg);exit(EXIT_FAILURE);}while(0) ssize_t readn(int fd, void *buf, size_t count) { if((fd < 0) || (buf == NULL) || (count < 0)) return -1; size_t nleft = count; //剩余字节数 ssize_t nread = 0; //已读字节数 char *pbuf = (char*)buf; while(nleft > 0) { if((nread = read(fd, pbuf, nleft)) < 0) { if(errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; pbuf += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { if((fd < 0) || (buf == NULL) || (count < 0)) return -1; size_t nleft = count; //剩余字节数 ssize_t nwritten = 0; //已发送字节数 char *pbuf = (char*)buf; while (nleft > 0) { if((nwritten = write(fd, pbuf, nleft)) < 0) { if(errno == EINTR) continue; return -1; } else if(nwritten == 0) continue; pbuf += nwritten; nleft -= nwritten; } return count; } //使用recv函数从套接字接收缓冲区中接收数据,但并不从缓冲去中清除数据 ssize_t recv_peek(int sockfd, void *buf, size_t len) { while(true) { int iret = recv(sockfd, buf, len, MSG_PEEK); if(iret == -1 && errno == EINTR) //如果失败是因为信号中端,那么就重新再试 continue; return iret; } } //如果读到的数据包含\n,则返回,表示一条完整的消息读取完毕 ssize_t recvline(int sockfd, void *buf, size_t maxlen) { int iret = 0; int nread = 0; //已读数据 char *pbuf = (char*)buf; int nleft = maxlen; //剩余字符 while(true) { iret = recv_peek(sockfd, pbuf, nleft); if(iret < 0) return iret; //读取失败 else if(iret == 0) return iret; //对方关闭套接口 nread = iret; if(nread > nleft) //已经读取到的数据只可能小于或者等于剩余的数据 exit(EXIT_FAILURE); for(int i = 0; i < nread; i++) { if(pbuf[i] == '\n') { iret = readn(sockfd, pbuf, i+1); //从缓冲区中读走包括\n在内的数据 if(iret != i+1) exit(EXIT_FAILURE); //没有读取都i+1个数据,说明失败 return iret; //读取都\n返回 } } //在当前读到的数据中没有发现\n,那么先将这部分数据从缓冲区中读走,然后接着偷窥后面的数据 nleft -= nread; iret = readn(sockfd, pbuf, nread); if(iret != nread) exit(EXIT_FAILURE); pbuf += nread; } return -1; } //为了防止SIGPIPE信号产生而终止了进程,所以捕获此信号 void handle_sigpipe(void) { struct sigaction act; act.sa_handler = SIG_IGN; //忽略SIGPIPE信号 act.sa_flags = 0; sigemptyset(&act.sa_mask); if(sigaction(SIGPIPE, &act, NULL) == -1) handle_error("sigaction"); } int main(void) { handle_sigpipe(); //捕获SIGPIPE信号 int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP); if(sk_fd < 0) handle_error("socket"); //使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口 int on = 1; if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { close(sk_fd); handle_error("setsockopt"); } struct sockaddr_in sr_addr; memset(&sr_addr,0,sizeof(sr_addr)); sr_addr.sin_family = AF_INET; sr_addr.sin_port = htons(5188); sr_addr.sin_addr.s_addr = htonl(INADDR_ANY); //sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //inet_aton("127.0.0.1",&sr_addr.sin_addr); if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0) { close(sk_fd); handle_error("bind"); } //被动套接字 if(listen(sk_fd, SOMAXCONN) < 0) //内核为此套接字排队的最大连接数由SOMAXCONN宏指定 { close(sk_fd); handle_error("listen"); } //使用select函数实现单进程并发服务器 fd_set readset; fd_set allset; FD_ZERO(&readset); FD_ZERO(&allset); FD_SET(sk_fd, &allset); //初始状态下,只关心监听套接字 int maxfd = sk_fd; //假设最大文件描述符 int maxfd_last = maxfd; //保存一个maxfd记录,上一次的最大值 int nready = 0; int maxindex = 0; //client_sk数组最大已用位置 int maxindex_last = 0; //maxindex上一次的值 int client_sk[FD_SETSIZE]; //存储客户端连接信息 for(int i = 0; i < FD_SETSIZE; i++) { client_sk[i] = -1; } while (true) { readset = allset; nready = select(maxfd + 1, &readset, NULL, NULL, NULL); if(nready == -1) { if(errno == EINTR) //被信号中断 continue; handle_error("select"); } if(nready == 0) //time out continue; if(FD_ISSET(sk_fd, &readset)) //检测到监听套接字事件 { //调用accept建立连接 struct sockaddr_in cl_addr; socklen_t cl_length = sizeof(cl_addr); memset(&cl_addr,0,sizeof(cl_addr)); int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length); if(ac_sk < 0) { if(errno == EINTR) continue; handle_error("accept"); } //加入到监听集合中 FD_SET(ac_sk, &allset); if(ac_sk > maxfd) { maxfd_last = maxfd; //保存历史记录 maxfd = ac_sk; //更新最大文件描述符 } int i; for(i = 0; i < FD_SETSIZE; i++) { if(client_sk[i] < 0) { client_sk[i] = ac_sk; if(maxindex < i) { maxindex_last = maxindex; //保存历史记录 maxindex = i; //更新最大已用位置 } break; } } if(i == FD_SETSIZE) //client_sk数组空间已满,客户端太多 { fprintf(stderr, "Client too many!"); exit(EXIT_FAILURE); } printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port)); if(--nready <= 0) //如果除了监听套接字外没有其他套接字发生事件 continue; } //客户端连接套接字发生事件 for(int i = 0; i <= maxindex; i++) { int conn_sk = client_sk[i]; if(FD_ISSET(conn_sk, &readset)) { //有数据可以接收 char recvbuf[1024]; memset(recvbuf,0,sizeof(recvbuf)); int iret = recvline(conn_sk,recvbuf,sizeof(recvbuf)); //获取包数据长度 if(iret == -1) handle_error("read"); else if(iret == 0) { //客户端关闭,删除对应的连接套接字 if(conn_sk == maxfd) { maxfd = maxfd_last; //如果删除的时最大文件描述符 } if(i == maxindex) { maxindex = maxindex_last; //如果删除的描述符的位置是当前最大已用位置 } client_sk[i] = -1; FD_CLR(conn_sk, &allset); printf("Client was closed!\n"); close(conn_sk); } fputs(recvbuf,stdout); //收到数据后,延迟4秒再发送出去 sleep(4); writen(conn_sk, recvbuf, strlen(recvbuf)); //回传数据 if(--nready <= 0) //判断是否处理完 break; } } } for(int i = 0; i <= maxindex; i++) { close(client_sk[i]); } close(sk_fd); return 0; }
客户端代码
client.c
编译命令
gcc -Wall -g -std=gnu99 client.c -o client
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <stdbool.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <string.h> #include <sys/select.h> #define handle_error(msg) \ do{perror(msg);exit(EXIT_FAILURE);}while(0) ssize_t readn(int fd, void *buf, size_t count) { if((fd < 0) || (buf == NULL) || (count < 0)) return -1; size_t nleft = count; //剩余字节数 ssize_t nread = 0; //已读字节数 char *pbuf = (char*)buf; while(nleft > 0) { if((nread = read(fd, pbuf, nleft)) < 0) { if(errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; pbuf += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { if((fd < 0) || (buf == NULL) || (count < 0)) return -1; size_t nleft = count; //剩余字节数 ssize_t nwritten = 0; //已发送字节数 char *pbuf = (char*)buf; while (nleft > 0) { if((nwritten = write(fd, pbuf, nleft)) < 0) { if(errno == EINTR) continue; return -1; } else if(nwritten == 0) continue; pbuf += nwritten; nleft -= nwritten; } return count; } //使用recv函数从套接字接收缓冲区中接收数据,但并不从缓冲去中清除数据 ssize_t recv_peek(int sockfd, void *buf, size_t len) { while(true) { int iret = recv(sockfd, buf, len, MSG_PEEK); if(iret == -1 && errno == EINTR) //如果失败是因为信号中端,那么就重新再试 continue; return iret; } } ssize_t recvline(int sockfd, void *buf, size_t maxlen) { int iret = 0; int nread = 0; //已读数据 char *pbuf = (char*)buf; int nleft = maxlen; //剩余字符 while(true) { iret = recv_peek(sockfd, pbuf, nleft); if(iret < 0) return iret; //读取失败 else if(iret == 0) return iret; //对方关闭套接口 nread = iret; if(nread > nleft) //已经读取到的数据只可能小于或者等于剩余的数据 exit(EXIT_FAILURE); for(int i = 0; i < nread; i++) { if(pbuf[i] == '\n') { iret = readn(sockfd, pbuf, i+1); //从缓冲区中读走包括\n在内的数据 if(iret != i+1) exit(EXIT_FAILURE); //没有读取都i+1个数据,说明失败 return iret; //读取都\n返回 } } //在当前读到的数据中没有发现\n,那么先将这部分数据从缓冲区中读走,然后接着偷窥后面的数据 nleft -= nread; iret = readn(sockfd, pbuf, nread); if(iret != nread) exit(EXIT_FAILURE); pbuf += nread; } return -1; } //select可看做一个管理者,可用它来管理多个IO, //一旦其中的一个IO或这多个IO发生了我们感兴趣的事件,select函数就返回, //返回值为检测到的个数,并且会告诉我们哪些IO发生了事件 //参数: //1、读写异常集合中的文件描述父的最大值+1 //读集合,输入输出参数 //写集合,输入输出参数 //异常集合,输入输出参数 //超时结构体,设置此结构体后,可设定让select函数返回的超时时间 void client_handler(int sk_fd) { fd_set readset; FD_ZERO(&readset); int nready = 0; int fd_stdin = fileno(stdin); //获得标准输入的描述符,防止输入重定向 int fd_max = fd_stdin > sk_fd ? fd_stdin : sk_fd; bool stdinflag = true; //表示是否需要select监听stdin while(true) { if(stdinflag) FD_SET(fd_stdin, &readset); FD_SET(sk_fd, &readset); //readset集合可能会改变,所以每次都要添加 nready = select(fd_max + 1, &readset, NULL, NULL, NULL); if(nready == -1) handle_error("select"); if(nready == 0) continue; if(FD_ISSET(sk_fd, &readset)) //套接口产生了读事件 { char recvbuf[1024] = {0}; int iret = recvline(sk_fd, recvbuf, sizeof(recvbuf)); //接收包数据长度 if(iret == -1) handle_error("read"); else if(iret == 0) { printf("Server was closed!\n"); break; } fputs(recvbuf, stdout); memset(&recvbuf, 0, sizeof(recvbuf)); } if(FD_ISSET(fd_stdin, &readset)) //标准输入产生读事件 { char sendbuf[1024] = {0}; if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) { //如果从标准输入接收到EOF,表示我们想关闭发送端口 stdinflag = false; //让select不再对stdin进行监听 printf("Ctrl + D\n"); shutdown(sk_fd, SHUT_WR); //只关闭写端,对方会收到FIN段 } else { writen(sk_fd, sendbuf, strlen(sendbuf)); //发送数据 memset(&sendbuf, 0, sizeof(sendbuf)); } } } } int main(void) { int sk_fd; sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP); if(sk_fd < 0) handle_error("socket"); struct sockaddr_in sr_addr; memset(&sr_addr,0,sizeof(sr_addr)); sr_addr.sin_family = AF_INET; sr_addr.sin_port = htons(5188); //sr_addr.sin_addr.s_addr = htonl(INADDR_ANY); sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //inet_aton("127.0.0.1",&sr_addr.sin_addr); if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0) { close(sk_fd); handle_error("connect"); } client_handler(sk_fd); close(sk_fd); return 0; }
相关文章推荐
- win10无法连接网络怎么办?Win10提示无法连接此网络的原因与解决方案
- IOS网络第四天 -网络文件上传(0923略)
- IOS网络第三天 - 01-网络文件下载(0922略)
- 网络地址转换NAT
- 图片的三级缓存策略(内存LruCache+磁盘DiskLruCache+网络Volley)
- android手势识别(转) http://www.eoeandroid.com/thread-229581-1-1.html (出处: eoe 移动开发者论坛)
- 启用selinux后,php访问网络异常
- IOS网络第二天 - 09-多值参数
- IOS网络第二天 - 07-发送JSON给服务器
- IOS网络第二天 - 06-POST请求
- IOS网络第二天 - 05-黑酷-NSXMLparser解析(略)
- IOS网络第二天 - 04-黑酷-GDataXML 解析
- SGU326Perspective(网络流量的最大流量)(经典赛车模型)
- IOS网络第二天 - 03-JSON显示数据,调用本地视频播放,数据转模型
- Wireshark使用方法(学习笔记二——查询统计) http://www.2cto.com/Article/201201/115880.html
- android网络框架的封装——volley
- Live555用做RTSPClient时,利用RTP时间戳进行音视频同步的解决方案(必须有RTCP支持才可行)
- 浅谈 Linux 内核开发之网络设备驱动
- 【网络通信:Volley】请求的发送与响应之String
- 计算机网络--NAT