服务器编程入门(10)TCP回射服务器实现 - 并发
问题聚焦:
在前面我们大概浏览了一下服务器编程需要掌握的一些知识和技术,以及架构思想。
实践,才是检验真理的唯一标准。。从这节起我们将在这些技术的基础上,一步步实现以及完善一个服务器,同时也是对这些技术的更深入的思考。
本节我们将实现一个简单的回射服务器,包括TCP连接,文本处理,并发(多进程实现),以及子进程退出后的处理动作。
功能描述:
客户端与服务器端进行TCP连接
客户端从标准输入(键盘)读入一行文本,发送给服务器
服务器从网络输入读取该行文本,并回射给客户
客户从网络输入读入这行文本,并显示在标准输出(终端显示器)上
支持并发(多进程实现)
封装:有,封装中处理了异常,报异常后退出程序。
架构:
语言:
C++(因为考虑到后面重构会用类来封装,所以这里选择C++来实现,在本节基本没有体现出来。)
编译环境:
Ubuntu12.04 g++
服务端代码:只贴出来关键代码,API翻翻书都看得懂,主要供自己大家参考这个流程
#include "mtserver.h" int main(int argc, char* argv[]) { checkArgc(argc, 2); const char* ip = argv[1]; int port = atoi( argv[2] ); /* 1 declare socket*/ int listenfd, connfd; int ret; /* 2 initialize listen socket*/ mySocket(listenfd); /* 3 server address */ struct sockaddr_in servaddr; initSockAddr(servaddr, ip, port); /* 4 bind */ myBind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); /* 5 listen */ myListen(listenfd, 5); /* handle SIGCHLD signal*/ signal(SIGCHLD, handle_sigchild); /* 6 waiting for connecting */ pid_t chipid; socklen_t clilen; struct sockaddr_in cliaddr; for(;;) { clilen = sizeof(cliaddr); std::cout << "Waiting for connecting ..." << std::endl; connfd = myAccept(listenfd, (struct sockaddr*)&cliaddr, &clilen); printf("Connection %d established...\n", listenfd); if ( (chipid=fork()) == 0 ) { handle_recv(connfd); } } }
服务器端消息处理函数:
void handle_recv(int connfd) { char recvbuf[BUFSIZE]; while(1) { memset( recvbuf, '\0', BUFSIZE ); if ( recv(connfd, recvbuf,BUFSIZE,0) != 0) { if (!strcmp(recvbuf, "exit")) break; fprintf(stderr,"recv msg: %s\n", recvbuf); send(connfd, recvbuf, strlen(recvbuf), 0); fprintf(stderr,"send back: %s\n\n", recvbuf); } } close(connfd); exit(0); } void handle_sigchild(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) { fprintf(stderr, "child %d terminated\n", pid); return; } }
客户端代码:
#include "mtclient.h" int main(int argc, char* argv[]) { if (argc <=2 ) { std::cout << "server ip and port needed." << std::endl; return 1; } int port = atoi(argv[2]); char* ip = argv[1]; int sockfd; struct sockaddr_in servaddr; mySocket(sockfd); initSockAddr(servaddr,ip, port); myConnect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); handle_msg(sockfd); exit(0); }
客户端消息处理函数:
void handle_msg(int sockfd) { char sendbuf[BUFSIZE]; char recvbuf[BUFSIZE]; while(1) { memset( sendbuf, '\0', BUFSIZE ); memset( recvbuf, '\0', BUFSIZE ); printf("%s", "send msg:"); gets(sendbuf); if (strlen(sendbuf) > 0) send(sockfd,sendbuf,strlen(sendbuf),0); if ( !strcmp(sendbuf, "exit")) break; recv(sockfd,recvbuf,BUFSIZE,0); printf("recv back:%s\n\n", recvbuf); } close( sockfd ); return; }
代码里处理了僵尸进程的问题,下面重点看一下:
处理SIGCHLD信号
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。
僵尸进程:占用内核空间,最终可能导致耗尽进程资源
所以,无论何时我们fork子进程都要wait它们。
当子进程结束后,会向父进程返回一个SIGCHLD信号,我们要捕获这个信号,并及时处理,防止出现僵尸进程
捕获的方法是signal系统调用,设置这个捕获的时机为在listen系统调用之后,fork子进程之前,且只做一次
signal系统调用
#include <signal.h> _sighandler_t signal ( int sig, _sighandler_t _handler );
参数说明:
sig:要捕获的信号类型
_handler:指定信号sig的处理函数
慢系统调用:如accept,指那些可能永远阻塞的系统调用
适用于慢系统调用的规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。有些内核自动重启某些被中断的系统调用。
如果内核不自动重启这些系统调用,那么需要我们手动检查返回错误并处理
for( ; ; ) { chilen = sizeof(cliaddr); if ( (connfd = accept(listenfd, (struct sockaddr* ) &cliaddr, &clilen )) < 0 ) { if (errno == EINTR) // 检测到错误类型为EINTR时,重启accept系统调用 continue; else err_sys ("accept error"); } }
我的环境是Ubuntu12.04,这里的accept是自动重启的,不过还是对EINTR信号进行了判断。
wait和waitpid函数
#include <sys/wait.h> pid_t wait( int *statloc ); pid_t waitpid(pid_t pid, int *statloc, int options);
返回:已终止子进程的pid,以及通过statloc指针返回的子进程终止状态(一个整数)。
区别:
调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到现有子进程第一个终止为止。
waitpid函数可以指定等待哪个进程,options参数允许我们指定附加选项,最常用的选项是WNOHANG,它告知内核在没有已终止子进程时不要阻塞。
使用:
void sig_child(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) printf("Child %d terminated\n", pid); return; }
运行截图:
客户端:
服务器端:
小结:我们在网络编程时可能会遇到三种情况
当fork子进程时,必须捕获SIGCHILD信号
当捕获信号时,必须处理被中断的系统调用
SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以避免留下僵死进程
TCP程序例子小结
客户端角度
服务器角度:
I/O复用的需求:
当服务器进程终止,客户进程没被告知。因为虽然客户的TCP确实被告知了,但是客户进程正阻塞于等待用户输入而为接收到该通知。
因此IO复用技术的必要性体现出来了。当然,我们之前就了解了IO复用技术,包括select,poll和epoll。
下一小节我们重点看一下IO复用的实现。
参考资料:
《Linux高性能服务器编程》
《UNIX网络编程 卷1:套接字联网API(第3版)》