🔭个人主页:****北 海
🛜所属专栏:****Linux学习之旅、神奇的网络世界
💻操作环境:****CentOS 7.6 阿里云远程服务器
文章目录
🌤️前言
随着数字时代的来临,TCP网络程序已成为程序员不可或缺的技术领域。本博客将带领读者深入研究,从最基础的字符串回响开始,逐步探索至多进程、多线程服务器的高级实践。我们将详细探讨每个环节的核心功能和实现细节,致力于帮助读者深刻理解网络编程的本质。通过系统学习本博客内容,读者将获得构建稳健网络应用的重要技能,更加自信地应对日益复杂的软件开发挑战。这里将为你的编程旅程提供扎实的基础和深远的启示。
🌦️正文
TCP网络程序
接下来实现一批基于
TCP
协议的网络程序
1.字符串回响
1.1.核心功能
字符串回响程序类似于
echo
指令,客户端向服务器发送消息,服务器在收到消息后会将消息发送给客户端,该程序实现起来比较简单,同时能很好的体现
socket
套接字编程的流程
1.2.程序结构
这个程序我们已经基于
UDP
协议实现过了,换成
TCP
协议实现时,程序的结构是没有变化的,同样需要
server.hpp
、
server.cc
、
client.hpp
、
client.cc
这几个文件
创建
server.hpp
服务器头文件
#pragmaonce#include<iostream>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_server
{constuint16_t default_port =8888;// 默认端口号classTcpServer{public:TcpServer(constuint16_t port = default_port):port_(port){}~TcpServer(){}// 初始化服务器voidInitServer(){}// 启动服务器voidStartServer(){}private:int sock_;// 套接字(存疑)uint16_t port_;// 端口号};}
注意:**这里的
sock_
套接字成员后面需要修改**
创建
server.cc
服务器源文件
#include<memory>// 智能指针头文件#include"server.hpp"usingnamespace std;usingnamespace nt_server;intmain(){
unique_ptr<TcpServer>usvr(newTcpServer());
usvr->InitServer();
usvr->StartServer();return0;}
创建
client.hpp
客户端头文件
#pragmaonce#include<iostream>#include<string>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_client
{classTcpClient{public:TcpClient(const std::string& ip,constuint16_t port):server_ip_(ip),server_port_(port){}~TcpClient(){}// 初始化客户端voidInitClient(){}// 启动客户端voidStartClient(){}private:int sock_;// 套接字
std::string server_ip_;// 服务器IPuint16_t server_port_;// 服务器端口号};}
创建
client.cc
客户端源文件
#include<memory>#include"client.hpp"usingnamespace std;usingnamespace nt_client;voidUsage(constchar*program){
cout <<"Usage:"<< endl;
cout <<"\t"<< program <<" ServerIP ServerPort"<< endl;}intmain(int argc,char*argv[]){if(argc !=3){// 错误的启动方式,提示错误信息Usage(argv[0]);return USAGE_ERR;}// 服务器IP与端口号
string ip(argv[1]);uint16_t port =stoi(argv[2]);
unique_ptr<TcpClient>usvr(newTcpClient(ip, port));
usvr->InitClient();
usvr->StartClient();return0;}
同时需要一个
Makefile
文件,用于快速编译和清理可执行程序
创建
Makefile
文件
.PHONY:all
all:server client
server:server.cc
g++ -o$@ $^ -std=c++11
client:client.cc
g++ -o$@ $^ -std=c++11
.PHONY:clean
clean:
rm-rf server client
最后为了方便判断程序错误,可以增加上一篇文章中的
err.hpp
头文件,里面包含错误码与简易错误信息
创建
err.hpp
错误码头文件
#pragmaonceenum{
USAGE_ERR =1,
SOCKET_ERR,
BIND_ERR
};
接下来开始填充代码内容
服务器
1.3.初始化服务器
基于
TCP
协议实现的网络程序也需要 **创建套接字、绑定
IP
和端口号**
在使用
socket
函数创建套接字时,
UDP
协议需要指定参数2为
SOCK_DGRAM
,
TCP
协议则是指定参数2为
SOCK_STREAM
注:**关于
socket
、
bind
、
sockaddr
的细节,可以看看这篇文章《网络编程『socket套接字 ‖ 简易UDP网络程序』》**
InitServer()
初始化服务器函数 — 位于
server.hpp
服务器头文件中的
TcpServer
类
constuint16_t default_port =8888;// 默认端口号// 初始化服务器voidInitServer(){// 1.创建套接字
sock_ =socket(AF_INET, SOCK_STREAM,0);if(sock_ ==-1){
std::cerr <<"Create Socket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create Socket Success! "<< sock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr*)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.TODO}
注意:在绑定端口号时,一定需要把主机序列转换为网络序列
为什么在绑定端口号阶段需要手动转换为网络序列,而在发送信息阶段则不需要?
**这是因为在发送信息阶段,recvfrom / sendto
等函数会自动将需要发送的信息转换为网络序列,接收信息时同样会将其转换为主机序列,所以不需要手动转换**
如果使用的
UDP
协议,那么初始化服务器到此就结束了,但我们本文中使用的是
TCP
协议,这是一个 面向连接 的传输层协议,意味着在初始化服务器时,需要设置服务器为 监听 状态
使用到的函数是
listen
函数
#include<sys/types.h>/* See NOTES */#include<sys/socket.h>intlisten(int sockfd,int backlog);
参数解读:
sockfd
通过该套接字进行监听backlog
全连接队列最大长度
返回值:**监听成功返回
0
,失败返回
-1
**
这里的参数2需要设置一个整数,通常为 **
16、32、64...
**,表示 全连接队列 的最大长度,关于 全连接队列 的详细知识放到后续博客中讲解,这里只需要直接使用
server.hpp
服务器头文件
#pragmaonce#include<iostream>#include<cerrno>#include<cstring>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_server
{constuint16_t default_port =8888;// 默认端口号constint backlog =32;// 全连接队列的最大长度classTcpServer{public:TcpServer(constuint16_t port = default_port):port_(port){}~TcpServer(){}// 初始化服务器voidInitServer(){// 1.创建套接字
sock_ =socket(AF_INET, SOCK_STREAM,0);if(sock_ ==-1){
std::cerr <<"Create Socket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create Socket Success! "<< sock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr*)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.监听if(listen(sock_, backlog)==-1){
std::cerr <<"Listen Fail!"<<strerror(errno)<< std::endl;exit(LISTEN_ERR);}
std::cout <<"Listen Success!"<< std::endl;}// 启动服务器voidStartServer(){}private:int sock_;// 套接字(存疑)uint16_t port_;// 端口号};}
至此基于
TCP
协议实现的初始化服务器函数就填充完成了,编译并运行服务器,显示初始化服务器成功
1.4.启动服务器
1.4.1.处理连接请求
TCP
是面向连接,当有客户端发起连接请求时,
TCP
服务器需要正确识别并尝试进行连接,当连接成功时,与其进行通信,可使用
accept
函数进行连接
#include<sys/types.h>/* See NOTES */#include<sys/socket.h>intaccept(int sockfd,structsockaddr*addr, socklen_t *addrlen);
参数解读:
sockfd
服务器用于处理连接请求的socket
套接字addr
客户端的sockaddr
结构体信息addrlen
客户端的sockaddr
结构体大写
其中
addr
与
addrlen
是一个 输入输出型 参数,类似于
recvfrom
中的参数
返回值:**连接成功返回一个用于通信的
socket
套接字(文件描述符),失败返回
-1
**
这也就意味着之前我们在
TcpServer
中创建的类内成员
sock_
并非是用于通信,而是专注于处理连接请求,在
TCP
服务器中,这种套接字称为 监听套接字
使用
accept
函数处理连接请求
server.hpp
服务器头文件
#pragmaonce#include<iostream>#include<cerrno>#include<cstring>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_server
{constuint16_t default_port =8888;// 默认端口号constint backlog =32;// 全连接队列的最大长度classTcpServer{public:TcpServer(constuint16_t port = default_port):port_(port),quit_(false){}~TcpServer(){}// 初始化服务器voidInitServer(){// 1.创建监听套接字
listensock_ =socket(AF_INET, SOCK_STREAM,0);if(listensock_ ==-1){
std::cerr <<"Create ListenSocket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create ListenSocket Success! "<< listensock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr*)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.监听if(listen(listensock_, backlog)==-1){
std::cerr <<"Listen Fail!"<<strerror(errno)<< std::endl;exit(LISTEN_ERR);}
std::cout <<"Listen Success!"<< std::endl;}// 启动服务器voidStartServer(){while(!quit_){// 1.处理连接请求structsockaddr_in client;
socklen_t len =sizeof(client);int sock =accept(listensock_,(structsockaddr*)&client,&len);// 2.如果连接失败,继续尝试连接if(sock ==-1){
std::cerr <<"Accept Fail!"<<strerror(errno)<< std::endl;continue;}// 连接成功,获取客户端信息
std::string clientip =inet_ntoa(client.sin_addr);uint16_t clientport =ntohs(client.sin_port);
std::cout <<"Server accept "<< clientip +"-"<< clientport <<" "<< sock <<" from "<< listensock_ <<" success!"<< std::endl;// 3.根据 sock 套接字进行通信Service(sock, clientip, clientport);}}private:int listensock_;// 监听套接字uint16_t port_;// 端口号bool quit_;// 判断服务器是否结束运行};}
1.4.2.业务处理
对于
TCP
服务器来说,它是面向字节流传输的,我们之前使用的文件相关操作也是面向字节流,凑巧的是在
Linux
中网络是以挂接在文件系统的方式实现的,种种迹象表明:可以通过文件相关接口进行通信
read
从文件中读取信息(接收消息)write
向文件中写入信息(发送消息)
这两个系统调用的核心参数是
fd
(文件描述符),即服务器与客户端在连接成功后,获取到的
socket
套接字,所以接下来可以按文件操作的套路,完成业务处理
Service()
业务处理函数 — 位于
server.hpp
服务器头文件中的
TcpServer
类
// 业务处理voidService(int sock,const std::string& clientip,constuint16_t& clientport){char buff[1024];
std::string who = clientip +"-"+ std::to_string(clientport);while(true){
ssize_t n =read(sock, buff,sizeof(buff)-1);// 预留 '\0' 的位置if(n >0){// 读取成功
buff[n]='\0';
std::cout <<"Server get: "<< buff <<" from "<< who << std::endl;
std::string respond =func_(buff);// 实际业务处理由上层指定// 发送给服务器write(sock, buff,strlen(buff));}elseif(n ==0){// 表示当前读取到文件末尾了,结束读取
std::cout <<"Client "<< who <<" "<< sock <<" quit!"<< std::endl;close(sock);// 关闭文件描述符break;}else{// 读取出问题(暂时)
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock);// 关闭文件描述符break;}}}
1.4.3.回调函数
为了更好的实现功能解耦,这里将真正的业务处理函数交给上层处理,编写完成后传给
TcpServer
对象即可,当然,在
TcpServer
类中需要添加对应的类型
这里设置回调函数的返回值为
string
,参数同样为
string
server.hpp
服务器头文件
#pragmaonce#include<iostream>#include<string>#include<functional>#include<cerrno>#include<cstring>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_server
{constuint16_t default_port =8888;// 默认端口号constint backlog =32;// 全连接队列的最大长度using func_t = std::function<std::string(std::string)>;// 回调函数类型classTcpServer{public:TcpServer(const func_t &func,constuint16_t port = default_port):func_(func),port_(port),quit_(false){}~TcpServer(){}// 初始化服务器voidInitServer(){// 1.创建监听套接字
listensock_ =socket(AF_INET, SOCK_STREAM,0);if(listensock_ ==-1){
std::cerr <<"Create ListenSocket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create ListenSocket Success! "<< listensock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr *)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.监听if(listen(listensock_, backlog)==-1){
std::cerr <<"Listen Fail!"<<strerror(errno)<< std::endl;exit(LISTEN_ERR);}
std::cout <<"Listen Success!"<< std::endl;}// 启动服务器voidStartServer(){while(!quit_){// 1.处理连接请求structsockaddr_in client;
socklen_t len =sizeof(client);int sock =accept(listensock_,(structsockaddr*)&client,&len);// 2.如果连接失败,继续尝试连接if(sock ==-1){
std::cerr <<"Accept Fail!"<<strerror(errno)<< std::endl;continue;}// 连接成功,获取客户端信息
std::string clientip =inet_ntoa(client.sin_addr);uint16_t clientport =ntohs(client.sin_port);
std::cout <<"Server accept "<< clientip +"-"<< clientport <<" "<< sock <<" from "<< listensock_ <<" success!"<< std::endl;// 3.根据 sock 套接字进行通信Service(sock, clientip, clientport);}}// 业务处理voidService(int sock,const std::string& clientip,constuint16_t& clientport){char buff[1024];
std::string who = clientip +"-"+ std::to_string(clientport);while(true){
ssize_t n =read(sock, buff,sizeof(buff)-1);// 预留 '\0' 的位置if(n >0){// 读取成功
buff[n]='\0';
std::cout <<"Server get: "<< buff <<" from "<< who << std::endl;
std::string respond =func_(buff);// 实际业务处理由上层指定// 发送给服务器write(sock, buff,strlen(buff));}elseif(n ==0){// 表示当前读取到文件末尾了,结束读取
std::cout <<"Client "<< who <<" "<< sock <<" quit!"<< std::endl;close(sock);// 关闭文件描述符break;}else{// 读取出问题(暂时)
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock);// 关闭文件描述符break;}}}private:int listensock_;// 监听套接字uint16_t port_;// 端口号bool quit_;// 判断服务器是否结束运行
func_t func_;// 回调函数};}
服务器头文件准备完成,接下来就是填充
server.cc
服务器源文件
1.5.服务器源文件
对于当前的
TCP
网络程序(字符串回响)来说,业务处理函数逻辑非常简单,无非就是直接将客户端发送过来的消息,重新转发给客户端
server.cc
服务器源文件
#include<memory>// 智能指针头文件#include<string>#include"server.hpp"usingnamespace std;usingnamespace nt_server;// 业务处理回调函数(字符串回响)
string echo(string request){return request;}intmain(){
unique_ptr<TcpServer>usvr(newTcpServer(echo));// 将回调函数进行传递
usvr->InitServer();
usvr->StartServer();return0;}
尝试编译并运行服务器,可以看到当前
bash
已经被我们的服务器程序占用了,重新打开一个终端,并通过
netstat
命令查看网络使用情况(基于
TCP
协议)
netstat-nltp
当前服务确实使用的是
8888
端口,并且采用的是
TCP
协议
客户端
1.6.初始化客户端
对于客户端来说,服务器的
IP
地址与端口号是两个不可或缺的元素,因此在客户端类中,
server_ip
和
server_port
这两个成员是少不了的,当然得有
socket
套接字
初始化客户端只需要干一件事:创建套接字,客户端是主动发起连接请求的一方,也就意味着它不需要使用
listen
函数设置为监听状态
注意:**客户端也是需要
bind
绑定的,但不需要自己手动绑定,由操作系统帮我们自动完成**
client.hpp
客户端头文件
#pragmaonce#include<iostream>#include<string>#include<cstring>#include<cerrno>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_client
{classTcpClient{public:TcpClient(const std::string& ip,constuint16_t port):server_ip_(ip),server_port_(port){}~TcpClient(){}// 初始化客户端voidInitClient(){// 创建套接字
sock_ =socket(AF_INET, SOCK_STREAM,0);if(sock_ ==-1){
std::cerr <<"Create Socket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create Sock Succeess! "<< sock_ << std::endl;}// 启动客户端voidStartClient(){}private:int sock_;// 套接字
std::string server_ip_;// 服务器IPuint16_t server_port_;// 服务器端口号};}
编译并运行客户端,显示
socket
套接字创建成功
1.7.启动客户端
1.7.1.尝试进行连接
因为
TCP
协议是面向连接的,服务器已经处于处理连接请求的状态了,客户端现在需要做的就是尝试进行连接,使用
connect
函数进行连接
#include<sys/types.h>/* See NOTES */#include<sys/socket.h>intconnect(int sockfd,conststructsockaddr*addr, socklen_t addrlen);
参数解读:
sockfd
需要进行连接的套接字addr
服务器的sockaddr
结构体信息addrlen
服务器的sockaddr
结构体大小
返回值:**连接成功返回
0
,连接失败返回
-1
**
在连接过程中,可能遇到很多问题,比如 网络传输失败、服务器未启动 等,这些问题的最终结果都是客户端连接失败,如果按照之前的逻辑(失败就退出),那么客户端的体验感会非常不好,因此在面对连接失败这种常见问题时,客户端应该尝试重连,如果重连数次后仍然失败,才考虑终止进程
注意:**在进行重连时,可以使用
sleep()
等函数使程序睡眠一会,给网络恢复留出时间**
StartClient()
启动客户端函数 — 位于
client.hpp
中的
TcpClient
类
// 启动客户端voidStartClient(){// 填充服务器的 sockaddr_in 结构体信息structsockaddr_in server;
socklen_t len =sizeof(server);memset(&server,0, len);
server.sin_family = AF_INET;inet_aton(server_ip_.c_str(),&server.sin_addr);// 将点分十进制转化为二进制IP地址的另一种方法
server.sin_port =htons(server_port_);// 尝试重连 5 次int n =5;while(n){int ret =connect(sock_,(conststructsockaddr*)&server, len);if(ret ==0){// 连接成功,可以跳出循环break;}// 尝试进行重连
std::cerr <<"网络异常,正在进行重连... 剩余连接次数: "<<--n << std::endl;sleep(1);}// 如果剩余重连次数为 0,证明连接失败if(n ==0){
std::cerr <<"连接失败! "<<strerror(errno)<< std::endl;close(sock_);exit(CONNECT_ERR);}// 连接成功
std::cout <<"连接成功!"<< std::endl;// 进行业务处理// Service();}
当然相应的错误码也得添加
err.hpp
错误码头文件
#pragmaonceenum{
USAGE_ERR =1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR
};
现在先不启动服务器,编译并启动客户端,模拟连接失败的情况
如果在数秒之后启动再服务器,可以看到重连成功
这种重连机制在实际中非常常见,出现这种
1.7.2.业务处理
客户端在进行业务处理时,同样可以使用
read
和
write
进行网络通信
Service()
业务处理函数 — 位于
client.hpp
客户端头文件中的
TcpClient
类
// 业务处理voidService(){char buff[1024];
std::string who = server_ip_ +"-"+ std::to_string(server_port_);while(true){// 由用户输入信息
std::string msg;
std::cout <<"Please Enter >> ";
std::getline(std::cin, msg);// 发送信息给服务器write(sock_, msg.c_str(), msg.size());// 接收来自服务器的信息
ssize_t n =read(sock_, buff,sizeof(buff)-1);if(n >0){// 正常通信
buff[n]='\0';
std::cout <<"Client get: "<< buff <<" from "<< who << std::endl;}elseif(n ==0){// 读取到文件末尾(服务器关闭了)
std::cout <<"Server "<< who <<" quit!"<< std::endl;close(sock_);// 关闭文件描述符break;}else{// 读取异常
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock_);// 关闭文件描述符break;}}}
至此整个 **基于
TCP
协议的字符串回响程序** 就完成了,下面来看看效果
可以看到,当客户端向服务器发起连接请求时,服务器可以识别并接受连接,双方建立连接关系后,可以正常进行通信;当客户端主动退出(断开连接),服务器也能感知到,并判断出是谁断开了连接
如果在通信过程中,服务器主动断开了连接,客户端也能感知到
如果我们此时立马重启服务器,会发现短期内无法再次启动服务(显示端口正在被占用),这是由于
TCP
协议断开连接时的特性导致的(正在处于
TIME_WAIT
状态),详细原因将会在后续博客中讲解
2.多进程版服务器
2.1.核心功能
对于之前编写的 字符串回响程序 来说,如果只有一个客户端进行连接并通信,是没有问题的,但如果有多个客户端发起连接请求,并尝试进行通信,服务器是无法应对的
原因在于 服务器是一个单进程版本,处理连接请求 和 业务处理 是串行化执行的,如果想处理下一个连接请求,需要把当前的业务处理完成
具体表现为下面这种情况
为什么客户端B会显示当前已经连接成功?
这是因为是客户端是主动发起连接请求的一方,在请求发出后,如果出现连接错误,客户端就认为已经连接成功了,但实际上服务器还没有处理这个连接请求
这显然是服务器的问题,处理连接请求 与 业务处理 应该交给两个不同的执行流完成,可以使用多进程或者多线程解决,这里先采用多进程的方案
所以当前需要实现的网络程序核心功能为:**当服务器成功处理连接请求后,
fork
新建一个子进程,用于进行业务处理,原来的进程专注于处理连接请求**
2.2.创建子进程
注:**当前的版本的修改只涉及
StartServer()
函数**
创建子进程使用
fork()
函数,它的返回值含义如下
ret == 0
表示创建子进程成功,接下来执行子进程的代码ret > 0
表示创建子进程成功,接下来执行父进程的代码ret < 0
表示创建子进程失败
子进程创建成功后,会继承父进程的文件描述符表,能轻而易举的获取客户端的
socket
套接字,从而进行网络通信
当然不止文件描述符表,得益于 写时拷贝 机制,子进程还会共享父进程的变量,当发生修改行为时,才会自己创建
注意:**当子进程取走客户端的
socket
套接字进行通信后,父进程需要将其关闭(因为它不需要了),避免文件描述符泄漏**
StartServer()
服务器启动函数 — 位于
server.hpp
的
TcpServer
类
// 进程创建、等待所需要的头文件#include<unistd.h>#include<sys/wait.h>#include<sys/types.h>// 启动服务器voidStartServer(){while(!quit_){// 1.处理连接请求structsockaddr_in client;
socklen_t len =sizeof(client);int sock =accept(listensock_,(structsockaddr*)&client,&len);// 2.如果连接失败,继续尝试连接if(sock ==-1){
std::cerr <<"Accept Fail!"<<strerror(errno)<< std::endl;continue;}// 连接成功,获取客户端信息
std::string clientip =inet_ntoa(client.sin_addr);uint16_t clientport =ntohs(client.sin_port);
std::cout <<"Server accept "<< clientip +"-"<< clientport <<" "<< sock <<" from "<< listensock_ <<" success!"<< std::endl;// 3.创建子进程
pid_t id =fork();if(id <0){// 创建子进程失败,暂时不与当前客户端建立通信会话close(sock);
std::cerr <<"Fork Fail!"<< std::endl;}elseif(id ==0){// 子进程内close(listensock_);// 子进程不需要监听(建议关闭)// 执行业务处理函数Service(sock, clientip, clientport);exit(0);// 子进程退出}else{// 父进程需要等待子进程
pid_t ret =waitpid(id,nullptr,0);// 默认为阻塞式等待if(ret == id)
std::cout <<"Wait "<< id <<" success!";}}}
虽然此时成功创建了子进程,但父进程(处理连接请求)仍然需要等待子进程退出后,才能继续运行,说白了就是 父进程现在处于阻塞等待状态,需要设置为 非阻塞等待
2.3.设置非阻塞
设置父进程为非阻塞的方式有很多,这里来一一列举
方式一:通过参数设置为非阻塞等待(不推荐)
可以直接给
waitpid()
函数的参数3传递
WNOHANG
,表示当前为 非阻塞等待
详见 《Linux进程控制【创建、终止、等待】》
pid_t ret =waitpid(id,nullptr, WNOHANG);// 设置为非阻塞式等待
这种方法可行,但不推荐,原因如下:**虽然设置成了非阻塞式等待,但父进程终究是需要通过
waitpid()
函数来尝试等待子进程,倘若父进程一直卡在
accept()
函数处,会导致子进程退出后暂时无人收尸,进而导致资源泄漏**
方式二:**忽略
SIGCHLD
信号(推荐使用)**
这是一个子进程在结束后发出的信号,默认动作是什么都不做;父进程需要检测并回收子进程,我们可以直接忽略该信号,这里的忽略是个特例,只是父进程不对其进行处理,转而由 操作系统 对其负责,自动清理资源并进行回收,不会产生 僵尸进程
详见 《Linux进程信号【信号处理】》
直接在
StartServer()
服务器启动函数刚开始时,使用
signal()
函数设置
SIGCHLD
信号的执行动作为 忽略
忽略了该信号后,就不需要父进程等待子进程退出了(由操作系统承担)
#include<signal.h>// 信号处理相关头文件// 启动服务器voidStartServer(){// 忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN);while(!quit_){// ...// 3.创建子进程
pid_t id =fork();if(id <0){// 创建子进程失败,暂时不与当前客户端建立通信会话close(sock);
std::cerr <<"Fork Fail!"<< std::endl;}elseif(id ==0){// 子进程内close(listensock_);// 子进程不需要监听(建议关闭)// 执行业务处理函数Service(sock, clientip, clientport);exit(0);// 子进程退出}}}
强烈推荐使用该方案,因为操作简单,并且没有后患之忧
方式三:**设置
SIGCHLD
信号的处理动作为子进程回收(不是很推荐)**
当子进程退出并发送该信号时,执行父进程回收子进程的操作
详见 《Linux进程信号【信号处理】》
设置
SIGCHLD
信号的处理动作为 回收子进程后,父进程同样不必再考虑回收子进程的问题
注意:**因为现在处于
TcpServer
类中,
handler()
函数需要设置为静态(避免隐含的
this
指针),避免不符合
signal()
函数中信号处理函数的参数要求**
#include<signal.h>// 信号处理相关头文件// 需要设置为静态staticvoidhandler(int signo){printf("进程 %d 捕捉到了 %d 号信号\n",getpid(), signo);// 这里的 -1 表示父进程等待时,只要是已经退出了的子进程,都可以进行回收while(1){
pid_t ret =waitpid(-1,NULL, WNOHANG);if(ret >0)printf("父进程: %d 已经成功回收了 %d 号进程\n",getpid(), ret);elsebreak;}printf("子进程回收成功\n");}// 启动服务器voidStartServer(){// 设置 SIGCHLD 信号的处理动作signal(SIGCHLD, handler);while(!quit_){// ...// 3.创建子进程
pid_t id =fork();if(id <0){// 创建子进程失败,暂时不与当前客户端建立通信会话close(sock);
std::cerr <<"Fork Fail!"<< std::endl;}elseif(id ==0){// 子进程内close(listensock_);// 子进程不需要监听(建议关闭)// 执行业务处理函数Service(sock, clientip, clientport);exit(0);// 子进程退出}}}
为什么不是很推荐这种方法?**因为这种方法实现起来比较麻烦,不如直接忽略
SIGCHLD
信号**
方式四:设置孙子进程(不是很推荐)
众所周知,父进程只需要对子进程负责,至于孙子进程交给子进程负责,如果某个子进程的父进程终止运行了,那么它就会变成 孤儿进程,父进程会变成
1
号进程,也就是由操作系统领养,回收进程的重担也交给了操作系统
可以利用该特性,在子进程内部再创建一个子进程(孙子进程),然后子进程退出,父进程可以直接回收(不必阻塞),子进程(孙子进程)的父进程变成
1
号进程
这种实现方法比较巧妙,而且与我们后面即将学到的 守护进程 有关
注意:使用这种方式时,父进程是需要等待子进程退出的
// 启动服务器voidStartServer(){while(!quit_){// ...// 3.创建子进程
pid_t id =fork();if(id <0){// 创建子进程失败,暂时不与当前客户端建立通信会话close(sock);
std::cerr <<"Fork Fail!"<< std::endl;}elseif(id ==0){// 子进程内close(listensock_);// 子进程不需要监听(建议关闭)// 再创建孙子进程if(fork()>0)exit(0);// 子进程退出// 执行业务处理函数Service(sock, clientip, clientport);exit(0);// 子进程退出}else{// 父进程需要等待子进程
pid_t ret =waitpid(id,nullptr,0);if(ret == id)
std::cout <<"Wait "<< id <<" success!";}}}
这种方法代码也很简单,但依旧不推荐,因为倘若连接请求变多,会导致孤儿进程变多,孤儿进程由操作系统接管,数量变多会给操作系统带来负担
以上就是设置 非阻塞 的四种方式,推荐使用方式二:**忽略
SIGCHLD
信号**
至此我们的 字符串回响程序 可以支持多客户端了
细节补充:**当子进程取走
sock
套接字进行网络通信后,父进程就不需要使用
sock
套接字了,可以将其进行关闭,下次连接时继续使用,避免文件描述符不断增长**
StartServer()
服务器启动函数 — 位于
server.hpp
服务器头文件中的
TcpServer
类
// 启动服务器voidStartServer(){// 忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN);while(!quit_){// 1.处理连接请求// ...// 2.如果连接失败,继续尝试连接// ...// 连接成功,获取客户端信息// ...// 3.创建子进程// ...close(sock);// 父进程不再需要资源(建议关闭)}}
这个补丁可以减少资源消耗,建议加上,前面是忘记加了,并且不太好修改,
server.hpp
服务器头文件完整代码如下
#pragmaonce#include<iostream>#include<string>#include<functional>#include<cerrno>#include<cstring>#include<unistd.h>#include<signal.h>// 信号处理相关头文件#include<sys/wait.h>// 进程等待时需要包含该头文件#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_server
{constuint16_t default_port =8888;// 默认端口号constint backlog =32;// 全连接队列的最大长度using func_t = std::function<std::string(std::string)>;// 回调函数类型classTcpServer{public:TcpServer(const func_t &func,constuint16_t port = default_port):func_(func),port_(port),quit_(false){}~TcpServer(){}// 初始化服务器voidInitServer(){// 1.创建监听套接字
listensock_ =socket(AF_INET, SOCK_STREAM,0);if(listensock_ ==-1){
std::cerr <<"Create ListenSocket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create ListenSocket Success! "<< listensock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr *)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.监听if(listen(listensock_, backlog)==-1){
std::cerr <<"Listen Fail!"<<strerror(errno)<< std::endl;exit(LISTEN_ERR);}
std::cout <<"Listen Success!"<< std::endl;}// 启动服务器voidStartServer(){// 忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN);while(!quit_){// 1.处理连接请求structsockaddr_in client;
socklen_t len =sizeof(client);int sock =accept(listensock_,(structsockaddr*)&client,&len);// 2.如果连接失败,继续尝试连接if(sock ==-1){
std::cerr <<"Accept Fail!"<<strerror(errno)<< std::endl;continue;}// 连接成功,获取客户端信息
std::string clientip =inet_ntoa(client.sin_addr);uint16_t clientport =ntohs(client.sin_port);
std::cout <<"Server accept "<< clientip +"-"<< clientport <<" "<< sock <<" from "<< listensock_ <<" success!"<< std::endl;// 3.创建子进程
pid_t id =fork();if(id <0){// 创建子进程失败,暂时不与当前客户端建立通信会话close(sock);
std::cerr <<"Fork Fail!"<< std::endl;}elseif(id ==0){// 子进程内close(listensock_);// 子进程不需要监听(建议关闭)// 执行业务处理函数Service(sock, clientip, clientport);exit(0);// 子进程退出}close(sock);// 父进程不再需要资源(必须关闭)}}// 业务处理voidService(int sock,const std::string& clientip,constuint16_t& clientport){char buff[1024];
std::string who = clientip +"-"+ std::to_string(clientport);while(true){
ssize_t n =read(sock, buff,sizeof(buff)-1);// 预留 '\0' 的位置if(n >0){// 读取成功
buff[n]='\0';
std::cout <<"Server get: "<< buff <<" from "<< who << std::endl;
std::string respond =func_(buff);// 实际业务处理由上层指定// 发送给服务器write(sock, buff,strlen(buff));}elseif(n ==0){// 表示当前读取到文件末尾了,结束读取
std::cout <<"Client "<< who <<" "<< sock <<" quit!"<< std::endl;close(sock);// 关闭文件描述符break;}else{// 读取出问题(暂时)
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock);// 关闭文件描述符break;}}}private:int listensock_;// 监听套接字uint16_t port_;// 端口号bool quit_;// 判断服务器是否结束运行
func_t func_;// 回调函数};}
3.多线程版服务器
3.1.核心功能
通过多线程,实现支持多客户端同时通信的服务器
核心功能:服务器与客户端成功连接后,创建一个线程,服务于客户端的业务处理
这里先通过 原生线程库 模拟实现
3.2.使用原生线程库
关于 原生线程库 中对于线程的操作可以看看这篇文章《Linux多线程【线程控制】》
线程的回调函数中需要
Service()
业务处理函数中的所有参数,同时也需要具备访问
Service()
业务处理函数的能力,单凭一个
void*
的参数是无法解决的,为此可以创建一个类,里面可以包含我们所需要的参数
ThreadData
类 — 位于
server.hpp
服务器头文件中
// 包含我们所需参数的类型classThreadData{public:ThreadData(int sock,const std::string& ip,constuint16_t& port, TcpServer* ptr):sock_(sock),clientip_(ip),clientport_(port),current_(ptr){}// 设置为公有是为了方便访问public:int sock_;
std::string clientip_;uint16_t clientport_;
TcpServer* current_;// 指向 TcpServer 对象的指针};
接下来就可以考虑如何借助多线程了
线程创建后,需要关闭不必要的
socket
套接字吗?
- 不需要,线程之间是可以共享这些资源的,无需关闭
如何设置主线程不必等待次线程退出?
- 可以把次线程进行分离
所以接下来我们需要在连接成功后,**创建次线程,利用已有信息构建
ThreadData
对象,为次线程编写回调函数(最终目的是为了执行
Service()
业务处理函数)**
注意:**因为当前在类中,线程的回调函数需要使用
static
设置为静态函数**
server.hpp
服务器头文件
#pragmaonce#include<iostream>#include<string>#include<functional>#include<cerrno>#include<cstring>#include<pthread.h>// 原生线程库#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"namespace nt_server
{constuint16_t default_port =8888;// 默认端口号constint backlog =32;// 全连接队列的最大长度using func_t = std::function<std::string(std::string)>;// 回调函数类型classTcpServer;// 前置声明// 包含我们所需参数的类型classThreadData{public:ThreadData(int sock,const std::string& ip,constuint16_t& port, TcpServer* ptr):sock_(sock),clientip_(ip),clientport_(port),current_(ptr){}// 设置为公有是为了方便访问public:int sock_;
std::string clientip_;uint16_t clientport_;
TcpServer* current_;// 指向 TcpServer 对象的指针};classTcpServer{public:TcpServer(const func_t &func,constuint16_t port = default_port):func_(func),port_(port),quit_(false){}~TcpServer(){}// 初始化服务器voidInitServer(){// 1.创建监听套接字
listensock_ =socket(AF_INET, SOCK_STREAM,0);if(listensock_ ==-1){
std::cerr <<"Create ListenSocket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create ListenSocket Success! "<< listensock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr *)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.监听if(listen(listensock_, backlog)==-1){
std::cerr <<"Listen Fail!"<<strerror(errno)<< std::endl;exit(LISTEN_ERR);}
std::cout <<"Listen Success!"<< std::endl;}// 启动服务器voidStartServer(){while(!quit_){// 1.处理连接请求structsockaddr_in client;
socklen_t len =sizeof(client);int sock =accept(listensock_,(structsockaddr*)&client,&len);// 2.如果连接失败,继续尝试连接if(sock ==-1){
std::cerr <<"Accept Fail!"<<strerror(errno)<< std::endl;continue;}// 连接成功,获取客户端信息
std::string clientip =inet_ntoa(client.sin_addr);uint16_t clientport =ntohs(client.sin_port);
std::cout <<"Server accept "<< clientip +"-"<< clientport <<" "<< sock <<" from "<< listensock_ <<" success!"<< std::endl;// 3.创建线程及所需要的线程信息类
ThreadData* td =newThreadData(sock, clientip, clientport,this);
pthread_t p;pthread_create(&p,nullptr, Routine, td);}}// 线程回调函数staticvoid*Routine(void* args){// 线程分离pthread_detach(pthread_self());
ThreadData* td =static_cast<ThreadData*>(args);// 调用业务处理函数
td->current_->Service(td->sock_, td->clientip_, td->clientport_);// 销毁对象delete td;}// 业务处理voidService(int sock,const std::string& clientip,constuint16_t& clientport){char buff[1024];
std::string who = clientip +"-"+ std::to_string(clientport);while(true){
ssize_t n =read(sock, buff,sizeof(buff)-1);// 预留 '\0' 的位置if(n >0){// 读取成功
buff[n]='\0';
std::cout <<"Server get: "<< buff <<" from "<< who << std::endl;
std::string respond =func_(buff);// 实际业务处理由上层指定// 发送给服务器write(sock, buff,strlen(buff));}elseif(n ==0){// 表示当前读取到文件末尾了,结束读取
std::cout <<"Client "<< who <<" "<< sock <<" quit!"<< std::endl;close(sock);// 关闭文件描述符break;}else{// 读取出问题(暂时)
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock);// 关闭文件描述符break;}}}private:int listensock_;// 监听套接字uint16_t port_;// 端口号bool quit_;// 判断服务器是否结束运行
func_t func_;// 回调函数};}
因为当前使用了 原生线程库,所以在编译时,需要加上
-lpthread
Makefile
文件
.PHONY:all
all:server client
server:server.cc
g++ -o$@ $^ -std=c++11 -lpthread
client:client.cc
g++ -o$@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm-rf server client
接下来就是编译并运行程序,可以看到 当前只有一个进程,同时有五个线程在运行
使用 原生线程库 过于单薄了,并且这种方式存在问题:连接都准备好了,才创建线程,如果创建线程所需要的资源较多,会拖慢服务器整体连接效率
为此可以改用之前实现的 线程池
3.3.使用线程池
之前在 《Linux多线程【线程池】》一文中实现了多个版本的线程池,这里我们直接使用最终版,也就是 单例模式版线程池
部分组件不需要修改,代码如下:
ThreadPool.hpp
线程池头文件
#pragmaonce#include<vector>#include<string>#include<memory>#include<functional>#include<unistd.h>#include<pthread.h>#include"Task.hpp"#include"Thread.hpp"#include"BlockingQueue.hpp"// CP模型namespace Yohifo
{#defineTHREAD_NUM10template<classT>classThreadPool{private:ThreadPool(int num = THREAD_NUM):_num(num){}~ThreadPool(){// 等待线程退出for(auto&t : _threads)
t.join();}// 删除拷贝构造ThreadPool(const ThreadPool<T>&)=delete;public:static ThreadPool<T>*getInstance(){// 双检查if(_inst ==nullptr){// 加锁
LockGuard lock(&_mtx);if(_inst ==nullptr){// 创建对象
_inst =newThreadPool<T>();// 初始化及启动服务
_inst->init();
_inst->start();}}return _inst;}public:voidinit(){// 创建一批线程for(int i =0; i < _num; i++)
_threads.push_back(Thread(i, threadRoutine,this));}voidstart(){// 启动线程for(auto&t : _threads)
t.run();}// 提供给线程的回调函数(已修改返回类型为 void)staticvoidthreadRoutine(void*args){// 避免等待线程,直接剥离pthread_detach(pthread_self());auto ptr =static_cast<ThreadPool<T>*>(args);while(true){// 从CP模型中获取任务
T task = ptr->popTask();task();// 回调函数}}// 装载任务voidpushTask(const T& task){
_blockqueue.Push(task);}protected:
T popTask(){
T task;
_blockqueue.Pop(&task);return task;}private:
std::vector<Thread> _threads;int _num;// 线程数量
BlockQueue<T> _blockqueue;// 阻塞队列// 创建静态单例对象指针及互斥锁static ThreadPool<T>*_inst;static pthread_mutex_t _mtx;};// 初始化指针template<classT>
ThreadPool<T>* ThreadPool<T>::_inst =nullptr;// 初始化互斥锁template<classT>
pthread_mutex_t ThreadPool<T>::_mtx = PTHREAD_MUTEX_INITIALIZER;}
Thread.hpp
封装实现的线程库头文件
#pragmaonce#include<iostream>#include<string>#include<pthread.h>enumclassStatus{
NEW =0,// 新建
RUNNING,// 运行中
EXIT // 已退出};// 参数、返回值为 void 的函数类型typedefvoid(*func_t)(void*);classThread{public:Thread(int num =0, func_t func =nullptr,void* args =nullptr):_tid(0),_status(Status::NEW),_func(func),_args(args){// 根据编号写入名字char name[128];snprintf(name,sizeof name,"thread-%d", num);
_name = name;}~Thread(){}// 获取 ID
pthread_t getTID()const{return _tid;}// 获取线程名
std::string getName()const{return _name;}// 获取状态
Status getStatus()const{return _status;}// 回调方法staticvoid*runHelper(void* args){
Thread* myThis =static_cast<Thread*>(args);// 很简单,回调用户传进来的 func 函数即可
myThis->_func(myThis->_args);}// 启动线程voidrun(){int ret =pthread_create(&_tid,nullptr, runHelper,this);if(ret !=0){
std::cerr <<"create thread fail!"<< std::endl;exit(1);// 创建线程失败,直接退出}
_status = Status::RUNNING;// 更改状态为 运行中}// 线程等待voidjoin(){int ret =pthread_join(_tid,nullptr);if(ret !=0){
std::cerr <<"thread join fail!"<< std::endl;exit(1);// 等待失败,直接退出}
_status = Status::EXIT;// 更改状态为 退出}private:
pthread_t _tid;// 线程 ID
std::string _name;// 线程名
Status _status;// 线程状态
func_t _func;// 线程回调函数void* _args;// 传递给回调函数的参数};
BlockingQueue.hpp
生产者消费者模型头文件
#pragmaonce#include<queue>#include<mutex>#include<pthread.h>#include"LockGuard.hpp"// 命名空间,避免冲突namespace Yohifo
{#defineDEF_SIZE10template<classT>classBlockQueue{public:BlockQueue(size_t cap = DEF_SIZE):_cap(cap){// 初始化锁与条件变量pthread_mutex_init(&_mtx,nullptr);pthread_cond_init(&_pro_cond,nullptr);pthread_cond_init(&_con_cond,nullptr);}~BlockQueue(){// 销毁锁与条件变量pthread_mutex_destroy(&_mtx);pthread_cond_destroy(&_pro_cond);pthread_cond_destroy(&_con_cond);}// 生产数据(入队)voidPush(const T& inData){// 加锁(RAII风格)
LockGuard lock(&_mtx);// 循环判断条件是否满足while(IsFull()){pthread_cond_wait(&_pro_cond,&_mtx);}
_queue.push(inData);// 可以加策略唤醒,比如生产一半才唤醒消费者pthread_cond_signal(&_con_cond);// 自动解锁}// 消费数据(出队)voidPop(T* outData){// 加锁(RAII 风格)
LockGuard lock(&_mtx);// 循环判读条件是否满足while(IsEmpty()){pthread_cond_wait(&_con_cond,&_mtx);}*outData = _queue.front();
_queue.pop();// 可以加策略唤醒,比如消费完后才唤醒生产者pthread_cond_signal(&_pro_cond);// 自动解锁}private:// 判断是否为满boolIsFull(){return _queue.size()== _cap;}// 判断是否为空boolIsEmpty(){return _queue.empty();}private:
std::queue<T> _queue;
size_t _cap;// 阻塞队列的容量
pthread_mutex_t _mtx;// 互斥锁
pthread_cond_t _pro_cond;// 生产者条件变量
pthread_cond_t _con_cond;// 消费者条件变量};}
LockGuard.hpp
自动化锁头文件
#pragmaonce#include<pthread.h>classLockGuard{public:LockGuard(pthread_mutex_t*pmtx):_pmtx(pmtx){// 加锁pthread_mutex_lock(_pmtx);}~LockGuard(){// 解锁pthread_mutex_unlock(_pmtx);}private:
pthread_mutex_t* _pmtx;};
现在需要修改
Task.hpp
任务头文件中的
Task
任务类,将其修改为一个服务于 网络通信中业务处理 的任务类(也就是
Service()
业务处理函数)
在
Service()
业务处理函数中,需要包含 **
socket
套接字、客户端
IP
、客户端端口号** 等必备信息,除此之外,我们还可以将 **可调用对象(
Service()
业务处理函数)** 作为参数传递给
Task
对象
Task.hpp
任务类
#pragmaonce#include<string>#include<functional>namespace Yohifo
{// Service() 业务处理函数的类型using cb_t = std::function<void(int, std::string,uint16_t)>;classTask{public:// 可以再提供一个默认构造(防止部分场景中构建对象失败)Task(){}Task(int sock,const std::string& ip,constuint16_t& port,const cb_t& cb):sock_(sock),ip_(ip),port_(port),cb_(cb){}// 重载运算操作,用于回调 [业务处理函数]voidoperator()(){// 直接回调 cb [业务处理函数] 即可cb_(sock_, ip_, port_);}private:int sock_;
std::string ip_;uint16_t port_;
cb_t cb_;// 回调函数};}
准备工作完成后,接下来就是往
server.hpp
服务器头文件中添加组件了
注意:
- 在构建
Task
对象时,需要使用bind
绑定类内函数,避免参数不匹配 - 当前的线程池是单例模式,在
Task
任务对象构建后,通过线程池操作句柄push
对象即可
其实也就是在
StartServer.hpp
中增加了这两句代码
// 3.构建任务对象 注意:使用 bind 绑定 this 指针
Yohifo::Task t(sock, clientip, clientport, std::bind(&TcpServer::Service,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// 4.通过线程池操作句柄,将任务对象 push 进线程池中处理
Yohifo::ThreadPool<Yohifo::Task>::getInstance()->pushTask(t);
完整的服务器代码如下
server.hpp
服务器头文件
#pragmaonce#include<iostream>#include<string>#include<functional>#include<cerrno>#include<cstring>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"#include"ThreadPool.hpp"// 线程池#include"Task.hpp"// 任务类namespace nt_server
{constuint16_t default_port =8888;// 默认端口号constint backlog =32;// 全连接队列的最大长度using func_t = std::function<std::string(std::string)>;// 回调函数类型classTcpServer;// 前置声明// 包含我们所需参数的类型classThreadData{public:ThreadData(int sock,const std::string& ip,constuint16_t& port, TcpServer* ptr):sock_(sock),clientip_(ip),clientport_(port),current_(ptr){}// 设置为公有是为了方便访问public:int sock_;
std::string clientip_;uint16_t clientport_;
TcpServer* current_;// 指向 TcpServer 对象的指针};classTcpServer{public:TcpServer(const func_t &func,constuint16_t port = default_port):func_(func),port_(port),quit_(false){}~TcpServer(){}// 初始化服务器voidInitServer(){// 1.创建监听套接字
listensock_ =socket(AF_INET, SOCK_STREAM,0);if(listensock_ ==-1){
std::cerr <<"Create ListenSocket Fail!"<<strerror(errno)<< std::endl;exit(SOCKET_ERR);}
std::cout <<"Create ListenSocket Success! "<< listensock_ << std::endl;// 2.绑定IP地址与端口号structsockaddr_in local;memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;// 绑定任意可用IP地址
local.sin_port =htons(port_);if(bind(listensock_,(const sockaddr *)&local,sizeof(local))){
std::cout <<"Bind IP&&Port Fail: "<<strerror(errno)<< std::endl;exit(BIND_ERR);}// 3.监听if(listen(listensock_, backlog)==-1){
std::cerr <<"Listen Fail!"<<strerror(errno)<< std::endl;exit(LISTEN_ERR);}
std::cout <<"Listen Success!"<< std::endl;}// 启动服务器voidStartServer(){while(!quit_){// 1.处理连接请求structsockaddr_in client;
socklen_t len =sizeof(client);int sock =accept(listensock_,(structsockaddr*)&client,&len);// 2.如果连接失败,继续尝试连接if(sock ==-1){
std::cerr <<"Accept Fail!"<<strerror(errno)<< std::endl;continue;}// 连接成功,获取客户端信息
std::string clientip =inet_ntoa(client.sin_addr);uint16_t clientport =ntohs(client.sin_port);
std::cout <<"Server accept "<< clientip +"-"<< clientport <<" "<< sock <<" from "<< listensock_ <<" success!"<< std::endl;// 3.构建任务对象 注意:使用 bind 绑定 this 指针
Yohifo::Task t(sock, clientip, clientport, std::bind(&TcpServer::Service,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// 4.通过线程池操作句柄,将任务对象 push 进线程池中处理
Yohifo::ThreadPool<Yohifo::Task>::getInstance()->pushTask(t);}}// 业务处理voidService(int sock,const std::string& clientip,constuint16_t& clientport){char buff[1024];
std::string who = clientip +"-"+ std::to_string(clientport);while(true){
ssize_t n =read(sock, buff,sizeof(buff)-1);// 预留 '\0' 的位置if(n >0){// 读取成功
buff[n]='\0';
std::cout <<"Server get: "<< buff <<" from "<< who << std::endl;
std::string respond =func_(buff);// 实际业务处理由上层指定// 发送给服务器write(sock, buff,strlen(buff));}elseif(n ==0){// 表示当前读取到文件末尾了,结束读取
std::cout <<"Client "<< who <<" "<< sock <<" quit!"<< std::endl;close(sock);// 关闭文件描述符break;}else{// 读取出问题(暂时)
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock);// 关闭文件描述符break;}}}private:int listensock_;// 监听套接字uint16_t port_;// 端口号bool quit_;// 判断服务器是否结束运行
func_t func_;// 回调函数};}
接下来编译并运行程序,当服务器启动后(此时无客户端连接),只有一个线程,这是因为我们当前的 线程池 是基于 懒汉模式 实现的,只有当第一次使用时,才会创建线程
接下来启动客户端,可以看到确实创建了一批次线程(十个)
当然可以支持多客户端同时通信
看似程序已经很完善了,其实隐含着一个大问题:**当前线程池中的线程,本质上是在回调一个
while(true)
死循环函数,当连接的客户端大于线程池中的最大线程数时,会导致所有线程始终处于满负载状态,直接影响就是连接成功后,无法再创建通信会话(倘若客户端不断开连接,线程池中的线程就无力处理其他客户端的会话)**
说白了就是 线程池 比较适合用于处理短任务,对于当前的场景来说,线程池 不适合建立持久通信会话,应该将其用于处理 **
read
读取、
write
写入** 任务
如果想解决这个问题,有两个方向:**
Service()
函数中支持一次 [收 / 发],或者多线程+线程池,多线程用于构建通信会话,线程池则用于处理 [收 / 发] 任务**
前者实现起来比较简单,无非就是把
Service()
业务处理函数中的
while(true)
循环去掉
Service()
业务处理函数
// 业务处理voidService(int sock,const std::string &clientip,constuint16_t&clientport){char buff[1024];
std::string who = clientip +"-"+ std::to_string(clientport);
ssize_t n =read(sock, buff,sizeof(buff)-1);// 预留 '\0' 的位置if(n >0){// 读取成功
buff[n]='\0';
std::cout <<"Server get: "<< buff <<" from "<< who << std::endl;
std::string respond =func_(buff);// 实际业务处理由上层指定// 发送给服务器write(sock, buff,strlen(buff));}elseif(n ==0){// 表示当前读取到文件末尾了,结束读取
std::cout <<"Client "<< who <<" "<< sock <<" quit!"<< std::endl;close(sock);// 关闭文件描述符}else{// 读取出问题(暂时)
std::cerr <<"Read Fail!"<<strerror(errno)<< std::endl;close(sock);// 关闭文件描述符}}
至于后者就比较麻烦了,需要结合 高级IO 相关知识,这里不再阐述
4.日志输出
4.1.日志的重要性
在之前的编程经历中,如果我们的程序运行出现了问题,都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上,
debug
阶段这样使用没啥问题,但如果出错的是一个不断在运行中的服务,那问题就大了,因为服务器是不间断运行中,直接将 错误信息 输出到屏幕上,会导致错误排查变得极为困难
将各种 错误信息 组织管理,就形成了日志,日志有属于自己的格式(包括时间、文件名及行号、错误等级等),利于排查问题
所以接下来我们将会实现一个简易版日志器,用于定向输出我们的日志信息
4.2.可变参数
日志需要我们指定格式并输出,依赖于可变参数
在编写简易版日志器之前,需要先认识一下 C语言 中有关可变参数的使用,主要包括这几个 宏
#include<stdarg.h>
va_list // 指向可变参数列表的指针va_start()// 将指针指向起始地址va_arg()// 根据类型,提取可变参数列表中的参数va_end()// 将指针置为空
关于 可变参数 更多知识详见 《【C语言】可变参数列表》
比如我们可以通过 可变参数 实现参数遍历
#include<stdio.h>#include<stdarg.h>voidforeach(int format,...){
va_list p;va_start(p, format);// 接下来就是获取其中的每一个参数for(int i =0; i < format; i++)printf("%d ",va_arg(p,int));printf("\n");// 置空va_end(p);}intmain(){foreach(5,1,2,3,4,5);return0;}
这种依靠自己动手的方法比较麻烦,我们也可以借助标准库提供的
vsnprintf()
函数进行参数解析
4.3.日志器实现
日志是有等级的,一般分为五级:
Debug
用于调试Info
提示信息Warning
警告Errorr
错误Fatal
致命错误
错误等级越高,代表影响越大
当然难免有不明确的错误,可以再添加一级:**
UnKnow
未知错误**
// 日志等级enum{
Debug =0,
Info,
Warning,
Error,
Fatal
};
string getLevel(int level){
vector<string> vs ={"<Debug>","<Info>","<Warning>","<Error>","<Fatal>","<Unknown>"};//避免非法情况if(level <0|| level >= vs.size()-1)return vs[vs.size()-1];return vs[level];}
接下来是获取时间信息,可以通过
time()
函数获取当前时间戳,然后再利用
localtime()
函数构建
struct tm
结构体对象,这个对象会将时间戳解析成 年月日 时分秒 等详细信息,直接获取即可
strcut tm
结构体的信息如下,细节:**年份已经
-1900
了,使用时需要加上
1900
;月份从
0
开始,使用时需要
+1
**
/* Used by other time functions. */structtm{int tm_sec;/* Seconds. [0-60] (1 leap second) */int tm_min;/* Minutes. [0-59] */int tm_hour;/* Hours. [0-23] */int tm_mday;/* Day. [1-31] */int tm_mon;/* Month. [0-11] */int tm_year;/* Year - 1900. */int tm_wday;/* Day of week. [0-6] */int tm_yday;/* Days in year.[0-365] */int tm_isdst;/* DST. [-1/0/1]*/#ifdef__USE_BSDlongint tm_gmtoff;/* Seconds east of UTC. */constchar*tm_zone;/* Timezone abbreviation. */#elselongint __tm_gmtoff;/* Seconds east of UTC. */constchar*__tm_zone;/* Timezone abbreviation. */#endif};
可以这样获取当前时间
// 获取当前时间
string getTime(){
time_t t =time(nullptr);//获取时间戳structtm*st =localtime(&t);//获取时间相关的结构体char buff[128];snprintf(buff,sizeof(buff),"%d-%d-%d %d:%d:%d", st->tm_year +1900, st->tm_mon +1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);return buff;}
接下来就是获取进程
PID
,这个简单,直接使用
getpid()
函数获取即可,最后是解析参数,需要用到
vsnprintf()
函数,只要传入缓冲区和
va_list
指针,该函数就可以自动解析出参数,并存入缓冲区中
voidlogMessage(int level,constchar* format,...){//截获主体消息char msgbuff[1024];
va_list p;va_start(p, format);//将 p 定位至 format 的起始位置vsnprintf(msgbuff,sizeof(msgbuff), format, p);//自动根据格式进行读取va_end(p);}
接下来就是将 **日志等级 时间
PID
** 与 参数 进行拼接,形成日志
log.hpp
日志头文件
#pragmaonce#include<iostream>#include<string>#include<vector>#include<cstdio>#include<time.h>#include<sys/types.h>#include<unistd.h>#include<stdarg.h>usingnamespace std;enum{
Debug =0,
Info,
Warning,
Error,
Fatal
};
string getLevel(int level){
vector<string> vs ={"<Debug>","<Info>","<Warning>","<Error>","<Fatal>","<Unknown>"};//避免非法情况if(level <0|| level >= vs.size()-1)return vs[vs.size()-1];return vs[level];}
string getTime(){
time_t t =time(nullptr);//获取时间戳structtm*st =localtime(&t);//获取时间相关的结构体char buff[128];snprintf(buff,sizeof(buff),"%d-%d-%d %d:%d:%d", st->tm_year +1900, st->tm_mon +1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);return buff;}//处理信息voidlogMessage(int level,constchar* format,...){//日志格式:<日志等级> [时间] [PID] {消息体}
string logmsg =getLevel(level);//获取日志等级
logmsg +=" "+getTime();//获取时间
logmsg +=" ["+to_string(getpid())+"]";//获取进程PID//截获主体消息char msgbuff[1024];
va_list p;va_start(p, format);//将 p 定位至 format 的起始位置vsnprintf(msgbuff,sizeof(msgbuff), format, p);//自动根据格式进行读取va_end(p);
logmsg +=" {"+string(msgbuff)+"}";//获取主体消息printf("%s\n", logmsg);}
为什么日志消息最后还是向屏幕输出?这样组织日志消息的好处是什么?
因为现在还在测试阶段,等测试完成后,可以将日志消息存入文件中,做到持久化存储;至于统一组织的好处不言而喻,能够确保每条日志消息都包含必要信息,便于排查错误
简单测试的效果如下
4.4.应用于程序中
接下来可以包含
log.hpp
这个日志器头文件,并进行日志输出了,比如先将
client.hpp
客户端头文件中的错误信息日志化(代码少一些,比较好改)
client.hpp
客户端头文件
#pragmaonce#include<iostream>#include<string>#include<cstring>#include<cerrno>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"err.hpp"#include"log.hpp"namespace nt_client
{classTcpClient{public:TcpClient(const std::string& ip,constuint16_t port):server_ip_(ip),server_port_(port){}~TcpClient(){}// 初始化客户端voidInitClient(){// 创建套接字
sock_ =socket(AF_INET, SOCK_STREAM,0);if(sock_ ==-1){logMessage(Fatal,"Create Socket Fail! %s",strerror(errno));exit(SOCKET_ERR);}logMessage(Debug,"Create Sock Succeess! %d", sock_);}// 启动客户端voidStartClient(){// 填充服务器的 sockaddr_in 结构体信息structsockaddr_in server;
socklen_t len =sizeof(server);memset(&server,0, len);
server.sin_family = AF_INET;inet_aton(server_ip_.c_str(),&server.sin_addr);// 将点分十进制转化为二进制IP地址的另一种方法
server.sin_port =htons(server_port_);// 尝试重连 5 次int n =5;while(n){int ret =connect(sock_,(conststructsockaddr*)&server, len);if(ret ==0){// 连接成功,可以跳出循环break;}// 尝试进行重连logMessage(Warning,"网络异常,正在进行重连... 剩余连接次数: %d",--n);sleep(1);}// 如果剩余重连次数为 0,证明连接失败if(n ==0){logMessage(Fatal,"连接失败! %s",strerror(errno));close(sock_);exit(CONNECT_ERR);}// 连接成功logMessage(Info,"连接成功!");// 进行业务处理Service();}// 业务处理voidService(){char buff[1024];
std::string who = server_ip_ +"-"+ std::to_string(server_port_);while(true){// 由用户输入信息
std::string msg;
std::cout <<"Please Enter >> ";
std::getline(std::cin, msg);// 发送信息给服务器write(sock_, msg.c_str(), msg.size());// 接收来自服务器的信息
ssize_t n =read(sock_, buff,sizeof(buff)-1);if(n >0){// 正常通信
buff[n]='\0';
std::cout <<"Client get: "<< buff <<" from "<< who << std::endl;}elseif(n ==0){// 读取到文件末尾(服务器关闭了)logMessage(Error,"Server %s quit! %s", who.c_str(),strerror(errno));close(sock_);// 关闭文件描述符break;}else{// 读取异常logMessage(Error,"Read Fail! %s",strerror(errno));close(sock_);// 关闭文件描述符break;}}}private:int sock_;// 套接字
std::string server_ip_;// 服务器IPuint16_t server_port_;// 服务器端口号};}
效果就是这个样子,至于代码中其他输出错误的地方,都可以采用 简易版日志器 进行统一输出
改造完成的程序长这个样子
4.5.持久化存储
所谓持久化存储就是将日志消息输出至文件中,修改
log.hpp
中的代码即可
- 指定日志文件存放路径
- 打开文件,将日志消息追加至文件中
注意:当前的改动中并未涉及目录创建,所以需要手动创建相关目录
log.hpp
日志头文件
#pragmaonce#include<iostream>#include<string>#include<vector>#include<cstdio>#include<time.h>#include<sys/types.h>#include<unistd.h>#include<stdarg.h>usingnamespace std;enum{
Debug =0,
Info,
Warning,
Error,
Fatal
};staticconst string file_name ="log/TcpServer.log";
string getLevel(int level){
vector<string> vs ={"<Debug>","<Info>","<Warning>","<Error>","<Fatal>","<Unknown>"};//避免非法情况if(level <0|| level >= vs.size()-1)return vs[vs.size()-1];return vs[level];}
string getTime(){
time_t t =time(nullptr);//获取时间戳structtm*st =localtime(&t);//获取时间相关的结构体char buff[128];snprintf(buff,sizeof(buff),"%d-%d-%d %d:%d:%d", st->tm_year +1900, st->tm_mon +1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);return buff;}//处理信息voidlogMessage(int level,constchar* format,...){//日志格式:<日志等级> [时间] [PID] {消息体}
string logmsg =getLevel(level);//获取日志等级
logmsg +=" "+getTime();//获取时间
logmsg +=" ["+to_string(getpid())+"]";//获取进程PID//截获主体消息char msgbuff[1024];
va_list p;va_start(p, format);//将 p 定位至 format 的起始位置vsnprintf(msgbuff,sizeof(msgbuff), format, p);//自动根据格式进行读取va_end(p);
logmsg +=" {"+string(msgbuff)+"}";//获取主体消息//持久化。写入文件中
FILE* fp =fopen(file_name.c_str(),"a");//以追加的方式写入if(fp ==nullptr)return;//不太可能出错fprintf(fp,"%s\n", logmsg.c_str());fflush(fp);//手动刷新一下fclose(fp);
fp =nullptr;}
5.守护进程
5.1.会话、进程组、进程
接下来进入本文中的最后一个小节: 守护进程
守护进程 的意思就是让进程不间断的在后台运行,即便是
bash
关闭了,也能照旧运行。守护进程 就是现实生活中的服务器,因为服务器是需要
24H
不间断运行的
当前我们的程序在启动后属于 前台进程,前台进程 是由
bash
进程替换而来的,因此会导致
bash
暂时无法使用
如果在启动程序时,带上
&
符号,程序就会变成 后台进程,后台进程 并不会与
bash
进程冲突,
bash
仍然可以使用
后台进程 也可以实现服务器不间断运行,但问题在于 **如果当前
bash
关闭了,那么运行中的后台进程也会被关闭**,最好的解决方案是使用 守护进程
在正式学习 守护进程 之前,需要先了解一组概念:会话、进程组、进程
分别运行一批 前台、后台进程,并通过指令查看进程运行情况
sleep1000|sleep2000|sleep3000&sleep100|sleep200|sleep300ps-ajx|head-1&&ps-ajx|grepsleep|grep-vgrep
其中 **会话 <->
SID
、进程组 <->
PGID
、进程 <->
PID
**,显然,
sleep 1000、2000、3000
处于同一个管道中(有血缘关系),属于同一个 进程组,所以他们的
PGID
都是一样的,都是
4261
;至于
sleep 100、200、300
属于另一个 进程组,
PGID
为
4308
;再仔细观察可以发现 **每一组的进程组
PGID
都与当前组中第一个被创建的进程
PID
一致,这个进程被称为 组长进程**
会话 >= 进程组 >= 进程
无论是 后台进程 还是 前台进程,都是从同一个
bash
中启动的,所以它们处于同一个 会话 中,
SID
都是
1939
,并且关联的 终端文件
TTY
都是
pts/1
Linux
中一切皆文件,终端文件也是如此,这里的终端其实就是当前
bash
输出结果时使用的文件(也就是屏幕),终端文件位于
dev/pts
目录下,如果向指定终端文件中写入数据,那么对方也可以直接收到
(关联终端文件说白了就是打开了文件,一方写,一方读,不就是管道吗)
根据当前的 **会话
SID
** 查找目标进程,发现这玩意就是
bash
进程,
bash
进程本质上就是一个不断运行中的 前台进程,并且自成 进程组
在同一个
bash
中启动前台、后台进程,它们的
SID
都是一样的,属于同一个 会话,关联了同一个 终端 (
SID
其实就是
bash
的
PID
)
我们使用
XShell
等工具登录
Linux
服务器时,会在服务器中创建一个 会话(
bash
),可以在该会话内创建 进程,当 进程 间有关系时,构成一个 进程组,组长 进程的
PID
就是该 进程组 的
PGID
Linux
中的登录操作实际上就是创建了一个会话,
Windows
中也是如此,当你的
Windows
变卡时,可以使用 [注销] 按钮结束整个会话,重新登录,电脑就会流畅如初
**在同一个会话中,只允许一个前台进程在运行,默认是
bash
,如果其他进程运行了,
bash
就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)**
如何将一个 后台进程 变成 前台进程?
首先通过指令查看当前 会话 中正在运行的 后台进程,获取 任务号
jobs
接下来通过 任务号 将 后台进程 变成 前台进程,此时
bash
就无法使用了
fg1
那如何将 前台进程 变成 后台进程 ?
首先是通过
ctrl + z
发送
19
号
SIGSTOP
信号,暂停正在运行中的 前台进程
键盘输入 ctrl + z
然后通过 任务号,可以把暂停中的进程变成 后台进程
bg1
5.2.守护进程化
一般网络服务器为了不受到用户登录重启的影响,会以 守护进程 的形式运行,有了上面那一批前置知识后,就可以很好的理解 守护进程 的本质了
守护进程:进程单独成一个会话,并且以后台进程的形式运行
说白了就是让服务器不间断运行,可以直接使用
daemon()
函数完成 守护进程化
#include<unistd.h>intdaemon(int nochdir,int noclose);
参数解读:
nochdir
改变进程的工作路径noclose
重定向标准输入、标准输出、标准错误
返回值:**成功返回
0
,失败返回
-1
**
一般情况下,
daemon()
函数的两个参数都只需要传递
0
,**默认工作在
/
路径下,默认重定向至
/dev/null
**
/dev/null
就像是一个 黑洞,可以把所有数据都丢入其中,相当于丢弃数据
使用
damon()
函数使之前的
server.cc
守护进程化
server.cc
服务器源文件
#include<memory>// 智能指针头文件#include<string>#include<unistd.h>#include"server.hpp"usingnamespace std;usingnamespace nt_server;// 业务处理回调函数(字符串回响)
string echo(string request){return request;}intmain(){// 直接守护进程化daemon(0,0);
unique_ptr<TcpServer>usvr(newTcpServer(echo));// 将回调函数进行传递
usvr->InitServer();
usvr->StartServer();return0;}
现在服务器启动后,会自动变成 后台进程,并且自成一个 新会话,归操作系统管(守护进程 本质上是一种比较坚强的 孤儿进程)
注意:**现在标准输出、标准错误都被重定向至
/dev/null
中了,之前向屏幕输出的数据,现在都会直接被丢弃,如果想保存数据,可以选择使用日志**
如果想终止 守护进程,需要通过
kill pid
杀死目标进程
使用系统提供的接口一键 守护进程化 固然方便,不过大多数程序员都会选择手动 守护进程化(可以根据自己的需求定制操作)
原理是 **使用
setsid()
函数新设一个会话,谁调用,会话
SID
就是谁的,成为一个新的会话后,不会被之前的会话影响**
#include<unistd.h>
pid_t setsid(void);
返回值:**成功返回该进程的
pid
,失败返回
-1
**
注意:调用该函数的进程,不能是组长进程,需要创建子进程后调用
手动实现守护进程时需要注意以下几点:
- 忽略异常信号
0、1、2
要做特殊处理(文件描述符)- 进程的工作路径可能要改变(从用户目录中脱离至根目录)
具体实现步骤如下:
1、忽略常见的异常信号:
SIGPIPE
、
SIGCHLD
2、如何保证自己不是组长? 创建子进程 ,成功后父进程退出,子进程变成守护进程
3、新建会话,自己成为会话的 话首进程
4、(可选)更改守护进程的工作路径:
chdir
5、处理后续对于
0、1、2
的问题
对于 标准输入、标准输出、标准错误 的处理方式有两种
暴力处理:**直接关闭
fd
**
优雅处理:**将
fd
重定向至
/dev/null
,也就是
daemon()
函数的做法**
这里我们选择后者,守护进程 的函数实现如下
Daemon.hpp
守护进程头文件
#pragmaonce#include<iostream>#include<cstring>#include<cerrno>#include<signal.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include"err.hpp"#include"log.hpp"staticconstchar*path ="/home/Yohifo";voidDaemon(){// 1、忽略常见信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2、创建子进程,自己退休
pid_t id =fork();if(id >0)exit(0);elseif(id <0){// 子进程创建失败logMessage(Error,"Fork Fail: %s",strerror(errno));exit(FORK_ERR);}// 3、新建会话,使自己成为一个单独的组
pid_t ret =setsid();if(ret ==-1){// 守护化失败logMessage(Error,"Setsid Fail: %s",strerror(errno));exit(SETSID_ERR);}// 4、更改工作路径int n =chdir(path);if(n ==-1){// 更改路径失败logMessage(Error,"Chdir Fail: %s",strerror(errno));exit(CHDIR_ERR);}// 5、重定向标准输入输出错误int fd =open("/dev/null", O_RDWR);if(fd ==-1){// 文件打开失败logMessage(Error,"Open Fail: %s",strerror(errno));exit(OPEN_ERR);}// 重定向标准输入、标准输出、标准错误dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd);}
当然相应的错误码也需要更新
err.hpp
错误码头文件
#pragmaonceenum{
USAGE_ERR =1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR,
SETSID_ERR,
CHDIR_ERR,
OPEN_ERR
};
接下来就是在服务启动成功后,将其 守护进程化
StartServer()
服务器启动函数 — 位于
server.hpp
服务器头文件中的
TcpServer
类
#include"myDaemon.hpp"// 启动服务器voidStartServer(){// 守护进程化Daemon();// ...}
现在服务器在启动后,会自动新建会话,以 守护进程 的形式运行
关于
inet_ntoa
函数的返回值(该函数的作用是将四字节的
IP
地址转化为点分十进制的
IP
地址)
**inet_ntoa
返回值为
char*
,转化后的
IP
地址存储在静态区,二次调用会覆盖上一次的结果,多线程场景中不是线程安全的**
- 不过在
CentOS 7
及更高版本中,接口进行了更新,新增了互斥锁,多线程场景中测试没问题
6.完整代码
下面是不同版本服务器的完整代码
「朴素版,支持单客户端连接」
「多进程版,支持多客户端连接」
「多线程版(原生线程库),支持多客户端连接」
「多线程版(线程池),支持多客户端连接」
「日志版,支持简易日志输出」
「守护进程版,支持服务部署」
🌨️总结
以上是关于『简易TCP网络程序』的全部内容,作为上一篇博客的延伸,本文重新实现了字符串回响网络程序,基于TCP协议逐步改造并引入多进程、多线程、线程池、日志输出、守护进程等技术。这使得网络程序更为成熟,为后续网络和高级IO的学习提供了有力支持。同时,对套接字编程的重要性也得到了充分体现。希望本文能为读者在网络编程领域的深入学习提供实质性帮助。
相关文章推荐
网络编程『socket套接字 ‖ 简易UDP网络程序』
网络基础『发展 ‖ 协议 ‖ 传输 ‖ 地址』
版权归原作者 北 海 所有, 如有侵权,请联系我们删除。