多进程网络服务端详细说明文档
一、概述
本项目实现了一个基于多进程的 TCP 网络服务端,主要用于处理多个客户端的连接请求。为了提高代码的可维护性和可复用性,分成了头文件(.h
)和多个源文件(.cpp
)。具体包含 ctcpserver.h
、ctcpserver.cpp
和 multi_process_network_server.cpp
三个文件。
二、文件结构及功能
1. ctcpserver.h
- 功能:该头文件主要用于声明
ctcpserver
类,定义了类的成员变量和成员函数接口,起到了对类的功能进行抽象和声明的作用,方便其他源文件引用该类。 - 关键内容
- 成员变量:
m_listenfd
:监听的 socket 描述符,-1
表示未初始化。m_clientfd
:客户端连接的 socket 描述符,-1
表示客户端未连接。m_clientip
:客户端的 IP 地址,以字符串形式存储。m_port
:服务端用于通信的端口号。
- 成员函数声明:包含了构造函数、析构函数以及一系列用于初始化服务端、接受客户端连接、发送和接收数据、关闭 socket 等操作的函数声明。
- 成员变量:
class ctcpserver {
private:int m_listenfd;int m_clientfd;std::string m_clientip;unsigned short m_port;
public:ctcpserver();~ctcpserver();bool initserver(const unsigned short in_port);bool accept();const std::string & clientip() const;bool send(const std::string &buffer);bool recv(std::string &buffer, const size_t maxlen);bool closelisten();bool closeclient();
};
2. ctcpserver.cpp
- 功能:该源文件实现了
ctcpserver.h
中声明的ctcpserver
类的所有成员函数,是类的具体实现部分。 - 关键函数及实现细节
- 构造函数
ctcpserver::ctcpserver()
:初始化m_listenfd
和m_clientfd
为-1
,表示初始状态下监听 socket 和客户端 socket 未初始化。 - 析构函数
ctcpserver::~ctcpserver()
:调用closelisten()
和closeclient()
函数,确保在对象销毁时关闭监听 socket 和客户端 socket,释放资源。 initserver
函数:- 使用
socket()
函数创建一个 TCP 套接字,使用 IPv4 协议(AF_INET
)和面向连接的 TCP 协议(SOCK_STREAM
)。 - 填充
sockaddr_in
结构体,设置协议族、端口号(使用htons()
进行字节序转换)和 IP 地址(INADDR_ANY
表示监听所有可用 IP)。 - 使用
bind()
函数将套接字绑定到指定的 IP 和端口,若绑定失败则关闭套接字并返回false
。 - 使用
listen()
函数将套接字设置为监听状态,允许最多 5 个客户端连接请求进入队列,若设置失败则关闭套接字并返回false
。
- 使用
accept
函数:- 使用
accept()
函数从监听队列中取出一个客户端连接请求,返回一个新的套接字描述符用于与该客户端通信。 - 使用
inet_ntoa()
函数将客户端的 IP 地址从大端序转换为字符串格式并存储在m_clientip
中。
- 使用
send
函数:检查客户端 socket 是否有效,若有效则使用send()
函数向客户端发送数据。recv
函数:- 清空接收缓冲区并调整其大小为指定的最大长度。
- 使用
recv()
函数接收客户端发送的数据,若接收失败则清空缓冲区并返回false
,否则根据实际接收的字节数调整缓冲区大小并返回true
。
closelisten
函数:关闭监听 socket 并将m_listenfd
设置为-1
。closeclient
函数:关闭客户端 socket 并将m_clientfd
设置为-1
。
- 构造函数
3. multi_process_network_server.cpp
- 功能:该文件是整个服务端程序的入口,包含
main
函数和信号处理函数,负责初始化服务端、处理客户端连接和信号处理。 - 关键流程及函数说明
main
函数:- 参数检查:检查命令行参数是否正确,若不正确则输出使用说明并退出程序。
- 信号处理设置:
- 使用
for
循环忽略所有信号,避免程序被不必要的信号干扰,同时解决僵尸进程问题。 - 设置
SIGTERM
和SIGINT
信号的处理函数为FathEXIT
,允许通过kill
命令或Ctrl + C
正常终止程序。
- 使用
- 服务端初始化:调用
tcpserver.initserver()
函数初始化服务端监听 socket,若初始化失败则输出错误信息并退出。 - 客户端连接处理:
- 使用
while
循环不断接受客户端连接请求,若接受失败则输出错误信息并退出。 - 调用
fork()
函数创建子进程,父进程关闭客户端 socket 并继续等待下一个客户端连接;子进程关闭监听 socket,重新设置信号处理函数,并与客户端进行通信。
- 使用
- 子进程通信处理:
- 循环接收客户端发送的数据,若接收失败则退出循环。
- 接收到数据后,向客户端发送 “ok” 作为响应,若发送失败则退出循环。
- 信号处理函数:
FathEXIT
函数:处理父进程接收到的SIGTERM
和SIGINT
信号。首先屏蔽这两个信号,防止信号处理函数被中断;输出父进程退出信息,使用kill(0, SIGTERM)
向所有子进程发送终止信号;关闭监听 socket 并退出父进程。ChldEXIT
函数:处理子进程接收到的SIGTERM
信号。屏蔽SIGINT
和SIGTERM
信号,输出子进程退出信息,关闭客户端 socket 并退出子进程。
四、注意事项
- 资源管理:每个子进程在处理完与客户端的通信后必须退出,否则会继续进入
accept
函数,导致错误。同时,要确保在程序结束时正确关闭所有 socket 资源,避免资源泄漏。 - 信号处理:信号处理函数中屏蔽了
SIGINT
和SIGTERM
信号,防止信号处理函数被中断,确保程序的稳定性。 - 高并发场景:该程序使用多进程处理客户端连接,每个客户端连接会创建一个新的子进程,可能会消耗较多的系统资源。在高并发场景下,可以考虑使用多线程或异步 I/O 技术进行优化。
1. ctcpserver.h
头文件
#ifndef CTCP_SERVER_H
#define CTCP_SERVER_H#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>class ctcpserver {
private:int m_listenfd; // 监听的socket,-1表示未初始化int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接std::string m_clientip; // 客户端字符串格式的IPunsigned short m_port; // 服务端用于通讯的端口
public:ctcpserver();~ctcpserver();// 初始化服务端用于监听的socketbool initserver(const unsigned short in_port);// 受理客户端的连接bool accept();// 获取客户端的IP(字符串格式)const std::string & clientip() const;// 向对端发送报文bool send(const std::string &buffer);// 接收对端的报文bool recv(std::string &buffer, const size_t maxlen);// 关闭监听的socketbool closelisten();// 关闭客户端连上来的socketbool closeclient();
};#endif
2. ctcpserver.cpp
源文件
#include "ctcpserver.h"// 构造函数,初始化监听socket和客户端socket为未初始化状态
ctcpserver::ctcpserver() : m_listenfd(-1), m_clientfd(-1) {}// 析构函数,关闭监听socket和客户端socket
ctcpserver::~ctcpserver() {closelisten();closeclient();
}// 初始化服务端用于监听的socket
bool ctcpserver::initserver(const unsigned short in_port) {// 第1步:创建服务端的socket// AF_INET表示使用IPv4协议,SOCK_STREAM表示使用面向连接的TCP协议if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;m_port = in_port;// 第2步:把服务端用于通信的IP和端口绑定到socket上struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET; // 协议族,固定填AF_INETservaddr.sin_port = htons(m_port); // 指定服务端的通信端口,htons用于将主机字节序转换为网络字节序servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 如果操作系统有多个IP,全部的IP都可以用于通讯// 绑定服务端的IP和端口(为socket分配IP和端口)if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {close(m_listenfd);m_listenfd = -1;return false;}// 第3步:把socket设置为可连接(监听)的状态// 5表示监听队列的最大长度if (listen(m_listenfd, 5) == -1) {close(m_listenfd);m_listenfd = -1;return false;}return true;
}// 受理客户端的连接(从已连接的客户端中取出一个客户端)
// 如果没有已连接的客户端,accept()函数将阻塞等待
bool ctcpserver::accept() {struct sockaddr_in caddr; // 客户端的地址信息socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小// 从监听队列中取出一个客户端连接if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1) return false;m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串return true;
}// 获取客户端的IP(字符串格式)
const std::string & ctcpserver::clientip() const {return m_clientip;
}// 向对端发送报文,成功返回true,失败返回false
bool ctcpserver::send(const std::string &buffer) {if (m_clientfd == -1) return false;// 发送报文if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0) return false;return true;
}// 接收对端的报文,成功返回true,失败返回false
// buffer - 存放接收到的报文的内容,maxlen - 本次接收报文的最大长度
bool ctcpserver::recv(std::string &buffer, const size_t maxlen) {buffer.clear(); // 清空容器buffer.resize(maxlen); // 设置容器的大小为maxlen// 直接操作buffer的内存接收数据int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0);if (readn <= 0) {buffer.clear();return false;}buffer.resize(readn); // 重置buffer的实际大小return true;
}// 关闭监听的socket
bool ctcpserver::closelisten() {if (m_listenfd == -1) return false;::close(m_listenfd);m_listenfd = -1;return true;
}// 关闭客户端连上来的socket
bool ctcpserver::closeclient() {if (m_clientfd == -1) return false;::close(m_clientfd);m_clientfd = -1;return true;
}
3. multi_process_network_server.cpp
主源文件
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "ctcpserver.h"ctcpserver tcpserver;// 父进程的信号处理函数
void FathEXIT(int sig);
// 子进程的信号处理函数
void ChldEXIT(int sig);int main(int argc, char *argv[]) {// 检查命令行参数if (argc != 2) {std::cout << "Using:./multi_process_network_server 通讯端口\nExample:./multi_process_network_server 5005\n\n";std::cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";std::cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";return -1;}// 忽略全部的信号,不希望被打扰。顺便解决了僵尸进程的问题for (int ii = 1; ii <= 64; ii++) signal(ii, SIG_IGN);// 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程// 但请不要用 "kill -9 +进程号" 强行终止signal(SIGTERM, FathEXIT);signal(SIGINT, FathEXIT); // SIGTERM 15 SIGINT 2// 初始化服务端用于监听的socketif (tcpserver.initserver(atoi(argv[1])) == false) {perror("initserver()");return -1;}while (true) {// 受理客户端的连接(从已连接的客户端中取出一个客户端)// 如果没有已连接的客户端,accept()函数将阻塞等待if (tcpserver.accept() == false) {perror("accept()");return -1;}// 创建子进程处理客户端连接int pid = fork();if (pid == -1) {perror("fork()");return -1;} // 系统资源不足if (pid > 0) {// 父进程tcpserver.closeclient(); // 父进程关闭客户端连接的socketcontinue; // 父进程返回到循环开始的位置,继续受理客户端的连接}// 子进程tcpserver.closelisten(); // 子进程关闭监听的socket// 子进程需要重新设置信号signal(SIGTERM, ChldEXIT); // 子进程的退出函数与父进程不一样signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号// 子进程负责与客户端进行通讯std::cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";std::string buffer;while (true) {// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待if (tcpserver.recv(buffer, 1024) == false) {perror("recv()");break;}std::cout << "接收:" << buffer << std::endl;buffer = "ok";if (tcpserver.send(buffer) == false) { // 向对端发送报文perror("send");break;}std::cout << "发送:" << buffer << std::endl;}return 0; // 子进程一定要退出,否则又会回到accept()函数的位置}
}// 父进程的信号处理函数
void FathEXIT(int sig) {// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);std::cout << "父进程退出,sig=" << sig << std::endl;kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出// 在这里增加释放资源的代码(全局的资源)tcpserver.closelisten(); // 父进程关闭监听的socketexit(0);
}// 子进程的信号处理函数
void ChldEXIT(int sig) {// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);std::cout << "子进程" << getpid() << "退出,sig=" << sig << std::endl;// 在这里增加释放资源的代码(只释放子进程的资源)tcpserver.closeclient(); // 子进程关闭客户端连上来的socketexit(0);
}
代码说明
ctcpserver.h
:定义了ctcpserver
类的接口,包括类的成员变量和成员函数的声明。使用了预处理器指令#ifndef
、#define
和#endif
来防止头文件被重复包含。ctcpserver.cpp
:实现了ctcpserver
类的成员函数,包括构造函数、析构函数以及各种操作函数。multi_process_network_server.cpp
:主源文件,包含了main
函数和信号处理函数,使用ctcpserver
类来实现多进程的网络服务端。
编译和运行
将上述三个文件放在同一目录下,使用以下命令进行编译:
g++ -o multi_process_network_server ctcpserver.cpp multi_process_network_server.cpp
编译成功后,使用以下命令运行服务端程序:
./multi_process_network_server 5005
其中 5005
是服务端监听的端口号,你可以根据需要修改。