Linux下的socket编程实践(四)TCP服务端优化和常见函数
并发下的僵尸进程处理
只有一个进程连接的时候,我们可以使用以下两种方法处理僵尸进程:
1)通过忽略SIGCHLD信号,避免僵尸进程
在server端代码中添加
signal(SIGCHLD, SIG_IGN);
2)通过wait/waitpid方法,解决僵尸进程
- signal(SIGCHLD,onSignalCatch);
- void onSignalCatch(int signalNumber)
- {
- wait(NULL);
- }
我们可以用下面的客户端代码测试:
- /** client端实现的测试代码**/
- int main()
- {
- int sockfd[50];
- for (int i = 0; i < 50; ++i)
- {
- if ((sockfd[i] = socket(AF_INET, SOCK_STREAM, 0)) == -1)
- err_exit("socket error");
- struct sockaddr_in serverAddr;
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(8001);
- serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
- if (connect(sockfd[i], (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)
- err_exit("connect error");
- }
- sleep(20);
- }
解决方法:
使用循环的 waitpid函数就可以将所有的子进程留下的僵尸进程处理掉
- void sigHandler(int signo)
- {
- while (waitpid(-1, NULL, WNOHANG) > 0)
- ;
- }
- //pid=-1 等待任何子进程,相当于 wait()。
- //WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
- #include <sys/socket.h>
- int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //获取本地addr结构
- int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //获取对方addr结构
- int gethostname(char *name, size_t len);
- int sethostname(const char *name, size_t len);
- #include <netdb.h>
- extern int h_errno;
- struct hostent *gethostbyname(const char *name);
- #include <sys/socket.h> /* for AF_INET */
- struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
- struct hostent *gethostent(void);
- //hostent结构体
- struct hostent
- {
- char *h_name; /* official name of host */
- char **h_aliases; /* alias list */
- int h_addrtype; /* host address type */
- int h_length; /* length of address */
- char **h_addr_list; /* list of addresses */
- }
- #define h_addr h_addr_list[0] /* for backward compatibility */
这两个函数调用的时机很重要,否则不能得到正确的地址和端口:
TCP
对于服务器来说,在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。
对于客户端来说,在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),当然如果调用了bind 以后可以使用getsockname。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getsockname的时机。
UDP
UDP分为链接和没有链接2种(这个到UDP与connect可以找到相关内容)
没有链接的UDP不能调用getpeername,但是可以调用getsockname,和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后
已经链接的UDP,在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。
- /**获取本机IP列表**/
- int gethostip(char *ip)
- {
- struct hostent *hp = gethostent();
- if (hp == NULL)
- return -1;
- strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr));
- return 0;
- }
- int main()
- {
- char host[128] = {0};
- if (gethostname(host, sizeof(host)) == -1)
- err_exit("gethostname error");
- cout << "host-name: " << host << endl;
- struct hostent *hp = gethostbyname(host);
- if (hp == NULL)
- err_exit("gethostbyname error");
- cout << "ip list: " << endl;
- for (int i = 0; hp->h_addr_list[i] != NULL; ++i)
- {
- cout << '\t'
- << inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]) << endl;
- }
- char ip[33] = {0};
- gethostip(ip);
- cout << "local-ip: " << ip << endl;
- }
1.客户端和服务器连接建立的时候,双方处于ESTABLISHED(建立)状态
2.关于TIME_WAIT状态 详见 http://www.mamicode.com/info-detail-190400.html
3.TCP/IP协议的第11种状态:图上只包含10种状态,还有一种CLOSING状态
产生CLOSING状态的原因:
Server端与Client端同时关闭(同时调用close,此时两端同时给对端发送FIN包),将产生closing状态,最后双方都进入TIME_WAIT状态。(因为主动关闭的一方会进入TIME_WAIT状态,双方同时主动关闭,则都进入)
SIGPIPE信号
往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据;但是在收到RST段之后,如果还继续写,调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可。
signal(SIGPIPE, SIG_IGN);
SIGPIPE,虽然已经接受到FIN,但是我还可以发送数据给对方;如果对方已经不存在了,那么TCP会进行重置,TCP协议栈发送RST段,收到RST后,再进行write会产生SIGPIPE信号。
其实很好理解:TCP可以看作是一个全双工的管道,读端信号不存在了,如果再对管道进行写的话会导致SIGIPPE信号的产生,处理的时候是忽略这个信号就可以了,其实就是按照管道的规则。
我们测试的时候在Client发送每条信息都发送两次,当Server端关闭之后Server端会发送一个FIN分节给Client端, 第一次消息发送之后, Server端会发送一个RST分节给Client端;第二次消息发送(调用write)时, 会产生SIGPIPE信号。
close和shutdown函数的区别
- #include <unistd.h>
- int close(int fd);
- #include <sys/socket.h>
- int shutdown(int sockfd, int how);
shutdown的how参数 | |
SHUT_RD |
关闭读端 |
SHUT_WR |
关闭写端 |
SHUT_RDWR |
读写均关闭 |
shutdowm how=1就可以保证对等方接收到一个EOF字符,而不管其他进程是否已经打开可套接字。
而close不能保证,直到套接字引用计数减为0的时候才发送。也就是说知道所有进程都关闭了套接字。
调用close函数,可能导致全双工的管道还没有回射给客户端时,产生丢失数据现象;例如
FIN D C B A
A B C D ----》丢失,已经关闭close
Close准确的含义是 套接字引用计数减为0 的时候,才发送FIN
- int conn;
- conn=accept(sock,NULL,NULL);
- pid_t pid=fork();
- if(pid==-1)
- ERR_EXIT("fork");
- if(pid==0)
- {
- close(sock);
- //通信
- close(conn); //这时才会向对方发送FIN段(因为这个时候conn引用计数减为0)
- }
- else if(pid>0)
- close (conn); //不会向客户端发送FIN段,仅仅只是将套接字的引用计数减1