前面的章节介绍socket通信的时候,socket的服务端在同一时间只能和一个客户端通信,并不是服务端有多忙,而是因为单进程的程序在同一时间只能做一件事情,不可能一边等待客户端的新连接一边与其它的客户端进行通信。
一、并发的服务端
如果把socket服务端改为多进程,在每次accept到一个客户端的连接后,生成一个子进程,让子进程负责和这个客户端通信,父进程继续accept客户端的连接,socket的服务端在监听新客户端的同时,还可以与多个客户端进行通信。这就是并发,如下图:
1、服务端
把book248.cpp修改一下,改为多进程。
示例(book250.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
|
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>
class CTcpServer { public: int m_listenfd; int m_clientfd;
CTcpServer();
bool InitServer(int port);
bool Accept();
int Send(const void *buf,const int buflen); int Recv(void *buf,const int buflen);
void CloseClient(); void CloseListen();
~CTcpServer(); };
CTcpServer TcpServer;
int main() {
if (TcpServer.InitServer(5051)==false) { printf("服务端初始化失败,程序退出。\n"); return -1; }
while (1) { if (TcpServer.Accept() == false) continue;
if (fork()>0) { TcpServer.CloseClient(); continue; }
TcpServer.CloseListen();
printf("客户端已连接。\n");
char strbuffer[1024];
while (1) { memset(strbuffer,0,sizeof(strbuffer)); if (TcpServer.Recv(strbuffer,sizeof(strbuffer))<=0) break; printf("接收:%s\n",strbuffer);
strcpy(strbuffer,"ok"); if (TcpServer.Send(strbuffer,strlen(strbuffer))<=0) break; printf("发送:%s\n",strbuffer); }
printf("客户端已断开连接。\n");
return 0; } }
CTcpServer::CTcpServer() { m_listenfd=m_clientfd=0; }
CTcpServer::~CTcpServer() { if (m_listenfd!=0) close(m_listenfd); if (m_clientfd!=0) close(m_clientfd); }
bool CTcpServer::InitServer(int port) { if (m_listenfd!=0) { close(m_listenfd); m_listenfd=0; }
m_listenfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ) { close(m_listenfd); m_listenfd=0; return false; }
if (listen(m_listenfd,5) != 0 ) { close(m_listenfd); m_listenfd=0; return false; }
return true; }
bool CTcpServer::Accept() { if ( (m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
return true; }
int CTcpServer::Send(const void *buf,const int buflen) { return send(m_clientfd,buf,buflen,0); }
int CTcpServer::Recv(void *buf,const int buflen) { return recv(m_clientfd,buf,buflen,0); }
void CTcpServer::CloseClient() { if (m_clientfd!=0) { close(m_clientfd); m_clientfd=0; } }
void CTcpServer::CloseListen() { if (m_listenfd!=0) { close(m_listenfd); m_listenfd=0; } }
|
解释一下:
1)在CTcpServer中增加了两个成员函数。
1 2
| void CloseClient(); void CloseListen();
|
2)当有客户端连上来的时候,主进程执行fork,这时候会客户端的socket(m_clientfd)被复制了一份,对父进程来说,只负责监听客户端的连接,不需要与客户端通信,所以父进程关闭m_clientfd,注意,父进程关闭m_clientfd对子进程中的m_clientfd没有影响。
3)当有客户端连上来的时候,主进程执行fork,这时候服务端用于监听的socket(m_listenfd)也会被复制了一份,对子进程来说,只需要与客户端通信,不需要监听客户端的连接,所以子进程关闭监听的m_listenfd,同理,子进程关闭m_listenfd对父进程中的m_listenfd没有影响。
4)子进程执行完任务后,要调用retrun或exit(0)退出,如果没有调用return或exit(0),子进程将又会回到while循环首部。
2、客户端
把book247.cpp修改一下,循环的次数改为50,每次与服务端完成报文交互后sleep一秒,方便观察程序运行的效果。
示例(book249.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>
class CTcpClient { public: int m_sockfd;
CTcpClient();
bool ConnectToServer(const char *serverip,const int port); int Send(const void *buf,const int buflen); int Recv(void *buf,const int buflen);
~CTcpClient(); };
int main() { CTcpClient TcpClient;
if (TcpClient.ConnectToServer("172.16.0.15",5051)==false) { printf("TcpClient.ConnectToServer(\"172.16.0.15\",5051) failed,exit...\n"); return -1; }
char strbuffer[1024]; for (int ii=0;ii<50;ii++) { memset(strbuffer,0,sizeof(strbuffer)); sprintf(strbuffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); if (TcpClient.Send(strbuffer,strlen(strbuffer))<=0) break; printf("发送:%s\n",strbuffer); memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Recv(strbuffer,sizeof(strbuffer))<=0) break; printf("接收:%s\n",strbuffer);
sleep(1); } }
CTcpClient::CTcpClient() { m_sockfd=0; }
CTcpClient::~CTcpClient() { if (m_sockfd!=0) close(m_sockfd); }
bool CTcpClient::ConnectToServer(const char *serverip,const int port) { m_sockfd = socket(AF_INET,SOCK_STREAM,0);
struct hostent* h; if ( (h=gethostbyname(serverip))==0 ) { close(m_sockfd); m_sockfd=0; return false; }
struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(port); memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
if (connect(m_sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0) { close(m_sockfd); m_sockfd=0; return false; }
return true; }
int CTcpClient::Send(const void *buf,const int buflen) { return send(m_sockfd,buf,buflen,0); }
int CTcpClient::Recv(void *buf,const int buflen) { return recv(m_sockfd,buf,buflen,0); }
|
先启动服务端book250,然后启动多个book249,可以看到服务端可以同时与多个客户端进行通信,查看服务端的进行如下:
注意,服务端book250的主程序的while是一个死循环,没有退出机制,可以按Ctrl+c强制中止它,这不是正确的办法,后面我会介绍正确的方法。
二、僵尸进程
1、僵尸进程产生的原因
一个子进程在调用return或exit(0)结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个僵尸进程。
先启动服务端程序book250,然后多次启动客户端程序book249,马上查看book250的进程,如下图:
等全部的客户端book249程序运行完成后,再查看book250的进程,如下图。
被选中的就是僵尸进程,有<defunct>标志。
如果按Ctrl+c终止book250后,父进程退出,僵尸进程随之消失。
2、僵尸进程的危害
僵尸进程是子进程结束时,父进程又没有回收子进程占用的资源。
僵尸进程在消失之前会继续占用系统资源。
如果父进程先退出,子进程被系统接管,子进程退出后系统会回收其占用的相关资源,不会成为僵尸进程。父进和先退出的应用场景在以后的章节中介绍。
3、如何解决僵尸进程
解决僵尸进程的方法有两种。
子进程退出之前,会向父进程发送一个信号,父进程调用waid函数等待这个信号,只要等到了,就不会产生僵尸进程。这话说得容易,在并发的服务程序中这是不可能的,因为父进程要做其它的事,例如等待客户端的新连接,不可能去等待子进程的退出信号,这个方法我就不介绍了。
另一种方法就是父进程直接忽略子进程的退出信号,具体做法很简单,在主程序中启用以下代码:
1
| signal(SIGCHLD,SIG_IGN);
|
signal函数的用法暂时不介绍,以后会有详细说明。
先启动服务端程序book250,然后多次启动客户端程序book249,等book249运行结束后再查看book250的进程,不再有僵尸进程。
三、应用经验
在学习了多进程的基础知识之后,初学者可能会认为多进程是一个高大上的技术,认为多进程处理数据肯定比单进程快,其实不是。在实际开发中,采用多进程的主要目的是处理多个并发的任务,而不是为了提高程序的效率。
从效率方面来说,某些场景下多进程的效率比单进程低,原因很简单,因为在有限的硬件资源中,多进程程序的内存开销更大,还会产生资源的竞争。就像多个人端着一盆水,不如一个人端着一盆水走得快。
四、课后作业
本章节的重点是介绍多进程的应用场景,属于概念性的知识,代码其实很简单,只要各位理解了多进程应用的原理就行了。
但是,文章中提到的知识点,大家一定要用程序去测试它。
五、版权声明
C语言技术网原创文章,转载请说明文章的来源、作者和原文的链接。
来源:C语言技术网(www.freecplus.net)
作者:码农有道