当前位置: 首页 > news >正文

TCP 多进程服务端

TCP 多进程服务端

TCP 多进程服务端 笔记

这是一个支持多客户端同时连接的 TCP 服务端,采用 父进程监听 + 子进程处理业务 的多进程模型,是 Linux 网络编程最经典的模型之一。


一、程序核心功能

  1. 单端口监听,支持无数客户端同时连接
  2. 父进程:只负责 accept() 等待新客户端连接
  3. 子进程:每个客户端连接成功,就创建一个子进程专门和它通信
  4. 信号安全处理:可以用 Ctrl+C / kill 正常退出,不产生僵尸进程
  5. 通信逻辑:接收客户端消息 → 回复 ok

二、多进程模型流程图

父进程:socket → bind → listen → while(true) accept()↓ (每来一个客户端)fork()├── 父进程:关闭客户端socket → 继续循环监听└── 子进程:关闭监听socket → 收发数据 → 退出

三、代码结构总览

// 1. 头文件
// 2. TCP服务端类 ctcpserver
// 3. 信号处理函数(父进程、子进程)
// 4. main 函数(多进程核心逻辑)

四、逐模块精讲(最关键)

模块1:TCP 服务端类 ctcpserver

和你之前的文件传输服务端几乎一样,功能:

  • initserver():创建、绑定、监听 socket
  • accept():接受客户端连接
  • send()/recv():收发数据
  • closelisten()/closeclient():安全关闭套接字

重点:

  • 父进程只用 m_listenfd
  • 子进程只用 m_clientfd

模块2:信号处理(非常重要)

1. 父进程信号函数 FathEXIT

void FathEXIT(int sig){signal(SIGINT,SIG_IGN);    // 防止重复触发signal(SIGTERM,SIG_IGN);cout<<"父进程退出\n";kill(0,SIGTERM);    // 给所有子进程发退出信号exit(0);
}

作用:

  • 捕获 Ctrl+C / kill
  • 通知所有子进程一起退出
  • 安全退出,不留僵尸进程

2. 子进程信号函数 ChIdEXIT

void ChIdEXIT(int sig){cout<<"子进程"<<getpid()<<"退出\n";exit(0);
}

作用:收到父进程通知后安全退出


五、main 函数 多进程核心逻辑(逐行精讲)

1. 忽略所有信号 + 注册退出信号

for(int i=0;i<=64;i++)signal(i,SIG_IGN);  // 先全部忽略signal(SIGTERM,FathEXIT);   // kill 命令
signal(SIGINT,FathEXIT);    // Ctrl+C

目的:让程序可以安全退出,不被系统信号打断


2. 初始化服务端(bind + listen)

ctcpserver tcpserver;
tcpserver.initserver(stoi(argv[1]));

3. 无限循环接受连接

while(true){tcpserver.accept();   // 阻塞等待客户端

4. 创建子进程 fork()

int pid = fork();   // 创建子进程

fork() 会返回两次

  • 返回 > 0:当前是父进程
  • 返回 = 0:当前是子进程
  • 返回 -1:失败

5. 父进程逻辑

if(pid > 0){tcpserver.closeclient();  // 父进程不需要客户端socketcontinue;                 // 回去继续监听
}

6. 子进程逻辑

tcpserver.closelisten();    // 子进程不需要监听socketsignal(SIGTERM,ChIdEXIT);    // 子进程注册自己的信号
signal(SIGINT,SIG_IGN);

子进程正式通信:

while(true){tcpserver.recv(buffer, 1024);  // 接收cout << "接收:" << buffer << endl;buffer = "ok";tcpserver.send(buffer);        // 回复 ok
}

六、多进程编程 3 条黄金规则

规则1:父进程只监听,必须关闭 clientfd

tcpserver.closeclient();

规则2:子进程只通信,必须关闭 listenfd

tcpserver.closelisten();

规则3:父子进程必须有独立的信号处理

  • 父进程:负责统一退出、管理子进程
  • 子进程:负责业务、收到通知后退出

七、为什么要这样设计?

  1. 单进程只能处理一个客户端
  2. 多进程可以处理无数个客户端
  3. 父子进程分工明确,稳定、安全
  4. 适合:并发服务、中间件、网关、游戏服务器

八、运行方式

g++ addcourse.cpp -o addcourse
./addcourse 5005

测试(用另一个终端):

telnet 127.0.0.1 5005

发消息 → 收到 ok


九、总结

多进程 TCP 服务端

  1. 父进程accept() 等待连接 → fork() → 关闭 clientfd
  2. 子进程:关闭 listenfd → 处理通信 → 退出
  3. 信号机制:保证父子进程安全退出
  4. 优点:稳定、并发强、适合生产环境

十、完整代码

/*程序名:addcourse.cpp,多进程TCP服务端功能:父进程监听,子进程处理客户端通信,支持多客户端同时连接
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<netdb.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<signal.h>
using namespace std;
class ctcpserver{       //tcp通讯的服务端类private:int m_listenfd;     //监听的socket,-1表示未初始化int m_clientfd;     //客户端连上来的socket,-1表示客户端未连接string m_clientip;      //客户端的IPunsigned short m_port;      //服务端用于通讯的端口public:ctcpserver():m_listenfd(-1),m_clientfd(-1){}//初始化服务端用于监听的socketbool initserver(const unsigned short in_port){//第1步:创建服务端的socketif((m_listenfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==-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;    //指定ipv4协议servaddr.sin_addr.s_addr=htonl(INADDR_ANY);     //INADDR_ANY 客户端可以从任意一个本机IP地址连接到服务端servaddr.sin_port=htons(m_port); //指定端口if(bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1){    //bind返回值为非0(失败)close(m_listenfd);m_listenfd=-1;return false;} //第3步:把socket设置成可连接的状态if(listen(m_listenfd,5)==-1){      //listen返回值为非0(失败) 5就是设置队列的最大长度为5,当连接请求超过此值,新请求将被拒绝close(m_listenfd);return false;}return true;}//第4步:受理客户端的连接请求(从已连接的客户端中取出一个客户端),如果没有客户端连上来,accept()函数将阻塞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;}//获取客户端的IP(字符串格式)//const string &表示引用的不会变(只读) 后面的const 表示不会对成员变量进行修改const string &clientip() const{return m_clientip;}//向对端发送报文,成功返回true,失败返回falsebool send(const string &buffer){if(m_clientfd==-1)return false;     //如果socket的状态是未连接,直接返回falseif((::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){//如果直接操作string对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小buffer.clear();     //清空容器//resize buffer.resize(maxlen);      //设置容器的大小为maxlen//用buffer.c_str()或者buffer.data()获取首地址是const 这里只能用&buffer[0]int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0);        //直接操作buffer的内存,返回实际值给readnif(readn<=0){buffer.clear();return false;}buffer.resize(readn);       //重置buffer的实际大小return true;}//关闭监听的socketbool closelisten(){if(m_listenfd==-1)return false;::close(m_listenfd);m_listenfd=-1;return true;}//关闭客户端连接上来的socketbool closeclient(){if(m_clientfd==-1)return false;::close(m_clientfd);m_clientfd=-1;return true;}~ctcpserver(){closelisten();closeclient();}
};
void FathEXIT(int sig);     //父进程的信号处理函数
void ChIdEXIT(int sig);     //子进程的信号处理函数
int main(int argc,char *argv[]){if(argc!=2){cout<<"Using:./addcourse 通讯端口\nExample:./addcourse 5005\n\n";cout<<"注意:运行服务端程序的linux系统的防火墙必须开通5005端口\n";cout<<" 如果是云服务器,还要开通云平台的访问策略\n\n";return -1;}//忽略全部的信号,不希望被打扰for(int i=0;i<=64;i++)signal(i,SIG_IGN);//设置信号,在shell状态下可用"kill"进程号或"ctrl+c"正常终止//但请不要用"kill -9 进程号"强制终止signal(SIGTERM,FathEXIT);   //15signal(SIGINT,FathEXIT);    //2ctcpserver tcpserver;if(tcpserver.initserver(stoi(argv[1]))==false){     //初始化服务端用于监听的socketperror("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,ChIdEXIT);   //子进程的退出函数与父进程不一样signal(SIGINT,SIG_IGN); //子进程不需要捕获SIGINT信号//子进程负责与客户端进行通讯cout<<"客户端已连接:"<<tcpserver.clientip()<<endl;//第5步:与客户端通信,接收客户端发过来的报文后,回复okstring buffer;while(true){//接收队端的回应报文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()函数的位置
}}
void FathEXIT(int sig){//为了防止信号处理函数在执行是过程中再次被信号中断signal(SIGINT,SIG_IGN);signal(SIGTERM,SIG_IGN);cout<<"父进程退出,sig="<<sig<<endl;kill(0,SIGTERM);    //通知全部的子进程退出//在这里增加释(只释放子进程的资源)exit(0);
}//子进程的信号处理函数
void ChIdEXIT(int sig){//为了防止信号处理函数在执行是过程中再次被信号中断signal(SIGINT,SIG_IGN);signal(SIGTERM,SIG_IGN);cout<<"子进程"<<getpid()<<"退出,sig="<<sig<<endl;//在这里增加释(只释放子进程的资源)exit(0);
}

http://www.jsqmd.com/news/830230/

相关文章:

  • 前端超能力:解锁浏览器控制权
  • FSearch终极指南:5分钟掌握Linux极速文件搜索神器
  • 5种技术方案彻底解决国内容器镜像拉取难题:DaoCloud公开镜像仓库实战指南
  • 告别水下照片的蓝绿色偏:手把手教你用OpenCV和Python实现图像增强与色彩还原
  • VTube Studio API开发终极指南:30分钟快速创建专业虚拟主播插件
  • 3分钟精通:Obsidian Excel转Markdown表格插件如何提升你的笔记效率500%
  • 嵌入式系统DDR选型实战:从规格参数到性能压测
  • 基于Docker与MCP协议构建AI智能体安全扩展工具箱
  • 5分钟终极指南:让你的Windows任务栏变透明,桌面美化从此简单
  • 通过模型广场快速对比与选择适合任务的大模型
  • PHP的final 类禁止继承的庖丁解牛
  • 英飞凌Aurix2G TC3XX时钟系统实战:从理论到MCAL配置全解析
  • 【ElevenLabs卡纳达文语音权威测评】:对比Amazon Polly与Google WaveNet,实测WPM、MOS分与情感连贯性数据
  • DayZ单机模式终极指南:用DayZCommunityOfflineMode打造专属末日世界
  • AI时代给予的是什么?
  • 黑鲨2 Pro游戏手机深度评测:性能怪兽如何用肩键与散热征服硬核玩家
  • 直播革命:GPT-Image2实时生成重塑互动体验
  • D3KeyHelper终极指南:如何用免费开源工具实现暗黑3一键操作革命
  • 保姆级教程:用PennyLane和泰坦尼克号数据集,5分钟上手你的第一个量子分类器(VQC)
  • 微服务架构设计模式:从理论到实战
  • 基于RT-Thread与MQTT的智慧班车管理系统:从硬件选型到云端部署全流程实战
  • 3分钟极速上手:Onekey Steam清单下载终极指南
  • Hermes桌面版安装使用指南与AI模型搭配性价比分析
  • 噬菌体:植物病害的 “天然杀手”,农业可持续的新希望
  • Cocos游戏开发中的Vibe Coding零代码实战与痛点,很详细!
  • 手把手教你用reverse-sourcemap调试线上Vue应用:从压缩JS到定位源码行号
  • AEUX终极指南:免费实现Figma/Sketch到After Effects的无缝动效转换
  • 【ElevenLabs儿童语音合成实战指南】:20年AI语音工程师亲授7大合规避坑要点与情感化调参公式
  • 为Hermes Agent配置自定义供应商接入Taotoken多模型广场
  • 如何用CellProfiler实现生物图像自动分析:创新方法