企业网站管理系统推荐,wordpress仪表盘空白,有限公司网站建设 互成网络地址 四川,移动互联网应用程序个人信息保护管理暂行规定文章目录 一.简单的TCP网络程序1.服务端创建套接字2.服务端绑定3.服务端监听4.服务端获取连接5.服务端处理请求6.客户端创建套接字7.客户端连接服务器8.客户端发起请求9.服务器测试10.单执行流服务器的弊端 二.多进程版的TCP网络程序1.捕捉SIGCHLD信号2.让孙子进程提供服务 三.… 文章目录 一.简单的TCP网络程序1.服务端创建套接字2.服务端绑定3.服务端监听4.服务端获取连接5.服务端处理请求6.客户端创建套接字7.客户端连接服务器8.客户端发起请求9.服务器测试10.单执行流服务器的弊端 二.多进程版的TCP网络程序1.捕捉SIGCHLD信号2.让孙子进程提供服务 三.多线程版的TCP网络程序四.线程池版的TCP网络程序五.守护进程1.进程知识补充2.守护进程 一.简单的TCP网络程序
1.服务端创建套接字 我们将TCP服务器封装成一个类当我们定义出一个服务器对象后需要马上对服务器进行初始化而初始化TCP服务器要做的第一件事就是创建套接字。 TCP服务器在调用socket函数创建套接字时参数设置如下
协议家族选择AF_INET因为我们要进行的是网络通信。创建套接字时所需的服务类型应该是SOCK_STREAM因为我们编写的是TCP服务器SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。协议类型默认设置为0即可。
如果创建套接字后获得的文件描述符是小于0的说明套接字创建失败此时也就没必要进行后续操作了直接终止程序即可。
class TcpServer
{
public:void InitServer(){//创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}}~TcpServer(){if (_sock 0){close(_sock);}}
private:int _sock; //套接字
};说明一下
实际TCP服务器创建套接字的做法与UDP服务器是一样的只不过创建套接字时TCP需要的是流式服务而UDP需要的是用户数据报服务。当析构服务器时可以将服务器对应的文件描述符进行关闭。
2.服务端绑定 套接字创建完毕后我们实际只是在系统层面上打开了一个文件该文件还没有与网络关联起来因此创建完套接字后我们还需要调用bind函数进行绑定操作。 绑定的步骤如下
定义一个struct sockaddr_in结构体将服务器网络相关的属性信息填充到该结构体当中比如协议家族、IP地址、端口号等。填充服务器网络相关的属性信息时协议家族对应就是AF_INET端口号就是当前TCP服务器程序的端口号。在设置端口号时需要调用htons函数将端口号由主机序列转为网络序列。在设置服务器的IP地址时我们可以设置为本地环回127.0.0.1表示本地通信。也可以设置为公网IP地址表示网络通信。如果使用的是云服务器那么在设置服务器的IP地址时不需要显示绑定IP地址直接将IP地址设置为INADDR_ANY即可此时服务器就可以从本地任何一张网卡当中读取数据。此外由于INADDR_ANY本质就是0因此在设置时不需要进行网络字节序的转换。填充完服务器网络相关的属性信息后需要调用bind函数进行绑定。绑定实际就是将文件与网络关联起来如果绑定失败也没必要进行后续操作了直接终止程序即可。
由于TCP服务器初始化时需要服务器的端口号因此在服务器类当中需要引入端口号当实例化服务器对象时就需要给传入一个端口号。而由于我当前使用的是云服务器因此在绑定TCP服务器的IP地址时不需要绑定公网IP地址直接绑定INADDR_ANY即可因此我这里没有在服务器类当中引入IP地址。
class TcpServer
{
public:TcpServer(int port): _sock(-1), _port(port){}void InitServer(){//创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}//绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}}~TcpServer(){if (_sock 0){close(_sock);}}
private:int _sock; //监听套接字int _port; //端口号
};当定义好struct sockaddr_in结构体后最好先用memset函数对该结构体进行清空也可以用bzero函数进行清空。bzero函数也可以对特定的一块内存区域进行清空bzero函数的函数原型如下
void bzero(void *s, size_t n);说明一下
TCP服务器绑定时的步骤与UDP服务器是完全一样的没有任何区别。
3.服务端监听
UDP服务器的初始化操作只有两步第一步就是创建套接字第二步就是绑定。而TCP服务器是面向连接的客户端在正式向TCP服务器发送数据之前需要先与TCP服务器建立连接然后才能与服务器进行通信。
因此TCP服务器需要时刻注意是否有客户端发来连接请求此时就需要将TCP服务器创建的套接字设置为监听状态。 listen函数 设置套接字为监听状态的函数叫做listen该函数的函数原型如下
int listen(int sockfd, int backlog);参数说明
sockfd需要设置为监听状态的套接字对应的文件描述符。backlog全连接队列的最大长度。如果有多个客户端同时发来连接请求此时未被服务器处理的连接就会放入连接队列该参数代表的就是这个全连接队列的最大长度一般不要设置太大设置为5或10即可。
返回值说明
监听成功返回0监听失败返回-1同时错误码会被设置。 服务器监听 TCP服务器在创建完套接字和绑定后需要再进一步将套接字设置为监听状态监听是否有新的连接到来。如果监听失败也没必要进行后续操作了因为监听失败也就意味着TCP服务器无法接收客户端发来的连接请求因此监听失败我们直接终止程序即可。
#define BACKLOG 5class TcpServer
{
public:void InitServer(){//创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}
private:int _listen_sock; //监听套接字int _port; //端口号
};说明一下
初始化TCP服务器时创建的套接字并不是普通的套接字而应该叫做监听套接字。为了表明寓意我们将代码中套接字的名字由sock改为_listen_sock 。在初始化TCP服务器时只有创建套接字成功、绑定成功、监听成功此时TCP服务器的初始化才算完成。
4.服务端获取连接
TCP服务器初始化后就可以开始运行了但TCP服务器在与客户端进行网络通信之前服务器需要先获取到客户端的连接请求。 accept函数 获取连接的函数叫做accept该函数的函数原型如下
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数说明
sockfd特定的监听套接字表示从该监听套接字中获取连接。addr对端网络相关的属性信息包括协议家族、IP地址、端口号等。addrlen调用时传入期望读取的addr结构体的长度返回时代表实际读取到的addr结构体的长度这是一个输入输出型参数。
返回值说明
获取连接成功返回接收到的套接字的文件描述符获取连接失败返回-1同时错误码会被设置。 accept函数返回的套接字是什么 调用accept函数获取连接时是从监听套接字当中获取的。如果accept函数获取连接成功此时会返回接收到的套接字对应的文件描述符。
监听套接字与accept函数返回的套接字的作用
监听套接字用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。accept函数返回的套接字用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接而真正为这些连接提供服务的套接字是accept函数返回的套接字而不是监听套接字。 服务端获取连接 服务端在获取连接时需要注意
accept函数获取连接时可能会失败但TCP服务器不会因为获取某个连接失败而退出因此服务端获取连接失败后应该继续获取连接。如果要将获取到的连接对应客户端的IP地址和端口号信息进行输出需要调用inet_ntoa函数将整数IP转换成字符串IP调用ntohs函数将端口号由网络序列转换成主机序列。inet_ntoa函数在底层实际做了两个工作一是将网络序列转换成主机序列二是将主机序列的整数IP转换成字符串风格的点分十进制的IP。
class TcpServer
{
public:void Start(){for (;;){//获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::coutget a new link-sock [client_ip]:client_portstd::endl;}}
private:int _listen_sock; //监听套接字int _port; //端口号
};服务端接收连接测试 现在我们可以做一下简单的测试看看当前服务器能否成功接收请求连接。在运行服务端时需要传入一个端口号作为服务端的端口号然后我们用该端口号构造一个服务端对象对服务端进行初始化后启动服务端即可。
void Usage(std::string proc)
{std::cout Usage: proc port std::endl;
}
int main(int argc, char* argv[])
{if (argc ! 2){Usage(argv[0]);exit(1);}int port atoi(argv[1]);TcpServer* svr new TcpServer(port);svr-InitServer();svr-Start();return 0;
}编译代码后以./tcp_server 端口号的方式运行服务端。 服务端运行后通过netstat命令可以查看到一个程序名为tcp_server的服务程序它绑定的端口就是8081而由于服务器绑定的是INADDR_ANY因此该服务器的本地IP地址是0.0.0.0这就意味着该TCP服务器可以读取本地任何一张网卡里面的数据。此外最重要的是当前该服务器所处的状态是LISTEN状态表明当前服务器可以接收外部的请求连接。 虽然现在还没有编写客户端相关的代码但是我们可以使用telnet命令远程登录到该服务器因为telnet底层实际采用的就是TCP协议。
使用telnet命令连接当前TCP服务器后可以看到此时服务器接收到了一个连接为该连接提供服务的套接字对应的文件描述符就是4。因为0、1、2是默认打开的其分别对应标准输入流、标准输出流和标准错误流而3号文件描述符在初始化服务器时分配给了监听套接字因此当第一个客户端发起连接请求时为该客户端提供服务的套接字对应的文件描述符就是4。 如果此时我们再用其他窗口继续使用telnet命令向该TCP服务器发起请求连接此时为该客户端提供服务的套接字对应的文件描述符就是5。 当然也可以直接用浏览器来访问这个TCP服务器因为浏览器常见的应用层协议是http或https其底层对应的也是TCP协议因此浏览器也可以向当前这个TCP服务器发起请求连接。 说明一下
至于这里为什么浏览器一次会向我们的TCP服务器发起两次请求这个问题这里就不作讨论了我们只是要证明当前TCP服务器能够正常接收外部的请求连接。
5.服务端处理请求
现在TCP服务器已经能够获取连接请求了下面当然就是要对获取到的连接进行处理。但此时为客户端提供服务的不是监听套接字因为监听套接字获取到一个连接后会继续获取下一个请求连接为对应客户端提供服务的套接字实际是accept函数返回的套接字下面就将其称为“服务套接字”。
为了让通信双方都能看到对应的现象我们这里就实现一个简单的回声TCP服务器服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出此时就能确保服务端和客户端能够正常通信了。 read函数 TCP服务器读取数据的函数叫做read该函数的函数原型如下
ssize_t read(int fd, void *buf, size_t count);参数说明
fd特定的文件描述符表示从该文件描述符中读取数据。buf数据的存储位置表示将读取到的数据存储到该位置。count数据的个数表示从该文件描述符中读取数据的字节数。
返回值说明
如果返回值大于0则表示本次实际读取到的字节个数。如果返回值等于0则表示对端已经把连接关闭了。如果返回值小于0则表示读取时遇到了错误。 read返回值为0表示对端连接关闭 这实际和本地进程间通信中的管道通信是类似的当使用管道进行通信时可能会出现如下情况
写端进程不写读端进程一直读此时读端进程就会被挂起因为此时数据没有就绪。读端进程不读写端进程一直写此时当管道被写满后写端进程就会被挂起因为此时空间没有就绪。写端进程将数据写完后将写端关闭此时当读端进程将管道当中的数据读完后就会读到0。读端进程将读端关闭此时写端进程就会被操作系统杀掉因为此时写端进程写入的数据不会被读取。
这里的写端就对应客户端如果客户端将连接关闭了那么此时服务端将套接字当中的信息读完后就会读取到0因此如果服务端调用read函数后得到的返回值为0此时服务端就不必再为该客户端提供服务了。 write函数 TCP服务器写入数据的函数叫做write该函数的函数原型如下
ssize_t write(int fd, const void *buf, size_t count);参数说明
fd特定的文件描述符表示将数据写入该文件描述符对应的套接字。buf需要写入的数据。count需要写入数据的字节个数。
返回值说明
写入成功返回实际写入的字节数写入失败返回-1同时错误码会被设置。
当服务端调用read函数收到客户端的数据后就可以再调用write函数将该数据再响应给客户端。 服务端处理请求 需要注意的是服务端读取数据是服务套接字中读取的而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字既可以读取数据也可以写入数据这就是TCP全双工的通信的体现。
在从服务套接字中读取客户端发来的数据时如果调用read函数后得到的返回值为0或者读取出错了此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标因此文件描述符的资源是有限的如果我们一直占用那么可用的文件描述符就会越来越少因此服务完客户端后要及时关闭对应的文件描述符否则会导致文件描述符泄漏。
class TcpServer
{
public:void Service(int sock, std::string client_ip, int client_port){char buffer[1024];while (true){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ //读取成功buffer[size] \0;std::cout get a new link- sock [ client_ip ]: client_port std::endl;write(sock, buffer, size);}else if (size 0){ //对端关闭连接std::cout client_ip : client_port close! std::endl;break;}else{ //读取失败std::cerr sock read error! std::endl;break;}}close(sock); //归还文件描述符std::cout client_ip : client_port service done! std::endl;}void Start(){for (;;){//获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link [ client_ip ]: client_port std::endl;//处理请求Service(sock, client_ip, client_port);}}
private:int _listen_sock; //监听套接字int _port; //端口号
};6.客户端创建套接字 同样的我们将客户端也封装成一个类当我们定义出一个客户端对象后也需要对其进行初始化而初始化客户端唯一需要做的就是创建套接字。而客户端在调用socket函数创建套接字时参数设置与服务端创建套接字时是一样的。 客户端不需要进行绑定和监听
服务端要进行绑定是因为服务端的IP地址和端口号必须要众所周知不能随意改变。而客户端虽然也需要IP地址和端口号但是客户端并不需要我们进行绑定操作客户端连接服务端时系统会自动指定一个端口号给客户端。服务端需要进行监听是因为服务端需要通过监听来获取新连接但是不会有人主动连接客户端因此客户端是不需要进行监听操作的。
此外客户端必须要知道它要连接的服务端的IP地址和端口号因此客户端除了要有自己的套接字之外还需要知道服务端的IP地址和端口号这样客户端才能够通过套接字向指定服务器进行通信。
class TcpClient
{
public:TcpClient(std::string server_ip, int server_port): _sock(-1), _server_ip(server_ip), _server_port(server_port){}void InitClient(){//创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}}~TcpClient(){if (_sock 0){close(_sock);}}
private:int _sock; //套接字std::string _server_ip; //服务端IP地址int _server_port; //服务端端口号
};7.客户端连接服务器
由于客户端不需要绑定也不需要监听因此当客户端创建完套接字后就可以向服务端发起连接请求。 connect函数 发起连接请求的函数叫做connect该函数的函数原型如下
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明
sockfd特定的套接字表示通过该套接字发起连接请求。addr对端网络相关的属性信息包括协议家族、IP地址、端口号等。addrlen传入的addr结构体的长度。
返回值说明
连接或绑定成功返回0连接失败返回-1同时错误码会被设置。 客户端连接服务器 需要注意的是客户端不是不需要进行绑定而是不需要我们自己进行绑定操作当客户端向服务端发起连接请求时系统会给客户端随机指定一个端口号进行绑定。因为通信双方都必须要有IP地址和端口号否则无法唯一标识通信双方。也就是说如果connect函数调用成功了客户端本地会随机给该客户端绑定一个端口号发送给对端服务器。
此外调用connect函数向服务端发起连接请求时需要传入服务端对应的网络信息否则connect函数也不知道该客户端到底是要向哪一个服务端发起连接请求。
class TcpClient
{
public:void Start(){struct sockaddr_in peer;memset(peer, \0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(_server_port);peer.sin_addr.s_addr inet_addr(_server_ip.c_str());if (connect(_sock, (struct sockaddr*)peer, sizeof(peer)) 0){ //connect successstd::cout connect success... std::endl;Request(); //发起请求}else{ //connect errorstd::cerr connect failed... std::endl;exit(3);}}
private:int _sock; //套接字std::string _server_ip; //服务端IP地址int _server_port; //服务端端口号
};8.客户端发起请求
由于我们实现的是一个简单的回声服务器因此当客户端连接到服务端后客户端就可以向服务端发送数据了这里我们可以让客户端将用户输入的数据发送给服务端发送时调用write函数向套接字当中写入数据即可。
当客户端将数据发送给服务端后由于服务端读取到数据后还会进行回显因此客户端在发送数据后还需要调用read函数读取服务端的响应数据然后将该响应数据进行打印以确定双方通信无误。
class TcpClient
{
public:void Request(){std::string msg;char buffer[1024];while (true){std::cout Please Enter# ;getline(std::cin, msg);write(_sock, msg.c_str(), msg.size());ssize_t size read(_sock, buffer, sizeof(buffer)-1);if (size 0){buffer[size] \0;std::cout server echo# buffer std::endl;}else if (size 0){std::cout server close! std::endl;break;}else{std::cerr read error! std::endl;break;}}}void Start(){struct sockaddr_in peer;memset(peer, \0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(_server_port);peer.sin_addr.s_addr inet_addr(_server_ip.c_str());if (connect(_sock, (struct sockaddr*)peer, sizeof(peer)) 0){ //connect successstd::cout connect success... std::endl;Request(); //发起请求}else{ //connect errorstd::cerr connect failed... std::endl;exit(3);}}
private:int _sock; //套接字std::string _server_ip; //服务端IP地址int _server_port; //服务端端口号
};在运行客户端程序时我们就需要携带上服务端对应的IP地址和端口号然后我们就可以通过服务端的IP地址和端口号构造出一个客户端对象对客户端进行初始后启动客户端即可。
void Usage(std::string proc)
{std::cout Usage: proc server_ip server_port std::endl;
}
int main(int argc, char* argv[])
{if (argc ! 3){Usage(argv[0]);exit(1);}std::string server_ip argv[1];int server_port atoi(argv[2]);TcpClient* clt new TcpClient(server_ip, server_port);clt-InitClient();clt-Start();return 0;
}9.服务器测试
现在服务端和客户端均已编写完毕下面我们进行测试。测试时我们先启动服务端然后通过netstat命令进行查看此时我们就能看到一个名为tcp_server的服务进程该进程当前处于监听状态。 然后再通过./tcp_client IP地址 端口号的形式运行客户端此时客户端就会向服务端发起连接请求服务端获取到请求后就会为该客户端提供服务。 当客户端向服务端发送消息后服务端可以通过打印的IP地址和端口号识别出对应的客户端而客户端也可以通过服务端响应回来的消息来判断服务端是否收到了自己发送的消息。 如果此时客户端退出了那么服务端在调用read函数时得到的返回值就是0此时服务端也就知道客户端退出了进而会终止对该客户端的服务。 注意 此时是服务端对该客户端的服务终止了而不是服务器终止了此时服务器依旧在运行它在等待下一个客户端的连接请求。
10.单执行流服务器的弊端
当我们仅用一个客户端连接服务端时这一个客户端能够正常享受到服务端的服务。 但在这个客户端正在享受服务端的服务时我们让另一个客户端也连接服务器此时虽然在客户端显示连接是成功的但这个客户端发送给服务端的消息既没有在服务端进行打印服务端也没有将该数据回显给该客户端。 只有当第一个客户端退出后服务端才会将第二个客户端发来是数据进行打印并回显该第二个客户端。 单执行流的服务器 通过实验现象可以看到这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器这个服务器一次只能为一个客户端提供服务。
当服务端调用accept函数获取到连接后就给该客户端提供服务但在服务端提供服务期间可能会有其他客户端发起连接请求但由于当前服务器是单执行流的只能服务完当前客户端后才能继续服务下一个客户端。 客户端为什么会显示连接成功 当服务端在给第一个客户端提供服务期间第二个客户端向服务端发起的连接请求时是成功的只不过服务端没有调用accept函数将该连接获取上来罢了。
实际在底层会为我们维护一个连接队列服务端没有accept的新连接就会放到这个连接队列当中而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的因此服务端虽然没有获取第二个客户端发来的连接请求但是在第二个客户端那里显示是连接成功的。 如何解决 单执行流的服务器一次只能给一个客户端提供服务此时服务器的资源并没有得到充分利用因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的此时就要引入多进程或多线程。
二.多进程版的TCP网络程序 我们可以将当前的单执行流服务器改为多进程版的服务器。 当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务而是当前执行流调用fork函数创建子进程然后让子进程为父进程获取到的连接提供服务。
由于父子进程是两个不同的执行流当父进程调用fork创建出子进程后父进程就可以继续从监听套接字当中获取新连接而不用关心获取上来的连接是否服务完毕。 子进程继承父进程的文件描述符表 需要注意的是文件描述符表是隶属于一个进程的子进程创建后会继承父进程的文件描述符表。比如父进程打开了一个文件该文件对应的文件描述符是3此时父进程创建的子进程的3号文件描述符也会指向这个打开的文件而如果子进程再创建一个子进程那么子进程创建的子进程的3号文件描述符也同样会指向这个打开的文件。 但当父进程创建子进程后父子进程之间会保持独立性此时父进程文件描述符表的变化不会影响子进程。最典型的代表就是匿名管道父子进程在使用匿名管道进行通信时父进程先调用pipe函数得到两个文件描述符一个是管道读端的文件描述符一个是管道写端的文件描述符此时父进程创建出来的子进程就会继承这两个文件描述符之后父子进程一个关闭管道的读端另一个关闭管道的写端这时父子进程文件描述符表的变化是不会相互影响的此后父子进程就可以通过这个管道进行单向通信了。
对于套接字文件也是一样的父进程创建的子进程也会继承父进程的套接字文件此时子进程就能够对特定的套接字文件进行读写操作进而完成对对应客户端的服务。 等待子进程问题 当父进程创建出子进程后父进程是需要等待子进程退出的否则子进程会变成僵尸进程进而造成内存泄漏。因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待。
阻塞式等待与非阻塞式等待
如果服务端采用阻塞的方式等待子进程那么服务端还是需要等待服务完当前客户端才能继续获取下一个连接请求此时服务端仍然是以一种串行的方式为客户端提供服务。如果服务端采用非阻塞的方式等待子进程虽然在子进程为客户端提供服务期间服务端可以继续获取新连接但此时服务端就需要将所有子进程的PID保存下来并且需要不断花费时间检测子进程是否退出。
总之服务端要等待子进程退出无论采用阻塞式等待还是非阻塞式等待都不尽人意。此时我们可以考虑让服务端不等待子进程退出。 不等待子进程退出的方式 让父进程不等待子进程退出常见的方式有两种
捕捉SIGCHLD信号将其处理动作设置为忽略。让父进程创建子进程子进程再创建孙子进程最后让孙子进程为客户端提供服务。
1.捕捉SIGCHLD信号 实际当子进程退出时会给父进程发送SIGCHLD信号如果父进程将SIGCHLD信号进行捕捉并将该信号的处理动作设置为忽略此时父进程就只需专心处理自己的工作不必关心子进程了。 该方式实现起来非常简单也是比较推荐的一种做法。
class TcpServer
{
public:void Start(){signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信号for (;;){//获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;pid_t id fork();if (id 0){ //child//处理请求Service(sock, client_ip, client_port);exit(0); //子进程提供完服务退出}}}
private:int _listen_sock; //监听套接字int _port; //端口号
};代码测试 重新编译程序运行服务端后可以通过以下监控脚本对服务进程进行监控。
while :; do ps axj | head -1 ps axj | grep tcp_server | grep -v grep;echo ######################;sleep 1;done此时可以看到一开始没有客户端连接该服务器此时服务进程只有一个该服务进程就是不断获取新连接的进程而获取到新连接后也是由该进程创建子进程为对应客户端提供服务的。 此时我们运行一个客户端让该客户端连接服务器此时服务进程就会调用fork函数创建出一个子进程由该子进程为这个客户端提供服务。 如果再有一个客户端连接服务器此时服务进程会再创建出一个子进程让该子进程为这个客户端提供服务。 最重要的是由于这两个客户端分别由两个不同的执行流提供服务因此这两个客户端可以同时享受到服务它们发送给服务端的数据都能够在服务端输出并且服务端也会对它们的数据进行响应。 当客户端一个个退出后在服务端对应为之提供服务的子进程也会相继退出但无论如何服务端都至少会有一个服务进程这个服务进程的任务就是不断获取新连接。 2.让孙子进程提供服务 我们也可以让服务端创建出来的子进程再次进行fork让孙子进程为客户端提供服务 此时我们就不用等待孙子进程退出了。 命名说明
爷爷进程在服务端调用accept函数获取客户端连接请求的进程。爸爸进程由爷爷进程调用fork函数创建出来的进程。孙子进程由爸爸进程调用fork函数创建出来的进程该进程调用Service函数为客户端提供服务。
我们让爸爸进程创建完孙子进程后立刻退出此时服务进程爷爷进程调用wait/waitpid函数等待爸爸进程就能立刻等待成功此后服务进程就能继续调用accept函数获取其他客户端的连接请求。 不需要等待孙子进程退出 而由于爸爸进程创建完孙子进程后就立刻退出了因此实际为客户端提供服务的孙子进程就变成了孤儿进程该进程就会被系统领养当孙子进程为客户端提供完服务退出后系统会回收孙子进程所以服务进程爷爷进程是不需要等待孙子进程退出的。 关闭对应的文件描述符 服务进程爷爷进程调用accept函数获取到新连接后会让孙子进程为该连接提供服务此时服务进程已经将文件描述符表继承给了爸爸进程而爸爸进程又会调用fork函数创建出孙子进程然后再将文件描述符表继承给孙子进程。
而父子进程创建后它们各自的文件描述符表是独立的不会相互影响。因此服务进程在调用fork函数后服务进程就不需要再关心刚才从accept函数获取到的文件描述符了此时服务进程就可以调用close函数将该文件描述符进行关闭。
同样的对于爸爸进程和孙子进程来说它们是不需要关心从服务进程爷爷进程继承下来的监听套接字的因此爸爸进程可以将监听套接字关掉。
关闭文件描述符的必要性
对于服务进程来说当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符服务套接字如果服务进程不及时关掉不用的文件描述符最终服务进程中可用的文件描述符就会越来越少。而对于爸爸进程和孙子进程来说还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字最终也只会导致这一个文件描述符泄漏但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作此时就会对监听套接字当中的数据造成影响。
class TcpServer
{
public:void Start(){for (;;){//获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;pid_t id fork();if (id 0){ //childclose(_listen_sock); //child关闭监听套接字if (fork() 0){exit(0); //爸爸进程直接退出}//处理请求Service(sock, client_ip, client_port); //孙子进程提供服务exit(0); //孙子进程提供完服务退出}close(sock); //father关闭为连接提供服务的套接字waitpid(id, nullptr, 0); //等待爸爸进程会立刻等待成功}}
private:int _listen_sock; //监听套接字int _port; //端口号
};服务器测试 重新编译程序运行客户端后继续使用监控脚本对服务进程进行实时监控。
while :; do ps axj | head -1 ps axj | grep tcp_server | grep -v grep;echo ######################;sleep 1;done此时没有客户端连接服务器因此也是只监控到了一个服务进程该服务进程正在等待客户端的请求连接。 此时我们运行一个客户端让该客户端连接当前这个服务器此时服务进程会创建出爸爸进程爸爸进程再创建出孙子进程之后爸爸进程就会立刻退出而由孙子进程为客户端提供服务。因此这时我们只看到了两个服务进程其中一个是一开始用于获取连接的服务进程还有一个就是孙子进程该进程为当前客户端提供服务它的PPID为1表明这是一个孤儿进程。 当我们运行第二个客户端连接服务器时此时就又会创建出一个孤儿进程为该客户端提供服务。 此时这两个客户端是由两个不同的孤儿进程提供服务的因此它们也是能够同时享受到服务的可以看到这两个客户端发送给服务端的数据都能够在服务端输出并且服务端也会对它们的数据进行响应。 当客户端全部退出后对应为客户端提供服务的孤儿进程也会跟着退出这时这些孤儿进程会被系统回收而最终剩下那个获取连接的服务进程。 三.多线程版的TCP网络程序 创建进程的成本是很高的创建进程时需要创建该进程对应的进程控制块task_struct、进程地址空间mm_struct、页表等数据结构。而创建线程的成本比创建进程的成本会小得多因为线程本质是在进程地址空间内运行创建出来的线程会共享该进程的大部分资源因此在实现多执行流的服务器时最好采用多线程进行实现。 当服务进程调用accept函数获取到一个新连接后就可以直接创建一个线程让该线程为对应客户端提供服务。
当然主线程服务进程创建出新线程后也是需要等待新线程退出的否则也会造成类似于僵尸进程这样的问题。但对于线程来说如果不想让主线程等待新线程退出可以让创建出来的新线程调用pthread_detach函数进行线程分离当这个线程退出时系统会自动回收该线程所对应的资源。此时主线程服务进程就可以继续调用accept函数获取新连接而让新线程去服务对应的客户端。 各个线程共享同一张文件描述符表 文件描述符表维护的是进程与文件之间的对应关系因此一个进程对应一张文件描述符表。而主线程创建出来的新线程依旧属于这个进程因此创建线程时并不会为该线程创建独立的文件描述符表所有的线程看到的都是同一张文件描述符表。 因此当服务进程主线程调用accept函数获取到一个文件描述符后其他创建的新线程是能够直接访问这个文件描述符的。
需要注意的是虽然新线程能够直接访问主线程accept上来的文件描述符但此时新线程并不知道它所服务的客户端对应的是哪一个文件描述符因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符的值也就是告诉每个新线程在服务客户端时应该对哪一个套接字进行操作。 参数结构体 实际新线程在为客户端提供服务时就是调用Service函数而调用Service函数时是需要传入三个参数的分别是客户端对应的套接字、IP地址和端口号。因此主线程创建新线程时需要给新线程传入三个参数而实际在调用pthread_create函数创建新线程时只能传入一个类型为void*的参数。
这时我们可以设计一个参数结构体Param此时这三个参数就可以放到Param结构体当中当主线程创建新线程时就可以定义一个Param对象将客户端对应的套接字、IP地址和端口号设计进这个Param对象当中然后将Param对象的地址作为新线程执行例程的参数进行传入。
此时新线程在执行例程当中再将这个void*类型的参数强转为Param*类型然后就能够拿到客户端对应的套接字IP地址和端口号进而调用Service函数为对应客户端提供服务。
class Param
{
public:Param(int sock, std::string ip, int port): _sock(sock), _ip(ip), _port(port){}~Param(){}
public:int _sock;std::string _ip;int _port;
};文件描述符关闭的问题 由于此时所有线程看到的都是同一张文件描述符表因此当某个线程要对这张文件描述符表做某种操作时不仅要考虑当前线程还要考虑其他线程。
对于主线程accept上来的文件描述符主线程不能对其进行关闭操作该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭。对于监听套接字虽然创建出来的新线程不必关心监听套接字但新线程不能将监听套接字对应的文件描述符关闭否则主线程就无法从监听套接字当中获取新连接了。 Service函数定义为静态成员函数 由于调用pthread_create函数创建线程时新线程的执行例程是一个参数为void*返回值为void*的函数。如果我们要将这个执行例程定义到类内就需要将其定义为静态成员函数否则这个执行例程的第一个参数是隐藏的this指针。
在线程的执行例程当中会调用Service函数由于执行例程是静态成员函数静态成员函数无法调用非静态成员函数因此我们需要将Service函数定义为静态成员函数。恰好Service函数内部进行的操作都是与类无关的因此我们直接在Service函数前面加上一个static即可。
class TcpServer
{
public:static void* HandlerRequest(void* arg){pthread_detach(pthread_self()); //分离线程//int sock *(int*)arg;Param* p (Param*)arg;Service(p-_sock, p-_ip, p-_port); //线程为客户端提供服务delete p; //释放参数占用的堆空间return nullptr;}void Start(){for (;;){//获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;Param* p new Param(sock, client_ip, client_port);pthread_t tid;pthread_create(tid, nullptr, HandlerRequest, p);}}
private:int _listen_sock; //监听套接字int _port; //端口号
};代码测试 此时我们再重新编译服务端代码由于代码当中用到了多线程因此编译时需要携带上-pthread选项。此外由于我们现在要监测的是一个个的线程因此在监控时使用的不再是ps -axj命令而是ps -aL命令。
while :; do ps -aL|head -1ps -aL|grep tcp_server;echo ####################;sleep 1;done运行服务端通过监控可以看到此时只有一个服务线程该服务线程就是主线程它现在在等待客户端的连接到来。 当一个客户端连接到服务端后此时主线程就会为该客户端构建一个参数结构体然后创建一个新线程将该参数结构体的地址作为参数传递给这个新线程此时该新线程就能够从这个参数结构体当中提取出对应的参数然后调用Service函数为该客户端提供服务因此在监控当中显示了两个线程。 当第二个客户端发来连接请求时主线程会进行相同的操作最终再创建出一个新线程为该客户端提供服务此时服务端当中就有了三个线程。 由于为这两个客户端提供服务的也是两个不同的执行流因此这两个客户端可以同时享受服务端提供的服务它们发送给服务端的消息也都能够在服务端进行打印并且这两个客户端也都能够收到服务端的回显数据。 此时无论有多少个客户端发来连接请求在服务端都会创建出相应数量的新线程为对应客户端提供服务而当客户端一个个退出后为其提供服务的新线程也就会相继退出最终就只剩下最初的主线程仍在等待新连接的到来。 四.线程池版的TCP网络程序 单纯多线程存在的问题 当前多线程版的服务器存在的问题
每当有新连接到来时服务端的主线程都会重新为该客户端创建为其提供服务的新线程而当服务结束后又会将该新线程销毁。这样做不仅麻烦而且效率低下每当连接到来的时候服务端才创建对应提供服务的线程。如果有大量的客户端连接请求此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多CPU的压力就越大因为CPU要不断在这些线程之间来回切换此时CPU在调度线程的时候线程和线程之间切换的成本就会变得很高。此外一旦线程太多每一个线程再次被调度的周期就变长了而线程是为客户端提供服务的线程被调度的周期变长客户端也迟迟得不到应答。 解决思路 针对这两个问题对应的解决思路如下
可以在服务端预先创建一批线程当有客户端请求连接时就让这些线程为客户端提供服务此时客户端一来就有线程为其提供服务而不是当客户端来了才创建对应的服务线程。当某个线程为客户端提供完服务后不要让该线程退出而是让该线程继续为下一个客户端提供服务如果当前没有客户端连接请求则可以让该线程先进入休眠状态当有客户端连接到来时再将该线程唤醒。服务端创建的这一批线程的数量不能太多此时CPU的压力也就不会太大。此外如果有客户端连接到来但此时这一批线程都在给其他客户端提供服务这时服务端不应该再创建线程而应该让这个新来的连接请求在全连接队列进行排队等服务端这一批线程中有空闲线程后再将该连接请求获取上来并为其提供服务。 引入线程池 实际要解决这里的问题我们就需要在服务端引入线程池因为线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价此外线程池还能够保证内核充分利用防止过分调度。
其中在线程池里面有一个任务队列当有新的任务到来的时候就可以将任务Push到线程池当中在线程池当中我们默认创建了5个线程这些线程不断检测任务队列当中是否有任务如果有任务就拿出任务然后调用该任务对应的Run函数对该任务进行处理如果线程池当中没有任务那么当前线程就会进入休眠状态。
在博主的另一篇博客当中详细介绍并实现了线程池这里就直接将线程池的代码接入到当前的TCP服务器因此下面只会讲解线程池接入的方法如果对线程池的实现有疑问的可以去阅读那篇博客。
#define NUM 5//线程池
templateclass T
class ThreadPool
{
private:bool IsEmpty(){return _task_queue.size() 0;}void LockQueue(){pthread_mutex_lock(_mutex);}void UnLockQueue(){pthread_mutex_unlock(_mutex);}void Wait(){pthread_cond_wait(_cond, _mutex);}void WakeUp(){pthread_cond_signal(_cond);}
public:ThreadPool(int num NUM): _thread_num(num){pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);}//线程池中线程的执行例程static void* Routine(void* arg){pthread_detach(pthread_self());ThreadPool* self (ThreadPool*)arg;//不断从任务队列获取任务进行处理while (true){self-LockQueue();while (self-IsEmpty()){self-Wait();}T task;self-Pop(task);self-UnLockQueue();task.Run(); //处理任务}}void ThreadPoolInit(){pthread_t tid;for (int i 0; i _thread_num; i){pthread_create(tid, nullptr, Routine, this); //注意参数传入this指针}}//往任务队列塞任务主线程调用void Push(const T task){LockQueue();_task_queue.push(task);UnLockQueue();WakeUp();}//从任务队列获取任务线程池中的线程调用void Pop(T task){task _task_queue.front();_task_queue.pop();}private:std::queueT _task_queue; //任务队列int _thread_num; //线程池中线程的数量pthread_mutex_t _mutex;pthread_cond_t _cond;
};服务类新增线程池成员 现在服务端引入了线程池因此在服务类当中需要新增一个指向线程池的指针成员
当实例化服务器对象时先将这个线程池指针先初始化为空。当服务器初始化完毕后再实际构造这个线程池对象在构造线程池对象时可以指定线程池当中线程的个数也可以不指定此时默认线程的个数为5。在启动服务器之前对线程池进行初始化此时就会将线程池当中的若干线程创建出来而这些线程创建出来后就会不断检测任务队列从任务队列当中拿出任务进行处理。 现在当服务进程调用accept函数获取到一个连接请求后就会根据该客户端的套接字、IP地址以及端口号构建出一个任务然后调用线程池提供的Push接口将该任务塞入任务队列。
这实际也是一个生产者消费者模型其中服务进程就作为了任务的生产者而后端线程池当中的若干线程就不断从任务队列当中获取任务进行处理它们承担的就是消费者的角色其中生产者和消费者的交易场所就是线程池当中的任务队列。。
class TcpServer
{
public:TcpServer(int port): _listen_sock(-1), _port(port), _tp(nullptr){}void InitServer(){//创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}_tp new ThreadPoolTask(); //构造线程池对象}void Start(){_tp-ThreadPoolInit(); //初始化线程池for (;;){//获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;Task task(sock, client_ip, client_port); //构造任务_tp-Push(task); //将任务Push进任务队列}}
private:int _listen_sock; //监听套接字int _port; //端口号ThreadPoolTask* _tp; //线程池
};设计任务类 现在我们要做的就是设计一个任务类该任务类当中需要包含客户端对应的套接字、IP地址、端口号表示该任务是为哪一个客户端提供服务对应操作的套接字是哪一个。
此外任务类当中需要包含一个Run方法当线程池中的线程拿到任务后就会直接调用这个Run方法对该任务进行处理而实际处理这个任务的方法就是服务类当中的Service函数服务端就是通过调用Service函数为客户端提供服务的。
我们可以直接拿出服务类当中的Service函数将其放到任务类当中作为任务类当中的Run方法但这实际不利于软件分层。我们可以给任务类新增一个仿函数成员当执行任务类当中的Run方法处理任务时就可以以回调的方式处理该任务。
class Task
{
public:Task(){}Task(int sock, std::string client_ip, int client_port): _sock(sock), _client_ip(client_ip), _client_port(client_port){}~Task(){}//任务处理函数void Run(){_handler(_sock, _client_ip, _client_port); //调用仿函数}
private:int _sock; //套接字std::string _client_ip; //IP地址int _client_port; //端口号Handler _handler; //处理方法
};注意 当任务队列当中有任务时线程池当中的线程会先定义出一个Task对象然后将这个Task对象作为输出型参数调用任务队列的Pop函数从任务队列当中获取任务因此Task类除了提供带参的构造函数以外还需要提供一个无参的构造函数方便我们可以定义无参对象。 设计Handler类 此时需要再设计一个Handler类在Handler类当中对()操作符进行重载将()操作符的执行动作重载为执行Service函数的代码。
class Handler
{
public:Handler(){}~Handler(){}void operator()(int sock, std::string client_ip, int client_port){char buffer[1024];while (true){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ //读取成功buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);}else if (size 0){ //对端关闭连接std::cout client_ip : client_port close! std::endl;break;}else{ //读取失败std::cerr sock read error! std::endl;break;}}close(sock); //归还文件描述符std::cout client_ip : client_port service done! std::endl;}
};实际我们可以让服务器处理不同的任务当前服务器只是在进行字符串的回显处理而实际要怎么处理这个任务完全是由任务类当中的handler成员来决定的。
如果想要让服务器处理其他任务只需要修改Handler类当中对()的重载函数就行了而服务器的初始化、启动服务器以及线程池的代码都是不需要更改的这就叫做把通信功能和业务逻辑在软件上做解耦。 代码测试 此时我们再重新编译服务端代码并用以下监控脚本查看服务端的各个线程。
while :; do ps -aL|head -1ps -aL|grep tcp_server;echo ####################;sleep 1;done运行服务端后就算没有客户端发来连接请求此时在服务端就已经有了6个线程其中有一个是接收新连接的服务线程而其余的5个是线程池当中为客户端提供服务的线程。 此时当客户端连接服务器后服务端的主线程就会获取该客户端的连接请求并将其封装为一个任务对象后塞入任务队列此时线程池中的5个线程就会有一个线程从任务队列当中获取到该任务并执行该任务的处理函数为客户端提供服务。 当第二个客户端发起连接请求时服务端也会将其封装为一个任务类塞到任务队列然后线程池当中的线程再从任务队列当中获取到该任务进行处理此时也是不同的执行流为这两个客户端提供的服务因此这两个客户端也是能够同时享受服务的。 与之前不同的是无论现在有多少客户端发来请求在服务端都只会有线程池当中的5个线程为之提供服务线程池当中的线程个数不会随着客户端连接的增多而增多这些线程也不会因为客户端的退出而退出。
五.守护进程
1.进程知识补充 进程组 进程组就是一个或多个进程的集合每个进程除了有一个PID外还属于一个进程组。每一个进程组都有一个唯一的标识PGID属于同一个进程组的进程其PGID相同。进程组中的第一个进程作为组长进程将其PID作为进程组的PGID
如下,我们同时启动了3个后台进程它们属于同一进程组进程组中的第一个进程PID14378的进程作为组长进程 任务启动一个进程就是启动一个任务 前台任务通过终端启动并且在启动后一直占据终端 后台任务启动时与终端无关或者通过终端启动后转入后台运行(即释放终端),不影响用户继续在终端中工作
在我们每次登录XShell后bash会默认占据前台任务也就是命令行解释器shell(即占用终端的控制权)当把进程任务自动切换为前台任务时shell自动切换为后台任务我们输入的命令就无效了
任务管理命令(Shell中控制进程组的方式) jobs查看所有任务 fg把任务提到前台此时这个任务就会变成前台任务shell自动切换为后台任务命令行解释器失效 ctrlz暂停前台任务 bg让暂停的任务在后台继续运行 会话 Linux是多用户多任务的分时系统所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话。在一个会话中用户可以与系统进行交互执行命令、操作文件、启动程序等。
比如我们先启动3个后台进程在启动3个前台进程后获取这些进程的信息 这些进程分别属于2个进程组它们与同一个终端关联属于同一个会话启动进程就是启动任务在每一次登录系统会为我们创建一次会话会话里至少有bash任务进行命令行解释命令行里可以启动多个任务,每个任务最终以进程组的形式在会话里存在。所以一个会话里可能存在很多进程组大小概念会话 进程组 进程
2.守护进程 概念 进程组分为前台任务和后台任务如果把后台任务提到前台则老的前台任务无法运行在会话中只能有一个前台任务在运行所以当我们在命令行启动一个进程的时候bash就无法运行了如果登录就是创建一个会话bash任务启动我们的进程就是在当前会话中创建新的前后台任务那么我们如果退出呢就会销毁会话可能会影响会话内部的所有任务
我们之前的服务器都是这样的 每一个服务端进程组与bash进程组同属于一个会话再次登录Xshell启动服务端就会创建新的会话 可是一般的网络服务器为了不受到用户的登录注销的影响就必须让服务端自成进程组自成会话使其与终端的状态无关可以一直运行的进程这样的进程就称作守护进程 创建守护进程 我们用的setsid函数自己实现守护进程不使用linux自带生成守护进程的接口daemon creates a session and sets the process group ID 创建会话并设置进程组ID
这个函数的使用关键调用的进程不能是组长进程
守护进程的创建步骤
让调用进程忽略掉异常信号fork()创建子进程让父进程直接退出自己不再是组长进程调用setsid新建会话, 子进程自成进程组成为会话的首进程将标准输入、输出和错误重定向到/dev/null中调用close关闭文件描述符防止守护进程与终端或其他进程的关联调用setsid,新建会话, 自己成为会话的首进程
dev/null是linux下的特殊文件会对写入的内容进行丢弃通常被用作丢弃不需要的输出或测试程序在遇到写入错误时的行为。 #pragma once#include cstdlib
#include unistd.h
#include signal.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h#include log.hpp
#include err.hpp// 1. setsid();
// 2. setsid(), 调用进程,不能是组长! 我们怎么保证自己不是组长呢?
// 3. 守护进程, 忽略异常信号 b. 0, 1, 2要特殊处理 c. 进程的工作路径可能要更改// 守护进程的本质: 是孤儿进程的一种
void Daemon()
{// 1. 忽略信号signal(SIGPIPE,SIG_IGN);signal(SIGCHLD,SIG_IGN);// 2. 让自己不要成为组长if(fork()0) // 父进程直接退出exit(0);// 3. 新建会话, 自己成为会话的首进程pid_t retsetsid();if((int)ret-1){logMessage(Fatal,deamon error, code: %d, error string: %s,errno, strerror(errno));exit(SET_ERR);}// 4. 可选: 可以更改守护进程的工作路径// chdir(/);// 5. 处理后续对于0,1,2的问题 --- /dev/null 文件就像垃圾桶int fdopen(/dev/null,O_RDWR); if(fd0){logMessage(Fatal,open error, code: %d, error string: %s,errno,strerror(errno));exit(SET_ERR);}dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd);
}给服务端加上该代码 运行结果 ?表示该进程与终端无关我们已经让此服务端以守护进程的方式运行该进程的PPID为1说明OS领养了守护进程守护进程本质是孤儿进程的一种 本文到此结束码文不易还请多多支持哦