在很早的文章中, 我们一起聊过服务器如何与多个客户端进行通信, 那时, 我们要么用select, 要么用多线程, 却没有用多进程。 其实, 多进程也可以实现与多个客户端进行通信。
如果是在while中循环accept, 然后循环处理事情, 此时, 这种服务是迭代服务, 只能逐一处理客户端的请求, 后一个请求必须等前一个请求处理完毕, 无法并发处理, 真是急死人。 要实现并发, 我们可以考虑多线程, 也可以考虑多进程, 本文来说说后者。 在我们的多进程服务器模型中, 我们用父进程来处理连接(监听socket), 用fork子进程的方法来处理通信(通信socket), 各司其职, 美哉。
一旦涉及到fork, 就必须注意僵尸进程的处理, 所以, 我们要用waitpid进行收尸, 这一点, 我们已经说过了。 另外, 要注意, 父子进程共享socket句柄的文件表(如果不理解的话, 建议看看APUE), 所以close socket的时候, 只是使引用计数减1, 并不是真正地直接关闭socket(减为0才是真正的关闭), 这一点, 我们也已经说过了。
废话少说, 直接上菜。
服务端程序为:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
void sigChildFun(int sigNO)
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) // 循环收尸(僵尸进程), 此时waitpid不会阻塞
{
NULL;
}
return;
}
int main()
{
sockaddr_in servAddr;
memset(&servAddr,0,sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = INADDR_ANY;
servAddr.sin_port = htons(8765);
int iListenSocket = socket(AF_INET, SOCK_STREAM, 0);
bind(iListenSocket, (sockaddr *)&servAddr, sizeof(servAddr));
listen(iListenSocket,5);
signal(SIGCHLD, sigChildFun);
while(1)
{
sockaddr_in clientAddr;
socklen_t iSize = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));
int iConnSocket = accept(iListenSocket,(sockaddr*)&clientAddr, &iSize);
if(iConnSocket < 0)
{
if(errno == EINTR || errno == ECONNABORTED)
{
continue;
}
else
{
printf("accept error, server\n");
return -1;
}
}
int tmpPid = fork();
if(tmpPid == 0)
{
close(iListenSocket); // 子进程让监听socket的计数减1, 并非直接关闭监听socket
char szBuf[1024] = {0};
snprintf(szBuf, sizeof(szBuf), "server pid[%u], client ip[%s]", getpid(), inet_ntoa(clientAddr.sin_addr));
write(iConnSocket, szBuf, strlen(szBuf) + 1);
while(1)
{
if(read(iConnSocket, szBuf, 1) <= 0)
{
close(iConnSocket); // 子进程让通信的socket计数减1
return -2; // 子进程退出
}
}
close(iConnSocket); // 子进程让通信的socket计数减1
return 0; // 子进程退出
}
close(iConnSocket); // 父进程让通信的socket计数减1
}
getchar();
close(iListenSocket); // 父进程让监听socket计数减1, 此时会关掉监听socket(因为之前子进程已经有此操作)
return 0;
}
启动它。
客户端程序为:
#include <unistd.h>我们开启一个客户端, 此时如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
int main()
{
int sockClient = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrSrv;
addrSrv.sin_addr.s_addr = inet_addr("10.100.70.140");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8765);
connect(sockClient, ( const struct sockaddr *)&addrSrv, sizeof(struct sockaddr_in));
char szBuf[2048] = {0};
int iRet = recv(sockClient, szBuf, sizeof(szBuf) - 1 , 0);
printf("msg from server: %s\n", szBuf);
getchar();
close(sockClient);
return 0;
}
客户端信息为:
xxxxxx$ ./client
msg from server: server pid[16402], client ip[10.100.70.139]
服务端信息为:
xxxxxx$ ps -ef | grep -w server可以看到, 服务端16402子进程是与客户端通信的进程, 父进程16096是监听的父进程(主进程)。
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
user_00 16402 16096 0 19:42 pts/18 00:00:00 ./server
另外再开启一个客户端(不要关闭旧的客户端), 此时如下:
新客户端信息为:
xxxxxx$ ./client
msg from server: server pid[16497], client ip[10.100.70.139]
服务端信息为:
xxxxxx$ ps -ef | grep -w server可以看到, 父进程16096新开了一个子进程16497来与新的客户端进行通信。
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
user_00 16402 16096 0 19:42 pts/18 00:00:00 ./server
user_00 16497 16096 0 19:42 pts/18 00:00:00 ./server
我们关闭第一个客户端, 然后看到服务端为:
xxxxxx$ ps -ef | grep -w server
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
user_00 16497 16096 0 19:42 pts/18 00:00:00 ./server
我们再关闭第二个客户端, 然后看到服务端为:
xxxxxx$ ps -ef | grep -w server显然, 客户端退出后, 发FIN包, 服务端子进程的recv函数就为0, 退出子进程的while循环了, 因此, 对应的子进程就over了, 而且不会留下僵尸进程(有waitpid)。 而且, 我们可以看到, 负责连接管理(accept)的父进程(主进程)依然安然无恙, 优哉游哉地等待下一个客户端连接。
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
在这里, 我们可以看到, 这个服务器是并发的, 而不是迭代的。 什么意思呢? 你看, 即使子进程处理业务需要很久很久, 那么上述服务依然能并发地响应n个几乎同时到达的客户端, 此时,父进程开启n个子进程, 并发地工作, 并发地与客户端进行通信, 而且还互不干扰, 大大提升了服务满意度。
客户自然满意多了, 因为服务方专门派人一对一地提供服务啊, 你要是再不满意, 这个客户就没良心了。
先说到这里, 吃个晚饭, 然后干点正事。