使用 fork() 设计一个并发服务器来处理多个客户端
先决条件: C/C++ 中的套接字编程,fork() 系统调用
问题陈述:在本文中,我们将编写一个程序来说明使用fork()系统调用的客户端-服务器模型,它可以同时处理多个客户端。
Fork() call creates multiple child processes for concurrent clients and runs each call block in its own process control block (PCB).
需要设计一个并发服务器来处理使用 fork() 调用的客户端:
通过 TCP 基本的服务器-客户端模型,一台服务器在特定时间只服务一个客户端。
但是,我们现在正试图让我们的 TCP 服务器处理多个客户端。虽然,我们可以使用select() 系统调用来实现这一点,但我们可以简化整个过程。
fork() 系统调用将如何帮助解决这个问题?
Fork()创建一个与其父进程同步运行的新子进程,如果子进程创建成功则返回0 。
- 每当一个新客户端尝试连接到 TCP 服务器时,我们将创建一个新的子进程,它将与其他客户端的执行并行运行。这样,我们将设计一个不使用Select() 系统调用的并发服务器。
- pid_t(进程 ID)数据类型将用于保存子进程 ID。示例: pid_t = fork( ) 。
与其他方法的区别:
这是创建并发服务器的最简单技术。每当一个新客户端连接到服务器时,都会执行一个 fork() 调用,为每个新客户端创建一个新的子进程。
- 多线程使用单个处理程序实现并发服务器。与连接共享数据/文件通常使用fork()比使用线程慢。
- Select() 系统调用不会创建多个进程。相反,它有助于在单个程序上多路复用所有客户端,并且不需要非阻塞 IO。
使用 fork() 设计并发服务器以处理多个客户端的程序
- 接受客户端会创建一个新的子进程,该子进程与其他客户端和父进程同时运行
C
// Accept connection request from client in cliAddr
// socket structure
clientSocket = accept(
sockfd, (struct sockaddr*)&cliAddr, &addr_size);
// Make a child process by fork() and check if child
// process is created successfully
if ((childpid = fork()) == 0) {
// Send a confirmation message to the client for
// successful connection
send(clientSocket, "hi client", strlen("hi client"),
0);
}
C
// Server side program that sends
// a 'hi client' message
// to every client concurrently
#include
#include
#include
#include
#include
#include
#include
#include
// PORT number
#define PORT 4444
int main()
{
// Server socket id
int sockfd, ret;
// Server socket address structures
struct sockaddr_in serverAddr;
// Client socket id
int clientSocket;
// Client socket address structures
struct sockaddr_in cliAddr;
// Stores byte size of server socket address
socklen_t addr_size;
// Child process id
pid_t childpid;
// Creates a TCP socket id from IPV4 family
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// Error handling if socket id is not valid
if (sockfd < 0) {
printf("Error in connection.\n");
exit(1);
}
printf("Server Socket is created.\n");
// Initializing address structure with NULL
memset(&serverAddr, '\0',
sizeof(serverAddr));
// Assign port number and IP address
// to the socket created
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
// 127.0.0.1 is a loopback address
serverAddr.sin_addr.s_addr
= inet_addr("127.0.0.1");
// Binding the socket id with
// the socket structure
ret = bind(sockfd,
(struct sockaddr*)&serverAddr,
sizeof(serverAddr));
// Error handling
if (ret < 0) {
printf("Error in binding.\n");
exit(1);
}
// Listening for connections (upto 10)
if (listen(sockfd, 10) == 0) {
printf("Listening...\n\n");
}
int cnt = 0;
while (1) {
// Accept clients and
// store their information in cliAddr
clientSocket = accept(
sockfd, (struct sockaddr*)&cliAddr,
&addr_size);
// Error handling
if (clientSocket < 0) {
exit(1);
}
// Displaying information of
// connected client
printf("Connection accepted from %s:%d\n",
inet_ntoa(cliAddr.sin_addr),
ntohs(cliAddr.sin_port));
// Print number of clients
// connected till now
printf("Clients connected: %d\n\n",
++cnt);
// Creates a child process
if ((childpid = fork()) == 0) {
// Closing the server socket id
close(sockfd);
// Send a confirmation message
// to the client
send(clientSocket, "hi client",
strlen("hi client"), 0);
}
}
// Close the client socket id
close(clientSocket);
return 0;
}
C
// Client Side program to test
// the TCP server that returns
// a 'hi client' message
#include
#include
#include
#include
#include
#include
#include
#include
// PORT number
#define PORT 4444
int main()
{
// Socket id
int clientSocket, ret;
// Client socket structure
struct sockaddr_in cliAddr;
// char array to store incoming message
char buffer[1024];
// Creating socket id
clientSocket = socket(AF_INET,
SOCK_STREAM, 0);
if (clientSocket < 0) {
printf("Error in connection.\n");
exit(1);
}
printf("Client Socket is created.\n");
// Initializing socket structure with NULL
memset(&cliAddr, '\0', sizeof(cliAddr));
// Initializing buffer array with NULL
memset(buffer, '\0', sizeof(buffer));
// Assigning port number and IP address
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
// 127.0.0.1 is Loopback IP
serverAddr.sin_addr.s_addr
= inet_addr("127.0.0.1");
// connect() to connect to the server
ret = connect(clientSocket,
(struct sockaddr*)&serverAddr,
sizeof(serverAddr));
if (ret < 0) {
printf("Error in connection.\n");
exit(1);
}
printf("Connected to Server.\n");
while (1) {
// recv() receives the message
// from server and stores in buffer
if (recv(clientSocket, buffer, 1024, 0)
< 0) {
printf("Error in receiving data.\n");
}
// Printing the message on screen
else {
printf("Server: %s\n", buffer);
bzero(buffer, sizeof(buffer));
}
}
return 0;
}
- 服务器实现:
C
// Server side program that sends
// a 'hi client' message
// to every client concurrently
#include
#include
#include
#include
#include
#include
#include
#include
// PORT number
#define PORT 4444
int main()
{
// Server socket id
int sockfd, ret;
// Server socket address structures
struct sockaddr_in serverAddr;
// Client socket id
int clientSocket;
// Client socket address structures
struct sockaddr_in cliAddr;
// Stores byte size of server socket address
socklen_t addr_size;
// Child process id
pid_t childpid;
// Creates a TCP socket id from IPV4 family
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// Error handling if socket id is not valid
if (sockfd < 0) {
printf("Error in connection.\n");
exit(1);
}
printf("Server Socket is created.\n");
// Initializing address structure with NULL
memset(&serverAddr, '\0',
sizeof(serverAddr));
// Assign port number and IP address
// to the socket created
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
// 127.0.0.1 is a loopback address
serverAddr.sin_addr.s_addr
= inet_addr("127.0.0.1");
// Binding the socket id with
// the socket structure
ret = bind(sockfd,
(struct sockaddr*)&serverAddr,
sizeof(serverAddr));
// Error handling
if (ret < 0) {
printf("Error in binding.\n");
exit(1);
}
// Listening for connections (upto 10)
if (listen(sockfd, 10) == 0) {
printf("Listening...\n\n");
}
int cnt = 0;
while (1) {
// Accept clients and
// store their information in cliAddr
clientSocket = accept(
sockfd, (struct sockaddr*)&cliAddr,
&addr_size);
// Error handling
if (clientSocket < 0) {
exit(1);
}
// Displaying information of
// connected client
printf("Connection accepted from %s:%d\n",
inet_ntoa(cliAddr.sin_addr),
ntohs(cliAddr.sin_port));
// Print number of clients
// connected till now
printf("Clients connected: %d\n\n",
++cnt);
// Creates a child process
if ((childpid = fork()) == 0) {
// Closing the server socket id
close(sockfd);
// Send a confirmation message
// to the client
send(clientSocket, "hi client",
strlen("hi client"), 0);
}
}
// Close the client socket id
close(clientSocket);
return 0;
}
- 客户端实现:
C
// Client Side program to test
// the TCP server that returns
// a 'hi client' message
#include
#include
#include
#include
#include
#include
#include
#include
// PORT number
#define PORT 4444
int main()
{
// Socket id
int clientSocket, ret;
// Client socket structure
struct sockaddr_in cliAddr;
// char array to store incoming message
char buffer[1024];
// Creating socket id
clientSocket = socket(AF_INET,
SOCK_STREAM, 0);
if (clientSocket < 0) {
printf("Error in connection.\n");
exit(1);
}
printf("Client Socket is created.\n");
// Initializing socket structure with NULL
memset(&cliAddr, '\0', sizeof(cliAddr));
// Initializing buffer array with NULL
memset(buffer, '\0', sizeof(buffer));
// Assigning port number and IP address
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
// 127.0.0.1 is Loopback IP
serverAddr.sin_addr.s_addr
= inet_addr("127.0.0.1");
// connect() to connect to the server
ret = connect(clientSocket,
(struct sockaddr*)&serverAddr,
sizeof(serverAddr));
if (ret < 0) {
printf("Error in connection.\n");
exit(1);
}
printf("Connected to Server.\n");
while (1) {
// recv() receives the message
// from server and stores in buffer
if (recv(clientSocket, buffer, 1024, 0)
< 0) {
printf("Error in receiving data.\n");
}
// Printing the message on screen
else {
printf("Server: %s\n", buffer);
bzero(buffer, sizeof(buffer));
}
}
return 0;
}
编译脚本:
- 执行服务器端代码
⇒ gcc server.c -o ser
./ser
- 执行客户端代码
⇒ gcc client.c -o cli
./cli
输出:
优点:使用此工艺的优点是:
- 易于在执行更复杂任务的程序中实现。
- 每个子进程(客户端)独立运行,无法读取/写入其他客户端的数据。
- 服务器的行为就像它只有一个客户端连接到它一样。子进程不需要关心其他传入连接或并行子进程的运行。因此,使用 fork() 系统调用进行编程是透明的并且花费更少的精力。
缺点:这里提到的缺点
- Fork 的效率低于多线程,因为它通过创建新进程会产生很大的开销,但线程是一个轻量级进程,它共享来自父进程本身的资源。
- 操作系统将需要内存共享或同步成本来实现并发。