阻塞IO模型的多进程形式
前言
在《IO模型:传统IO模型(阻塞IO模型)》中,使用的是阻塞IO,阻塞IO会导致进程阻塞,在处理完当前的连接之前是无法获取新的连接的,这也就导致了服务器的吞吐量低。那么如何在使用阻塞IO的前提下能够获取更多的连接请求呢?这个时候就需要用到多进程,服务器在接收到一个请求时不会直接在本进程处理,而是另外创建一个子进程进行处理,之后继续阻塞等待客户端连接,这样就可以做到接收连接请求和处理客户端请求同时进行了,实现多客户端并发。相比之前的传统IO模型中的循环服务器快上不少。
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#define PORT 8080
#define BACKLOG SOMAXCONN
#define BUFFERSIZE 4089
void process_connection(int connfd) {
char buffer[BUFFERSIZE];
ssize_t n = read(connfd, buffer, BUFFERSIZE);
if (n > 0) {
buffer[n] = '\0';
printf("收到的请求信息:\n%s\n", buffer);
char *response = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html;charset=utf-8\r\n"
"\r\n"
"<html><body><h1>Hello, World!</h1></body></html>\r\n";
write(connfd, response, strlen(response));
}
close(connfd);
exit(0);
}
int main() {
int listenfd, connfd;
struct sockaddr_in serv_addr, client_addr;
socklen_t addr_size;
pid_t pid;
int status = 0;//初始化状态
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1) {
perror("绑定失败");
exit(EXIT_FAILURE);
}
if (listen(listenfd, BACKLOG) == -1) {
perror("监听失败");
exit(EXIT_FAILURE);
}
addr_size = sizeof(client_addr);
printf("服务器启动, 监听端口 %d\n", PORT);
while (1) {
connfd = accept(listenfd, (struct sockaddr *) &client_addr, &addr_size);
int port = ntohs(client_addr.sin_port);
struct in_addr in = client_addr.sin_addr;
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &in, str, sizeof(str));
printf("客户端信息: ip:port = %s:%d\n", str, port);
if (connfd == -1) {
perror("accept失败");
break;
}
pid = fork();
if (pid > 0) {
//父进程
//关闭连接套接字,原因和下面子进程关闭监听套接字类似
close(connfd);
//options=WNOHANG,如果pid指定的子进程之一不能立即获得状态,则waitpid () 函数不应阻塞调用线程的执行。
//options=WUNTRACED,由 pid 指定的任何子进程的状态,如果子进程已停止,并且其状态自停止以来尚未报告,也应报告给请求进程。
waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);
if (WIFEXITED(status)) {//如果子进程正常结束,它就返回真;否则返回假。
//WEXITSTATUS(status) 如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
printf("status = %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) { //如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
//如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
printf("signal status = %d\n", WTERMSIG(status));
}
if (WIFSTOPPED(status)) {//如果当前子进程被暂停了,则返回真;否则返回假。
//如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。
printf("stop sig num = %d\n", WSTOPSIG(status));
}
} else if (pid == 0) {
//子进程
//子进程会导致父进程中socket引用计数+1
//close的时候只有当引用计数为0时才会真正关闭socket
//关闭监听套接字,这个套接字是从父进程继承过来,防止内存泄露
close(listenfd);
process_connection(connfd);
} else {
printf("创建子进程失败\n");
exit(EXIT_FAILURE);
}
}
close(listenfd);
return 0;
}
代码解释
上面的代码实际逻辑非常简单,主要说一下几个比较重要的点:
fork 函数:创建子进程处理客户端请求,以实现多客户端并发处理。
close 函数:描述符的计数就会减1,直到计数为0。当计数为0时,也就是所有进程都调用了close,这时程序会调用shutdown函数释放套接字。
waitpid 函数:父进程等待子进程结束,回收子进程的资源。如果不加waitpid或wait,会导致出现僵尸进程,因为系统所能使用的进程号是有限的,如果僵尸进程积累多了会导致系统无法再创建新的进程。