「Linux 网络编程」基于多进程的并发服务器

0 前言

在通过使用 Linux C API 实现一个多进程并发服务器之前,需要掌握以下知识:

  1. 如何通过系统调用 fork() 创建子进程:「Linux 系统编程」fork() 创建子进程

  2. 什么是僵尸进程?如何使用 wait()waitpid() 回收子进程:「Linux 系统编程」孤儿进程、僵尸进程、wait 和 waitpid 子进程回收

  3. 进程间常用的通信方式:「Linux 系统编程」进程间通信方式、管道基本使用方法

  4. 信号进行进程间通信,信号处理函数 signalsigaction 的使用:「Linux 系统编程」信号处理函数 signal、sigaction



1 为什么需要并发服务器

「Linux 网络编程」Socket 构建 TCP 本地通信 中,实现了一个基本的单进程顺序处理的服务器端。但是,传统的单进程顺序处理服务器端在面对多个客户端连接时,似乎就不是那么高效了😣。

试想一下,现在有 100 个用户通过客户端请求连接服务器,处理每个用户的服务需要 1 秒的时间。第 1 个连接请求的受理时间很快,受理时间为 0 秒。然而,后续每个用户的请求都需要等到前面的用户结束服务才能受理,也就是说,第 50 个请求的受理时间为 50 秒,第 88 个请求的受理时间为 88 秒。这对于后来的用户来说,实在是漫长的等待。

因此,不如考虑以下提供服务的方式:

所有连接请求的受理时间不超过 1 秒,平均的服务时间为 2 ~ 3 秒。

即便服务的时间可能会延长,但是这显然能够向大量同时发起连接请求的客户端提供服务。在网络编程中,数据传输的 I/O 操作过程中,CPU 通常处于空闲状态,在此期间有效地处理多个客户端的连接请求并提供服务,无疑是一种高效利用 CPU 资源的方式。

比较具有代表性的并发服务器实现方式有:

  • 多进程并发服务器:通过创建多个子进程,多进程并发地处理请求和提供服务。
  • 多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务。
  • 多线程服务器:通过生成与客户端等量地线程提供服务。


2 多进程并发服务器的优势

2.1 单进程的局限性

在单进程服务器模型中:

1
2
3
4
5
6
// 伪码:单进程顺序处理
while (1) {
client_socket = accept(server_socket);
handle_request(client_socket); // 阻塞处理
close(client_socket);
}

关键问题在于,处理一个请求时,其他客户端必须等待。如果后续连接数显著增加,响应的时间可想而知,会变得非常大。

除此以外,一个请求处理失败可能影响整个服务,容错性有待提高


2.2 多进程的优势

在多进程服务器模型中:

1
2
3
4
5
6
7
8
9
10
11
// 伪码:多进程并发处理
while (1) {
client_socket = accept(server_socket);
pid = fork();
if (pid == 0) {
// 子进程专门处理这个客户端
handle_request(client_socket);
exit(0);
}
// 父进程继续接受新连接
}

核心优势

  • 并发:每个客户端由独立进程服务
  • 故障隔离:单个进程崩溃不影响整体服务
  • 资源隔离:每个进程有独立的内存空间


3 多进程并发服务器的实现

3.1 基本架构

1
2
3
4
5
6
主进程 (监听者)

├── 子进程1 (处理客户端A)
├── 子进程2 (处理客户端B)
├── 子进程3 (处理客户端C)
└── ...

3.2 服务器的初始化

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
int server_init(int port) {
int server_fd; // 服务器端套接字文件描述符
struct sockaddr_in server_addr; // 配置的服务器地址

// 创建socket, IPv4, TCP
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 设置 socket 选项
// TCP 服务器被杀死后尝试重启,一段时间处于 TIME_WAIT 状态
// SO_REUSEADDR 可以使得服务器快速重启,避免服务器重启绑定同一端口出现失败
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

// 初始化地址
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听任何端口
server_addr.sin_port = htons(port);

// 绑定地址
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 开始监听
listen(server_fd, BACKLOG); // 设置监听数量上限

return server_fd;
}

上面的代码实现了 服务器的初始化。不过上面的代码省去了一些错误处理,后续完整实现还是要加上这些错误处理。

其中,setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) 是网络编程中很重要的一个调用,可以实现服务器的快速重启。

「Linux 网络编程」TCP 内部工作原理_ 以客户端到服务器端发送请求为例介绍了 TCP 的三次握手和四次挥手。上述 SO_REUSEADDR 调用主要是为了应对服务器端到客户端(服务器端主动断开连接)时出现的 TIME_WAIT 问题。

当主动发起断开的服务器端处于 TIME_WAIT 状态下时,相应的端口处于正在使用的状态。通过 netstat 查看 TIME_WAIT 可能出现类似如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
# 启动测试服务器
$ ./test_server &

# 查看端口状态
$ netstat -tulnp | grep 8080
tcp6 0 0 :::8080 :::* LISTEN 12345/./test_server

# 连接并断开后查看
$ telnet localhost 8080
$ netstat -tulnp | grep 8080
tcp6 0 0 :::8080 :::* LISTEN 12345/./test_server
tcp6 0 0 localhost:8080 localhost:54321 TIME_WAIT -

那么为什么需要 TIME_WAIT

  1. 可靠的连接终止

    确保最后的ACK丢失时,可以重传 FIN。等待时间:2 * MSL (Maximum Segment Lifetime),通常为60秒

  2. 防止旧连接的重复数据包干扰新连接

    确保所有旧连接的数据包都在网络中消失。

基于上述原因,TIME_WAIT 还是很重要的,但是我们想要实现尽快重启服务器,以快速提供服务。

解决方案就是使用 SO_REUSEADDR,在套接字的设置选项中更改为 SO_REUSEADDR 的状态,这样就可以使得服务器快速重启,避免服务器重启绑定同一端口出现失败。

1
2
3
4
5
// 设置 socket 选项
// TCP 服务器被杀死后尝试重启,一段时间处于 TIME_WAIT 状态
// SO_REUSEADDR 可以使得服务器快速重启,避免服务器重启绑定同一端口出现失败
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));


3.3 进程管理与信号处理

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
void setup_signal_handlers() {
struct sigaction sa;

// 优雅退出处理,注册服务器退出函数
sa.sa_handler = graceful_shutdown;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);

// 僵尸进程回收
// SIG_IGN 忽略 SIGCHLD(不会发送SIGCHLD信号给父进程)
sa.sa_handler = SIG_IGN; // 忽略 SIGCHLD,避免僵尸进程
sa.sa_flags = SA_NOCLDWAIT | SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
}

// 全局标志,用于控制程序退出
// static 限制变量只在文件内可见, volatile 防止编译器优化,强制每次都从内存读取
static volatile int running = 1;

void graceful_shutdown(int sig) {
if (sig == SIGINT) {
printf("\n接收到中断信号,正在关闭服务器...\n");
running = 0; // 清理资源,关闭服务器
}
}

上述代码主要的作用是实现服务器的优雅退出 以及 子进程回收(避免僵尸进程)。主要采用 sigaction 信号处理函数实现这一部分功能。

在子进程回收的 sa_handler 中特别地使用了 SIG_IGN,然后注册函数 sigaction(SIGCHLD, &sa, NULL);,这样可以直接忽略 SIGCHLD 信号,不会发生这个信号给父进程。结合 SA_NOCLDWAIT 使用:

  • 子进程退出时 不会变成僵尸进程
  • 内核立即自动回收子进程资源
  • 不会发送SIGCHLD信号 给父进程
  • 但是,父进程无法获取子进程的退出状态

另外,SA_RESTART 是为了:系统调用被该信号中断,让内核 自动重启被中断的系统调用(如 read, write)。

当然,也可以实现能够获取子进程退出状态的回收子进程方式,主要方法就是循环调用 waitpid() 回收子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
void handle_sigchld(int sig) {
int status;
pid_t pid;

while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
// 子进程状态处理...
}
}

// 在其他地方进行信号处理设置
sa.sa_handler = handle_sigchld;
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);

3.4 客户端请求处理

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
void handle_echo_client(int client_fd, struct sockaddr_in *client_addr) {
char buffer[BUFFER_SIZE]; // 用于服务端收发数据的临时缓冲区
char client_ip[INET_ADDRSTRLEN]; // 存储获取的客户端 ip 的临时缓冲区
ssize_t bytes_read; // 统计接收数据字节数

// 显示当前处理连接的子进程 id 以及 连接的客户端信息
// 客户端 ip 地址二进制格式转换为字符串格式
inet_ntop(AF_INET, &client_addr->sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr->sin_port);
printf("子进程 %d: 服务客户端 %s:%d\n", getpid(), client_ip, client_port);

// 发送欢迎消息
const char *welcome_msg = "欢迎连接到TCP服务器!输入 'quit' 退出连接。\n";
send(client_fd, welcome_msg, strlen(welcome_msg), 0);

// 与客户端通信
while (1) {
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);

// 接收客户端数据
bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("子进程 %d: 接收到来自 %s:%d 的数据: %s",
getpid(), client_ip, client_port, buffer);

// 去除换行符
buffer[strcspn(buffer, "\r\n")] = '\0';

// 检查是否退出
if (strcmp(buffer, "quit") == 0) {
const char *bye_msg = "再见!\n";
send(client_fd, bye_msg, strlen(bye_msg), 0);
break;
}

// 回显数据给客户端
char echo_msg[BUFFER_SIZE + 50];
snprintf(echo_msg, sizeof(echo_msg),
"服务器回复: 已收到 %ld 字节数据: %s\n",
bytes_received, buffer);
send(client_fd, echo_msg, strlen(echo_msg), 0); // 发送给客户端
} else if (bytes_received == 0) {
printf("子进程 %d: 客户端 %s:%d 断开连接\n",
getpid(), client_ip, client_port);
break;
} else {
perror("recv失败");
break;
}
}

// 关闭客户端套接字
close(client_fd);
printf("子进程 %d: 完成服务 %s:%d\n", getpid(), client_ip, client_port);
}

上述代码实现的主要是 服务端处理客户端请求的功能。这和以前实现的服务器端没有什么太大差别 「Linux 网络编程」Socket 构建 TCP 本地通信


3.5 主服务循环

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
int main() {
int server_fd, client_fd;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
pid_t pid;

printf("启动多进程并发服务器,端口: %d\n", PORT);

setup_signal_handlers(); // 设置信号处理函数,用于服务器退出 和 子进程回收
server_fd = server_init(PORT); // 初始化服务器端套接字

printf("服务器准备就绪,等待连接...\n");

while (running) {
// 开始接收客户端请求,同时获取客户端的地址信息
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);

if (client_fd == -1) {
if (errno == EINTR && !running) {
// 被信号中断且正在退出
break;
}
perror("accept 失败");
continue;
}

// 再次检查,确保在 accept 成功后 running 仍然为 true
if (!running) {
close(client_fd);
printf("服务器退出中,拒绝新连接\n");
break;
}

pid = fork(); // 创建子进程来处理客户端的请求并提供服务
if (pid == 0) {
// 子进程,读时共享、写时复制
close(server_fd); // 在子进程中需要关闭设置服务器端的套接字文件描述符
handle_echo_client(client_fd, &client_addr); // 子进程处理客户端请求
exit(0);
} else if (pid > 0) {
// 父进程
close(client_fd); // 在父进程中需要关闭给提供客户端服务的套接字文件描述符

// 在父进程中显示提供服务的子进程
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("创建子进程 %d 处理 %s:%d\n",
pid, client_ip, ntohs(client_addr.sin_port));
} else {
perror("fork");
close(client_fd);
}
}

close(server_fd);
printf("服务器已关闭\n");

return 0;
}

上述代码主要是在主函数中调用之前的设置信号处理函数、初始化服务端套接字等操作,然后父进程创建子进程处理客户端请求。

在父进程创建一个子进程时,调用 fork() 函数时复制了父进程的所有资源,但是并不会复制套接字

套接字并非进程所有,而是属于操作系统的。只是 进程拥有指向相应套接字的文件描述符。进程 close() 文件描述符减少引用计数,当引用为 0 时才会真正关闭。

想象一下,文件描述符就像是遥控器,套接字对象就像是电视机。fork()复制了遥控器,但电视机只有一台。多个遥控器可以控制同一台电视机,只有当所有遥控器都扔掉后,电视机才会被关闭。

在上述代码中,子进程中需要关闭设置服务器端的套接字文件描述符,而在父进程中需要关闭给提供客户端服务的套接字文件描述符。为什么要这么做呢?

场景 不关闭的后果 正确做法
父进程不关闭客户端socket 文件描述符泄漏,连接无法释放 close(client_fd)
子进程不关闭服务器socket 监听端口无法释放,阻止服务器重启 close(server_fd)
fork失败时不关闭socket 资源泄漏 错误处理中也要close()


4 多进程并发服务器测试

4.1 完整多进程并发服务器代码

结合 3 多进程并发服务器的实现 中的各个模块,可以得到如下 多进程并发服务器 的完整代码:

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// mp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <errno.h>

#define PORT 8080
#define BACKLOG 10
#define BUFFER_SIZE 1024

// 全局标志,用于控制程序退出
// static 限制变量只在文件内可见, volatile 防止编译器优化,强制每次都从内存读取
static volatile int running = 1;

void graceful_shutdown(int sig) {
if (sig == SIGINT) {
printf("\n接收到中断信号,正在关闭服务器...\n");
running = 0; // 清理资源,关闭服务器
}
}

// 信号处理函数设置
void setup_signal_handlers() {
struct sigaction sa;

// 优雅退出处理,注册服务器退出函数
sa.sa_handler = graceful_shutdown;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);

// 僵尸进程回收
// SIG_IGN 忽略 SIGCHLD(不会发送SIGCHLD信号给父进程)
sa.sa_handler = SIG_IGN; // 忽略 SIGCHLD,避免僵尸进程
sa.sa_flags = SA_NOCLDWAIT | SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
}

// 初始化服务器
int server_init(int port) {
int server_fd; // 服务器端套接字文件描述符
struct sockaddr_in server_addr; // 配置的服务器地址

// 创建socket, IPv4, TCP
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}

// 设置 socket 选项
// TCP 服务器被杀死后尝试重启,一段时间处于 TIME_WAIT 状态
// SO_REUSEADDR 可以使得服务器快速重启,避免服务器重启绑定同一端口出现失败
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 配置服务器地址
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听任何端口
server_addr.sin_port = htons(port);

// 绑定地址
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, BACKLOG) == -1) { // 设置监听数量上限
perror("listen 失败");
close(server_fd);
exit(EXIT_FAILURE);
}

return server_fd;
}

// 处理客户端请求
void handle_echo_client(int client_fd, struct sockaddr_in *client_addr) {
char buffer[BUFFER_SIZE]; // 用于服务端收发数据的临时缓冲区
char client_ip[INET_ADDRSTRLEN]; // 存储获取的客户端 ip 的临时缓冲区
ssize_t bytes_received; // 统计接收数据字节数

// 显示当前处理连接的子进程 id 以及 连接的客户端信息
// 客户端 ip 地址二进制格式转换为字符串格式
inet_ntop(AF_INET, &client_addr->sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr->sin_port);
printf("子进程 %d: 服务客户端 %s:%d\n", getpid(), client_ip, client_port);

// 发送欢迎消息
const char *welcome_msg = "欢迎连接到TCP服务器!输入 'quit' 退出连接。\n";
send(client_fd, welcome_msg, strlen(welcome_msg), 0);

// 与客户端通信
while (1) {
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);

// 接收客户端数据
bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("子进程 %d: 接收到来自 %s:%d 的数据: %s",
getpid(), client_ip, client_port, buffer);

// 去除换行符
buffer[strcspn(buffer, "\r\n")] = '\0';

// 检查是否退出
if (strcmp(buffer, "quit") == 0) {
const char *bye_msg = "再见!\n";
send(client_fd, bye_msg, strlen(bye_msg), 0);
break;
}

// 回显数据给客户端
char echo_msg[BUFFER_SIZE + 50];
snprintf(echo_msg, sizeof(echo_msg),
"服务器回复: 已收到 %ld 字节数据: %s\n",
bytes_received, buffer);
send(client_fd, echo_msg, strlen(echo_msg), 0); // 发送给客户端
} else if (bytes_received == 0) {
printf("子进程 %d: 客户端 %s:%d 断开连接\n",
getpid(), client_ip, client_port);
break;
} else {
perror("recv失败");
break;
}
}

// 关闭客户端套接字
close(client_fd);
printf("子进程 %d: 完成服务 %s:%d\n", getpid(), client_ip, client_port);
}


int main() {
int server_fd, client_fd;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
pid_t pid;

printf("启动多进程并发服务器,端口: %d\n", PORT);

setup_signal_handlers(); // 设置信号处理函数,用于服务器退出 和 子进程回收
server_fd = server_init(PORT); // 初始化服务器端套接字

printf("服务器准备就绪,等待连接...\n");

while (running) {
// 开始接收客户端请求,同时获取客户端的地址信息
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);

if (client_fd == -1) {
if (errno == EINTR && !running) {
// 被信号中断且正在退出
break;
}
perror("accept失败");
continue;
}

// 再次检查,确保在 accept 成功后 running 仍然为 true
if (!running) {
close(client_fd);
printf("服务器退出中,拒绝新连接\n");
break;
}

pid = fork(); // 创建子进程来处理客户端的请求并提供服务
if (pid == 0) {
// 子进程,读时共享、写时复制
close(server_fd); // 在子进程中需要关闭设置服务器端的套接字文件描述符
handle_echo_client(client_fd, &client_addr); // 子进程处理客户端请求
exit(0);
} else if (pid > 0) {
// 父进程
close(client_fd); // 在父进程中需要关闭给提供客户端服务的套接字文件描述符

// 在父进程中显示提供服务的子进程
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("创建子进程 %d 处理 %s:%d\n",
pid, client_ip, ntohs(client_addr.sin_port));
} else {
perror("fork");
close(client_fd);
}
}

close(server_fd);
printf("服务器已关闭\n");

return 0;
}

4.2 测试客户端代码

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
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];

// 创建socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket 创建失败");
exit(1);
}

// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

// 连接服务器
if (connect(sock, (struct sockaddr*)&server_addr,
sizeof(server_addr)) == -1) {
perror("连接服务器失败");
close(sock);
exit(1);
}

printf("已连接到服务器 %s:%d\n", SERVER_IP, PORT);

// 接收欢迎消息
memset(buffer, 0, BUFFER_SIZE);
recv(sock, buffer, BUFFER_SIZE - 1, 0);
printf("%s", buffer);

// 与服务器交互
while (1) {
printf("请输入消息: ");
fgets(buffer, BUFFER_SIZE, stdin);

// 发送消息
send(sock, buffer, strlen(buffer), 0);

// 检查是否退出
if (strncmp(buffer, "quit", 4) == 0) {
break;
}

// 接收回复
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("服务器回复: %s", buffer);
}
}

close(sock);
printf("连接已关闭\n");

return 0;
}

4.3 运行与测试

编译和运行

1
2
3
4
5
6
7
8
gcc mp_server.c -o mp_server
gcc client.c -o client

# 在终端运行服务器
./mp_server

# 在多个其他终端中运行客户端
./client

服务端输出

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
marisa@marisa-virtual-machine:~/mp_server_test$ ./mp_server
启动多进程并发服务器,端口: 8080
服务器准备就绪,等待连接...
创建子进程 34799 处理 127.0.0.1:40332
子进程 34799: 服务客户端 127.0.0.1:40332
创建子进程 34801 处理 127.0.0.1:40346
子进程 34801: 服务客户端 127.0.0.1:40346
创建子进程 34803 处理 127.0.0.1:60102
子进程 34803: 服务客户端 127.0.0.1:60102
子进程 34799: 接收到来自 127.0.0.1:40332 的数据: Hello, this is marisa.
子进程 34803: 接收到来自 127.0.0.1:60102 的数据: Hi, this is Alice.
子进程 34801: 接收到来自 127.0.0.1:40346 的数据: Good afternoon, Server!
子进程 34801: 接收到来自 127.0.0.1:40346 的数据: quit
子进程 34801: 完成服务 127.0.0.1:40346
子进程 34803: 接收到来自 127.0.0.1:60102 的数据: One
子进程 34799: 接收到来自 127.0.0.1:40332 的数据: Two
子进程 34803: 接收到来自 127.0.0.1:60102 的数据: Three
子进程 34799: 接收到来自 127.0.0.1:40332 的数据: Four
子进程 34803: 接收到来自 127.0.0.1:60102 的数据: quit
子进程 34803: 完成服务 127.0.0.1:60102
子进程 34799: 接收到来自 127.0.0.1:40332 的数据: It's time to say goodbye.
子进程 34799: 接收到来自 127.0.0.1:40332 的数据: quit
子进程 34799: 完成服务 127.0.0.1:40332
^C
接收到中断信号,正在关闭服务器...
服务器已关闭

客户端 1 输出

1
2
3
4
5
6
7
8
9
10
11
12
13
marisa@marisa-virtual-machine:~/mp_server_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到TCP服务器!输入 'quit' 退出连接。
请输入消息: Hello, this is marisa.
服务器回复: 服务器回复: 已收到 23 字节数据: Hello, this is marisa.
请输入消息: Two
服务器回复: 服务器回复: 已收到 4 字节数据: Two
请输入消息: Four
服务器回复: 服务器回复: 已收到 5 字节数据: Four
请输入消息: It's time to say goodbye.
服务器回复: 服务器回复: 已收到 26 字节数据: It's time to say goodbye.
请输入消息: quit
连接已关闭

客户端 2 输出

1
2
3
4
5
6
7
8
9
10
11
marisa@marisa-virtual-machine:~/mp_server_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到TCP服务器!输入 'quit' 退出连接。
请输入消息: Hi, this is Alice.
服务器回复: 服务器回复: 已收到 19 字节数据: Hi, this is Alice.
请输入消息: One
服务器回复: 服务器回复: 已收到 4 字节数据: One
请输入消息: Three
服务器回复: 服务器回复: 已收到 6 字节数据: Three
请输入消息: quit
连接已关闭

客户端 3 输出

1
2
3
4
5
6
7
marisa@marisa-virtual-machine:~/mp_server_test$ ./client
已连接到服务器 127.0.0.1:8080
欢迎连接到TCP服务器!输入 'quit' 退出连接。
请输入消息: Good afternoon, Server!
服务器回复: 服务器回复: 已收到 24 字节数据: Good afternoon, Server!
请输入消息: quit
连接已关闭

「Linux 网络编程」基于多进程的并发服务器
https://marisamagic.github.io/2025/10/12/20251012/
作者
MarisaMagic
发布于
2025年10月12日
许可协议