前三章中,服务器端一次只能处理一个客户,当多个客户端连接几乎同时到达服务端时,系统内核将其排入队列,accept函数依次处理连接,那么如何使得服务器端能够同时服务于多个客户呢?多进程编程
并发服务器最简单的实现方法就是使用fork函数为每个客户端创建各自进程。
并发服务器的框架如下:
server_socket = socket()
bind(server_socket,... )
listen(server_socket, 5)
while (1)
{
confd = accept(server_socket, );
pid_t pid = fork();
if (pid == 0) { // 子进程
close(server_socket);
do_something(connfd); // read or write
close(connfd);
exit(0);
} else if (pid == -1) { // error
} else { // 父进程
close(connfd);
}
}
close(server_socket);
思路还是蛮清楚的,据此我们得到如下并发服务端:
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 2222;
int main()
{
int server_socket;
char buff[BUFFER_SIZE];
int n;
server_socket = socket(AF_INET, SOCK_STREAM, 0);
assert(server_socket != -1);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
assert(listen(server_socket, 5) != -1);
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
while(1)
{
printf("waiting...\n");
int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
if(connfd == -1)
continue;
printf("connect from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
pid_t pid = fork();
if(pid == 0)
{
close(server_socket);
n = recv(connfd, buff, BUFFER_SIZE, 0);
send(connfd, buff, n, 0);
close(connfd);
exit(0);
}
else if(pid < 0)
printf("ERROR\n");
else
close(connfd);
}
close(server_socket);
return 0;
}
上述实现的并发服务端,只是一个最简单的雏形,分析可以发现一些问题:
1.每次新连接一个客户,都创建一个进程,其实是很不合理的。原因如下:
频繁的创建和销毁进程开销会很大,因此为每一个客户都创建一个新的进程会很浪费很多服务器资源,如内存、CPU等;
2.每次调用系统函数 recv 或者 send 进行读写数据其实是有缺陷的。比如我们要接收(发送)长度为len的数据,recv(send)成功时返回实际读取(写入)的数据长度,可能小于长度len,因此我们需要多次调用 recv(send),循环读写以保证完整接收(发送)数据;
3.信号处理机制缺乏,上述服务端未有效处理僵尸进程,对于EINTR错误也未有效处理。
据此,得到以下几点建议以供后续学习的方向:
1、使用多线程编程,而不是多进程编程,因为线程是程序执行的最小单元,也称为轻量级进程。多线程编程可以减少处理器的空转时间,支持多处理器以及减少上下文切换开销;
2、使用线程池技术,即预先创建若干个线程,没必要不断动态创建和销毁线程,减少消耗;
3、使用I/O复用技术,应用select、poll或者epoll提高系统效率;
4、利用高效的事件处理方式:Reactor模式和Proactor模式。