大家好,我是涛哥。
今天还在假期中,最近被朋友拉进了一个乒乓球群,发现了一个打球的地方,又近又好,球友也多,打得挺好,今年减肥有望啦。
有很多读者对网络编程很感兴趣,觉得挺有意思,希望我多写一些网络编程方面的实战例子。没问题,这就来了。我写的这些网络程序例子,都是自己亲自调试过的,大家可以直接拿去运行。
在后端开发中,并发服务是一个基本的要求,大家手机上的APP连接到后端服务器,这些服务器是怎样处理成千上万的请求的呢?方法有很多,今天,我们来实战fork一下多进程并发服务器。
服务端实现
我们思考一下:服务端程序该怎么写?如果是在while中循环accept, 然后循环处理事情,此时只能逐一处理客户端的请求,后一个请求必须等前一个请求处理完毕,真是急死人。
可以考虑多线程,也可以考虑多进程,本文来说说后者。在多进程服务器模型中,用父进程来处理连接(监听socket),用fork子进程来处理通信(通信socket),各司其职,美哉。
关于fork这个单词(不是fuck),我们应该很熟悉:叉子。在linux系统中,这个fork函数也是很常见的,其作用是分叉一个子进程出来。在很多笔试面试中,fork几乎是必考的内容。
提到fork,就必须注意僵尸进程的处理。另外,父子进程共享socket句柄的文件表,所以,在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
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
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
可以看到,父进程16096新开了一个子进程16497来与新的客户端进行通信。
接下来,我们关闭第一个客户端进程,然后可以看到服务端显示的信息如下:
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
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
显然,客户端退出后,发了FIN包,服务端子进程的recv函数就为0,退出子进程的while循环了,因此,对应的子进程就over了,而且不会留下僵尸进程(有waitpid)。
而且,可以看到,负责连接管理(accept)的父进程(主进程)依然安然无恙,优哉游哉地等待下一个客户端连接。我们可以看到,这个服务器是并发的,而不是迭代的。
什么意思呢?你看,即使子进程处理业务需要很久,上述服务依然能并发地响应n个同时到达的客户端,父进程开启n个子进程,并发地工作,并发地与客户端来通信。
而且,这种并发的行为互不干扰,大大提升了服务满意度。客户自然满意多了,因为服务方专门派人一对一地提供服务啊,你要是再不满意,那这个客户就没良心了。
建议有兴趣的同学实际玩一下这个例子,加深对多进程并发服务器的理解,提高实战能力。同时,对fork的理解也会更加深刻。
网络编程就是这样,定好思路,多编程,多抓包,多调试,爽爽哒!大家如果有疑问请留言。晚上又要去打球了,减肥走起来。