多进程Socket服务端编程
多进程Socket服务端编程详解
一、代码概述
本程序实现了一个多进程的TCP服务端,主进程负责监听客户端连接,子进程处理具体通信。核心类ctcpserver
封装了Socket操作,通过信号处理确保资源释放。
代码示例
#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>
using namespace std;
// 核心类定义
class ctcpserver // TCP通讯的服务端类。
{
// 类的成员变量和方法
};
ctcpserver tcpserver;
void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。
int main(int argc,char *argv[])
{
// 主函数流程
return 0;
}
// 父进程的信号处理函数实现
void FathEXIT(int sig)
{
// 函数实现
}
// 子进程的信号处理函数实现
void ChldEXIT(int sig)
{
// 函数实现
}
二、关键类与函数解析
1. ctcpserver
类
成员变量
m_listenfd
:监听Socket(类似总机电话),初始值为 -1 表示未初始化。m_clientfd
:客户端Socket(类似分机电话),初始值为 -1 表示客户端未连接。m_clientip
:客户端IP(字符串格式)。m_port
:服务端端口。
核心方法
initserver()
:初始化服务端Socket(创建→绑定→监听)。
bool initserver(const unsigned short in_port)
{
// 第1步:创建服务端的socket。
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_INET。
servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。
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设置为可连接(监听)的状态。
if (listen(m_listenfd,5) == -1 )
{
close(m_listenfd); m_listenfd=-1; return false;
}
return true;
}
accept()
:接受客户端连接,保存其IP。
bool 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;
}
send()
/recv()
:与客户端通信。
// 向对端发送报文,成功返回true,失败返回false。
bool send(const 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 recv(string &buffer,const size_t maxlen)
{
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作buffer的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置buffer的实际大小。
return true;
}
closelisten()
/closeclient()
:关闭监听/客户端Socket。
// 关闭监听的socket。
bool closelisten()
{
if (m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
// 关闭客户端连上来的socket。
bool closeclient()
{
if (m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
2. 主函数流程
int main(int argc,char *argv[])
{
if (argc!=2)
{
cout << "Using:./demo10 通讯端口\nExample:./demo10 5005\n\n";
cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
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
if (tcpserver.initserver(atoi(argv[1]))==false) // 初始化服务端用于监听的socket。
{
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(); // 父进程关闭客户端连接的socket。
continue; // 父进程返回到循环开始的位置,继续受理客户端的连接。
}
tcpserver.closelisten(); // 子进程关闭监听的socket。
// 子进程需要重新设置信号。
signal(SIGTERM,ChldEXIT); // 子进程的退出函数与父进程不一样。
signal(SIGINT ,SIG_IGN); // 子进程不需要捕获SIGINT信号。
// 子进程负责与客户端进行通讯。
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
string buffer;
while (true)
{
// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。
if (tcpserver.recv(buffer,1024)==false)
{
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
buffer="ok";
if (tcpserver.send(buffer)==false) // 向对端发送报文。
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
return 0; // 子进程一定要退出,否则又会回到accept()函数的位置。
}
}
三、多进程模型
1. 父进程职责
- 监听新连接(通过
m_listenfd
)。 - 创建子进程处理客户端请求。
- 关闭客户端Socket:避免资源泄漏。在代码中,父进程在
fork()
之后调用tcpserver.closeclient()
关闭客户端连接的socket。
if (pid> 0)
{ // 父进程。
tcpserver.closeclient(); // 父进程关闭客户端连接的socket。
continue; // 父进程返回到循环开始的位置,继续受理客户端的连接。
}
2. 子进程职责
- 处理客户端通信(通过
m_clientfd
)。 - 关闭监听Socket:专注当前任务,避免干扰。子进程在
fork()
之后调用tcpserver.closelisten()
关闭监听的socket。
if (pid == 0)
{
tcpserver.closelisten(); // 子进程关闭监听的socket。
// 子进程负责与客户端进行通讯
}
3. fork()
的底层行为
- 文件描述符复制:子进程复制父进程的所有Socket。
- 引用计数机制:每个Socket的引用计数随
fork()
增加,需父子进程协作关闭。
四、信号处理机制
1. 信号处理逻辑
// 忽略所有信号(防止僵尸进程)
for (int ii = 1; ii <= 64; ii++) signal(ii, SIG_IGN);
// 设置父进程退出信号
signal(SIGTERM, FathEXIT);
signal(SIGINT, FathEXIT);
2. 关键信号
SIGTERM
:kill
命令触发的终止信号。SIGINT
:Ctrl+C中断信号。SIGCHLD
:子进程退出信号(通过忽略自动回收资源)。
3. 信号处理函数
FathEXIT
:父进程退出前关闭监听Socket,终止所有子进程。
void FathEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "父进程退出,sig=" << sig << endl;
kill(0,SIGTERM); // 向全部的子进程发送15的信号,通知它们退出。
// 在这里增加释放资源的代码(全局的资源)。
tcpserver.closelisten(); // 父进程关闭监听的socket。
exit(0);
}
ChldEXIT
:子进程退出前关闭客户端Socket。
void ChldEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "子进程" << getpid() << "退出,sig=" << sig << endl;
// 在这里增加释放资源的代码(只释放子进程的资源)。
tcpserver.closeclient(); // 子进程关闭客户端连上来的socket。
exit(0);
}
五、Socket编程细节
1. struct sockaddr_in
- 作用:存储IPv4地址信息(IP + 端口)。
- 成员:
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET)
in_port_t sin_port; // 端口(网络字节序)
struct in_addr sin_addr; // IP地址(二进制形式)
char sin_zero[8];// 填充字段
};
在initserver()
函数中使用该结构体来绑定服务端的IP和端口。
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(m_port);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
close(m_listenfd); m_listenfd=-1; return false;
}
2. 初始化Socket的三步曲
- 创建Socket:
socket(AF_INET, SOCK_STREAM, 0)
。
if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
- 绑定地址:
bind()
指定IP和端口。
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
close(m_listenfd); m_listenfd=-1; return false;
}
- 开始监听:
listen()
设置队列长度。
if (listen(m_listenfd,5) == -1 )
{
close(m_listenfd); m_listenfd=-1; return false;
}
3. accept()
的工作原理
- 阻塞等待:直到有客户端连接。
- 返回新Socket:
m_clientfd
专用于当前客户端。
if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return false;
六、引用计数与资源管理
1. 引用计数机制
- 定义:内核为每个Socket维护全局引用计数,表示使用该Socket的进程数。
- 关闭操作:
close()
减少引用计数,归零时内核释放资源。
2. 父子进程的关闭逻辑
进程 | 关闭的Socket | 目的 |
---|---|---|
父进程 | m_clientfd | 避免资源泄漏,专注监听新连接 |
子进程 | m_listenfd | 避免干扰父进程,专注通信任务 |
3. 资源泄漏的后果
- 未关闭Socket:端口占用、内存泄漏、僵尸进程。
七、常见问题解答
1. 为何父进程关闭客户端Socket?
- 资源管理:父进程不处理通信,关闭避免泄漏。
- 引用计数:父进程关闭后,子进程是唯一持有者,确保最终释放。
2. 为何子进程关闭监听Socket?
- 职责分离:子进程无需监听新连接,关闭避免干扰父进程。
3. 如何避免僵尸进程?
- 忽略
SIGCHLD
信号:内核自动回收子进程资源。在代码中,通过for (int ii = 1; ii <= 64; ii++) signal(ii, SIG_IGN);
忽略所有信号,包括SIGCHLD
。
4. close()
与shutdown()
的区别
close()
:减少引用计数,归零时释放资源。shutdown()
:强制关闭读写通道,不依赖引用计数。
八、总结与最佳实践
1. 关键知识点
- 多进程分工:父进程监听,子进程通信。
- 信号处理:优雅退出和资源回收。
- Socket引用计数:父子进程协作关闭Socket。
2. 最佳实践
- 明确关闭职责:父子进程及时关闭不需要的Socket。
- 使用
shutdown()
:在需要强制关闭时使用。 - 监控工具:
netstat
检查Socket状态,ps
查看进程状态。