前面提到,connect发生EINTR错误时,是不能重新启动的。那怎么办呢,是关闭套接字还是直接退出进程呢?如果EINTR前,三次握手已经发起,我们当然希望链路就此已经建立完成,不要再重新走流程了。这个时候我们就需要为connect量身定做一个使用方案。代码如下:
1 STATUS connectWithTimeout(int sock, struct sockaddr*addrs, int adrsLen,struct timeval* tm) 2 { 3 int err = 0; 4 int len = sizeof(int); 5 int flag; 6 int ret; 7 fd_set set; 8 struct timeval mytm; 9 10 if(tm!=NULL){ 11 memcpy(&mytm, tm, sizeof(struct timeval)); 12 } 13 14 flag = 1; 15 ioctl(sock,FIONBIO,&flag); 16 17 ret = connect(sock, addrs, adrsLen); 18 if ( ret == -1 ) 19 { 20 if ( EINPROGRESS == errno ) 21 { 22 FD_ZERO(&set); 23 FD_SET(sock,&set); 24 if ( select(sock+1,NULL,&set,NULL,tm) > 0 ) 25 { 26 getsockopt(sock,SOL_SOCKET,SO_ERROR,&err,(socklen_t*)&len); 27 if ( 0 == err ) 28 ret = OK; 29 else 30 ret = ERROR; 31 } 32 else 33 { 34 ret = ERROR; 35 } 36 } 37 } 38 39 flag = 0; 40 ioctl(sock,FIONBIO,&flag); 41 42 if(tm!=NULL){ 43 memcpy(tm, &mytm, sizeof(struct timeval)); 44 } 45 46 return ret; 47 }
这部分代码是从原有工程里抠出来的,在学习完前面知识后,我觉得有些地方不是很完善,按照下面的代码做了修改。此处将老代码贴出只是为了防止自己的理解有误,做了画蛇添足的事情。新代码如下:
1 STATUS connectWithTimeout(int sock, struct sockaddr* addrs, int adrsLen,struct timeval* tm) 2 { 3 int err = 0; 4 int len = sizeof(int); 5 int flag; 6 int ret = -1; 7 int retselect = -1; 8 fd_set set; 9 struct timeval mytm; 10 11 if (tm != NULL){ 12 memcpy(&mytm, tm, sizeof(struct timeval)); 13 } 14 15 flag = 1; 16 ioctl(sock,FIONBIO,&flag); 17 18 ret = connect(sock, addrs, adrsLen); 19 if (-1 == ret) 20 { 21 if (EINPROGRESS == errno) 22 { 23 reselect: 24 FD_ZERO(&set); 25 FD_SET(sock,&set); 26 if ((retselect = select(sock+1, NULL, &set, NULL, tm)) > 0) 27 { 28 if (FD_ISSET(sock, &set)) 29 { 30 getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len); 31 if (0 == err) 32 ret = 0; 33 else 34 ret = -1; 35 } 36 37 } 38 else if (retselect < 0) 39 { 40 if (EINTR == errno) 41 { 42 printf("error! errno = %s:%d\n", strerror(errno), errno); 43 goto reselect; 44 } 45 } 46 } 47 } 48 else if (0 == ret) 49 { 50 ret = 0; //OK 51 } 52 53 flag = 0; 54 ioctl(sock, FIONBIO, &flag); 55 56 if (tm != NULL){ 57 memcpy(tm, &mytm, sizeof(struct timeval)); 58 } 59 60 return ret; 61 }
这是一个非阻塞式connect的模型,更为详细的介绍可以看《UNIX网络编程》(我真不是个做广告的)。但此处模型个人认为逻辑上还是完善的。
15-16行:设置套接口为非阻塞方式,这个步骤需要视使用环境而定,因此这里存在一个移植性问题。
18-21行:非阻塞模式下的connect,调用后立即返回,如果已经errno为EINPROGRESS,说明已经发起了三次握手。否则为异常。
23-45行:使用select去监测套接口状态。实现规定(那些乱七八糟的不同实现,我一直搞不清楚,哎,接触的不多!大家可以自己去查查):(1)当连接建立成功时,描述字变为可写,(2)当连接建立遇到错误时,描述字变为既可读又可写。我是个比较投机的人,归纳一下,描述字变为可写时说明连接有结果了。30行使用getsockopt获取描述字状态(使用SO_ERROR选项)。建立成功err为0,否则为-1。对于一个商业软件的“贡献者”,我们不能放过任何出错处理,但这里不对getsockopt的返回值进行出错处理是有原因的。在异常情况下,有些实现该调用返回0,有些则返回-1,因此我们不能根据它的返回值来判断连接是否异常。38-45行,前面已经提到,select这个阻塞的家伙很有可能发生EINTR错误,我们也必须兼容这种错误。注意reselect的位置,自己使用时位置放错了效果可是截然不同的。
48-51行:connect有时反应很快啊,一经调用就成功建立连接了,这种情况是存在的。所以我们也要兼容这种情况。
后面行:恢复调用前状态。
又到了热血澎湃的总结时刻:以前,看到有些地方使用非阻塞的connect,真的很费解,为什么简单的connect不直接使用,还要搞那么多花样?现在其实我们应该可以想明白了,作为一个完美程序的追求者,着眼于代码长期地维护(自己太高尚了),阻塞的connect确实会存在很多的问题。那是否存在一个稳健的阻塞的connect版本,说实话,我没仔细想过,粗略地想想那应该是个很复杂的东西,比如:connect返回时是否已经开始三次握手等等?因此,阻塞的或者非阻塞的connect目前并非再是二者选其一、根据喜好选择使用的问题了,而是必须使用非阻塞的connect。这个结论或许很武断,但本人通过目前了解的知识得出了这样的结论,还是希望有大神突然出现,指点一二。
这是《UNIX网络编程》上的原装问题,新发现的,按照书上的说法,貌似很严重,我要先测试一下。在我给出一个比较稳健的程序之前不允许存在已知的类似问题。又要牵涉到SO_LINGER选项了,真是有点无法自圆自说的感觉。就当初探SO_LINGER选项吧,现在它只是配角,后面有机会详细研究一下它。
服务器代码,这个代码同样可以用来测试select发生的EINTR的问题:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <sys/types.h> 6 #include <sys/socket.h> 7 #include <netinet/in.h> 8 #include <arpa/inet.h> 9 #include <signal.h> 10 #include <errno.h> 11 12 #define PORT 1234 13 #define BACKLOG 5 14 #define MAXDATASIZE 1000 15 16 void sig_chld(int signo) 17 { 18 pid_t pid; 19 int stat = 0; 20 21 pid = wait(&stat); 22 printf("child %d terminated(stat:%d)\n", pid, stat); 23 24 return; 25 } 26 27 void signal_ex(int signo, void* func) 28 { 29 struct sigaction act, oact; 30 31 act.sa_handler = func; 32 sigemptyset(&act.sa_mask); //清空此信号集 33 act.sa_flags = 0; 34 35 if (sigaction(signo, &act, &oact) < 0) 36 { 37 printf("sig err!\n"); 38 } 39 40 //sigaction(SIGINT, &oact, NULL); //恢复成原始状态 41 return; 42 } 43 44 int main() 45 { 46 int listenfd, connectfd; 47 struct sockaddr_in server; 48 struct sockaddr_in client; 49 socklen_t addrlen; 50 char szbuf[MAXDATASIZE + 1] = {0}; 51 int num = 0; 52 pid_t pid_child; 53 int ret; 54 fd_set set; 55 struct timeval mytm; 56 57 if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 58 { 59 perror("Creating socket failed."); 60 exit(1); 61 } 62 63 int opt = SO_REUSEADDR; 64 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 65 66 bzero(&server, sizeof(server)); 67 server.sin_family = AF_INET; 68 server.sin_port = htons(PORT); 69 server.sin_addr.s_addr = htonl(INADDR_ANY); 70 if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1) 71 { 72 perror("Bind()error."); 73 exit(1); 74 } 75 if (listen(listenfd, BACKLOG) == -1) 76 { 77 perror("listen()error\n"); 78 exit(1); 79 } 80 81 signal_ex(SIGCHLD, sig_chld); 82 while (1) 83 { 84 addrlen = sizeof(client); 85 sleep(10); 86 printf("start accept!\n"); 87 if ((connectfd = accept(listenfd, (struct sockaddr*)&client, &addrlen)) == -1) 88 { 89 #if 1 90 if (EINTR == errno) 91 { 92 printf("EINTR!\n"); 93 continue; 94 } 95 #endif 96 97 perror("accept()error\n"); 98 exit(1); 99 } 100 printf("You got a connection from cient's ip is %s, prot is %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port)); 101 102 if (0 == (pid_child = fork())) 103 { 104 close(connectfd); 105 close(listenfd); 106 printf("child a ha!\n"); 107 sleep(5); 108 exit(0); 109 } 110 111 mytm.tv_sec = 15; 112 mytm.tv_usec = 0; 113 reselect: 114 FD_ZERO(&set); 115 FD_SET(connectfd, &set); 116 if ((ret = select(connectfd + 1, &set, NULL, NULL, &mytm)) > 0) 117 { 118 if(FD_ISSET(connectfd, &set)) 119 { 120 printf("connectfd can be readn!\n"); 121 } 122 } 123 else if (0 == ret) 124 { 125 printf("timeout!\n"); 126 } 127 else if (ret < 0) 128 { 129 //perror("error! "); 130 if (EINTR == errno) 131 { 132 printf("error! errno = %s:%d\n", strerror(errno), errno); 133 goto reselect; 134 } 135 } 136 137 close(connectfd); 138 connectfd = -1; 139 } 140 141 close(listenfd); 142 143 return 0; 144 }
客户端代码:
1 #include <sys/types.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <netdb.h> 5 #include <signal.h> 6 7 #define PORT 1234 8 #define MAXDATASIZE 1000 9 10 int main(int argc, char *argv[]) 11 { 12 int sockfd = -1; 13 struct sockaddr_in server; 14 struct linger ling; 15 16 if (argc != 2) 17 { 18 printf("Usage:%s <IP Address>\n", argv[0]); 19 exit(1); 20 } 21 22 signal(SIGPIPE, SIG_IGN); 23 24 if ((sockfd=socket(AF_INET, SOCK_STREAM, 0)) == -1) 25 { 26 printf("socket()error\n"); 27 exit(1); 28 } 29 bzero(&server, sizeof(server)); 30 server.sin_family = AF_INET; 31 server.sin_port = htons(PORT); 32 server.sin_addr.s_addr = inet_addr(argv[1]); 33 if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1) 34 { 35 printf("connect()error\n"); 36 exit(1); 37 } 38 39 ling.l_onoff = 1; 40 ling.l_linger = 0; 41 setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling)); 42 43 close(sockfd); 44 45 return 0; 46 }
书上提到,有些实现内核会抛一个错误给应用进程,应用进程在accept时会返回错误。而有些实现内核完全能够兼容这种问题,不对应用进程产生任何影响。强大的linux显然是后者,客户端发送RST后,服务器accept没有返回任何错误,运行正常。
因此,该问题暂且略过,实际使用时还是应当注意这种极端的情况。
写到这,赶紧要为这个标题画句号了。一边写一边学,再这么下去就成无底洞了。而UDP的相关知识自己本身用得不多,在这不敢下笔,等哪一天心中有物再来详细整理一下。因此,这边要很不负责任地给出一个最终版本的TCP代码了。
个人认为,网络编程还有很多知识,但是该代码已经可以应付一个新手解决很多实际的问题了。之所以叫“快速上手”,干得也就是这种事,离精通还是很远的。之后,有什么问题再以专题的形式给出吧。
先是服务器代码:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <sys/types.h> 6 #include <sys/socket.h> 7 #include <netinet/in.h> 8 #include <arpa/inet.h> 9 #include <signal.h> 10 #include <errno.h> 11 12 #define PORT 1234 13 #define BACKLOG 5 14 #define MAXDATASIZE 1000 15 #define TEST_STRING "HELLO, WORLD!" 16 #define TEST_STRING_LEN strlen(TEST_STRING) 17 18 int readn(int connfd, void *vptr, int n) 19 { 20 int nleft; 21 int nread; 22 char *ptr; 23 int ret = -1; 24 struct timeval select_timeout; 25 fd_set rset; 26 27 ptr = (char*)vptr; 28 nleft = n; 29 30 while (nleft > 0) 31 { 32 FD_ZERO(&rset); 33 FD_SET(connfd, &rset); 34 select_timeout.tv_sec = 5; 35 select_timeout.tv_usec = 0; 36 if ((ret = select(connfd+1, &rset, NULL, NULL, &select_timeout)) < 0) 37 { 38 if (errno == EINTR) 39 { 40 continue; 41 } 42 else 43 { 44 return -1; 45 } 46 } 47 else if (0 == ret) 48 { 49 return -1; 50 } 51 if ((nread = recv(connfd, ptr, nleft, 0)) < 0) 52 { 53 if(errno == EINTR) 54 { 55 nread = 0; 56 } 57 else 58 { 59 return -1; 60 } 61 } 62 else if (nread == 0) 63 { 64 break; 65 } 66 nleft -= nread; 67 ptr += nread; 68 } 69 70 return(n - nleft); 71 } 72 73 void sig_chld(int signo) 74 { 75 pid_t pid; 76 int stat = 0; 77 78 pid = wait(&stat); 79 80 return; 81 } 82 83 void signal_ex(int signo, void* func) 84 { 85 struct sigaction act, oact; 86 87 act.sa_handler = func; 88 sigemptyset(&act.sa_mask); //清空此信号集 89 act.sa_flags = 0; 90 91 if (sigaction(signo, &act, &oact) < 0) 92 { 93 printf("sig err!\n"); 94 } 95 96 //sigaction(SIGINT, &oact, NULL); //恢复成原始状态 97 return; 98 } 99 100 int main() 101 { 102 int listenfd, connectfd; 103 struct sockaddr_in server; 104 struct sockaddr_in client; 105 socklen_t addrlen; 106 char szbuf[MAXDATASIZE + 1] = {0}; 107 int num = 0; 108 pid_t pid_child; 109 110 signal(SIGPIPE, SIG_IGN); 111 112 if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 113 { 114 perror("Creating socket failed."); 115 exit(1); 116 } 117 118 int opt = SO_REUSEADDR; 119 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 120 121 bzero(&server, sizeof(server)); 122 server.sin_family = AF_INET; 123 server.sin_port = htons(PORT); 124 server.sin_addr.s_addr = htonl(INADDR_ANY); 125 if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1) 126 { 127 perror("Bind()error."); 128 exit(1); 129 } 130 if (listen(listenfd, BACKLOG) == -1) 131 { 132 perror("listen()error\n"); 133 exit(1); 134 } 135 136 signal_ex(SIGCHLD, sig_chld); 137 while (1) 138 { 139 addrlen = sizeof(client); 140 printf("start accept!\n"); 141 if ((connectfd = accept(listenfd, (struct sockaddr*)&client, &addrlen)) == -1) 142 { 143 if (EINTR == errno) 144 { 145 printf("EINTR!\n"); 146 continue; 147 } 148 149 perror("accept()error\n"); 150 exit(1); 151 } 152 printf("You got a connection from cient's ip is %s, prot is %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port)); 153 154 if (0 == (pid_child = fork())) 155 { 156 while (1) 157 { 158 num = readn(connectfd, szbuf, TEST_STRING_LEN); 159 if (num < 0) 160 { 161 printf("read error!\n"); 162 break; 163 } 164 else if (0 == num) 165 { 166 printf("read over!\n"); 167 break; 168 } 169 else 170 { 171 printf("recv: %s\n", szbuf); 172 } 173 } 174 close(connectfd); 175 close(listenfd); 176 sleep(5); 177 exit(0); 178 } 179 180 close(connectfd); 181 connectfd = -1; 182 } 183 184 close(listenfd); 185 186 return 0; 187 }
客户端:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <sys/types.h> 6 #include <sys/socket.h> 7 #include <netinet/in.h> 8 #include <netdb.h> 9 #include <signal.h> 10 #include <errno.h> 11 #include <fcntl.h> 12 13 #define PORT 1234 14 #define MAXDATASIZE 1000 15 #define TEST_STRING "HELLO, WORLD!" 16 #define TEST_STRING_LEN strlen(TEST_STRING) 17 18 int writen(int connfd, void *vptr, size_t n) 19 { 20 int nleft, nwritten; 21 char *ptr; 22 23 ptr = (char*)vptr; 24 nleft = n; 25 26 while (nleft > 0) 27 { 28 if ((nwritten = send(connfd, ptr, nleft, MSG_NOSIGNAL)) == -1) 29 { 30 if (errno == EINTR) 31 { 32 nwritten = 0; 33 } 34 else 35 { 36 return -1; 37 } 38 } 39 nleft -= nwritten; 40 ptr += nwritten; 41 } 42 43 return(n); 44 } 45 46 int connectWithTimeout(int sock, struct sockaddr* addrs, int adrsLen,struct timeval* tm) 47 { 48 int err = 0; 49 int len = sizeof(int); 50 int flag; 51 int ret = -1; 52 int retselect = -1; 53 fd_set set; 54 struct timeval mytm; 55 56 if (tm != NULL){ 57 memcpy(&mytm, tm, sizeof(struct timeval)); 58 } 59 60 flag = fcntl(sock, F_GETFL, 0); 61 fcntl(sock, F_SETFL, flag | O_NONBLOCK); //linux用这个 62 // flag = 1; 63 // ioctl(sock,FIONBIO,&flag); 64 65 ret = connect(sock, addrs, adrsLen); 66 if (-1 == ret) 67 { 68 if (EINPROGRESS == errno) 69 { 70 reselect: 71 printf("start check!\n"); 72 FD_ZERO(&set); 73 FD_SET(sock,&set); 74 if ((retselect = select(sock+1, NULL, &set, NULL, tm)) > 0) 75 { 76 if (FD_ISSET(sock, &set)) 77 { 78 getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len); 79 if (0 == err) 80 ret = 0; 81 else 82 ret = -1; 83 } 84 85 } 86 else if (retselect < 0) 87 { 88 if (EINTR == errno) 89 { 90 printf("error! errno = %s:%d\n", strerror(errno), errno); 91 goto reselect; 92 } 93 } 94 } 95 } 96 else if (0 == ret) 97 { 98 printf("OK at right!\n"); 99 ret = 0; //OK 100 } 101 102 fcntl(sock, F_SETFL, flag); 103 // flag = 0; 104 // ioctl(sock, FIONBIO, &flag); 105 106 if (tm != NULL){ 107 memcpy(tm, &mytm, sizeof(struct timeval)); 108 } 109 110 return ret; 111 } 112 113 int main(int argc, char *argv[]) 114 { 115 int sockfd, num; 116 char szbuf[MAXDATASIZE] = {0}; 117 struct sockaddr_in server; 118 struct timeval timeOut; 119 int ret = -1; 120 int iSendTime = 0; 121 122 if (argc != 2) 123 { 124 printf("Usage:%s <IP Address>\n", argv[0]); 125 exit(1); 126 } 127 128 signal(SIGPIPE, SIG_IGN); 129 130 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 131 { 132 printf("socket()error\n"); 133 exit(1); 134 } 135 136 bzero(&server, sizeof(server)); 137 server.sin_family = AF_INET; 138 server.sin_port = htons(PORT); 139 server.sin_addr.s_addr = inet_addr(argv[1]); 140 141 timeOut.tv_sec = 5; 142 timeOut.tv_usec = 0; 143 ret = connectWithTimeout(sockfd, (struct sockaddr *)&server, sizeof(server), &timeOut); 144 if (-1 == ret) 145 { 146 printf("connect()error\n"); 147 exit(1); 148 } 149 150 memset(szbuf, 0, sizeof(szbuf)); 151 strcpy(szbuf, TEST_STRING); 152 153 while (iSendTime < 5) 154 { 155 ret = writen(sockfd, szbuf, TEST_STRING_LEN); 156 if (TEST_STRING_LEN != ret) 157 { 158 break; 159 } 160 else 161 { 162 printf("%dth send success!\n", iSendTime); 163 iSendTime++; 164 } 165 } 166 167 close(sockfd); 168 169 return 0; 170 }
总结的时候,有很多知识都得到了更新,最终代码对部分函数进行了完善。当然还有一些东西没有考虑进去,比如说对端掉电、路由器损坏等问题,以上程序都无法很好的适应这些问题。后续再慢慢改进吧。
最终的例子功能很简单,最主要还是在socket编程的各个细节的处理上。
在此要说OVER了!