【Linux网络(一)】Socket编程

文章目录

  • [1. 预备知识](#1. 预备知识)
    • [1.1 认识端口号](#1.1 认识端口号)
    • [1.2 初识TCP协议](#1.2 初识TCP协议)
    • [1.3 初识UDP协议](#1.3 初识UDP协议)
    • [1.4 网络字节序](#1.4 网络字节序)
    • [1.5 socket编程接口](#1.5 socket编程接口)
      • [1.5.1 套接字编程的种类](#1.5.1 套接字编程的种类)
      • [1.5.2 sockaddr结构体](#1.5.2 sockaddr结构体)
      • [1.5.3 socket 常见API](#1.5.3 socket 常见API)
      • [1.5.4 地址转换函数](#1.5.4 地址转换函数)
  • [2. 编写UDP服务器与客户端](#2. 编写UDP服务器与客户端)
    • [2.1 UDP服务器的创建](#2.1 UDP服务器的创建)
    • [2.2 UDP服务器接收/发送数据](#2.2 UDP服务器接收/发送数据)
    • [2.3 补充知识](#2.3 补充知识)
      • [2.3.1 查看网络状态](#2.3.1 查看网络状态)
      • [2.3.2 IP地址的绑定](#2.3.2 IP地址的绑定)
      • [2.3.3 关于端口号的绑定](#2.3.3 关于端口号的绑定)
    • [2.4 UDP客户端的创建](#2.4 UDP客户端的创建)
    • [2.5 UDP客户端接收/发送数据](#2.5 UDP客户端接收/发送数据)
  • [3. 编写TCP服务器与客户端](#3. 编写TCP服务器与客户端)
    • [3.1 TCP socket API详解](#3.1 TCP socket API详解)
    • [3.2 简单的TCP echo客户端](#3.2 简单的TCP echo客户端)
    • [3.3 TCP服务器](#3.3 TCP服务器)
      • [3.3.1 单进程版的TCP echo服务器](#3.3.1 单进程版的TCP echo服务器)
      • [3.3.2 多进程版的TCP echo服务器](#3.3.2 多进程版的TCP echo服务器)
      • [3.3.3 多线程版的TCP echo服务器](#3.3.3 多线程版的TCP echo服务器)
      • [3.3.4 线程池版的TCP echo服务器](#3.3.4 线程池版的TCP echo服务器)
  • [4. 守护进程化](#4. 守护进程化)
    • [4.1 前台进程和后台进程](#4.1 前台进程和后台进程)
    • [4.2 使用方法](#4.2 使用方法)
    • [4.3 进程组](#4.3 进程组)
    • [4.4 守护进程](#4.4 守护进程)

1. 预备知识

1.1 认识端口号

1、在进行网络通信的时候,是不是两台机器的通信呢?

  • 网络协议中(四层模型)的下三层,主要解决数据安全可靠的送到远端机器。
  • 用户使用应用层软件,完成数据的发送和接受。首先用户要将这个软件启动(进程的启动),实际上网络通信的本质就是进程间的通信

2、我怎么知道底层的报文向上交付要交给哪个应用程序?---通过端口号(port)。

下面介绍端口号相关的内容:

  • 端口号是一个2字节16位的整数;
  • 端口号用来唯一标识一个网络应用层的进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
    ○ 在公网上,因为IP地址能标识唯一的一台主机,而端口号port能标识主机上唯一的一个进程,所以 IP : Port 用于标识全网唯一的一台主机的唯一的一个进程。
    ○ 客户端和服务器均采用这种标识方式来标识它们在全网中唯一的主机的唯一的进程。
    ○ 我们把这种基于 IP地址 + 端口号 的通信方式叫socket
    ○ 一个端口号只能被一个进程占用。

3、在系统当中,我们学过一个概念,叫进程pid ,表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

  • 不是所有的进程都要网络通信,但是所有的进程都要pid
  • 首先进程pid可以标识进程的唯一性,但我们并不这么做,其目的是让系统和网络功能的做解耦,给网络单独设计一套标识进程唯一性的属性。

4、一个进程可以绑定多个端口号吗?一个端口号可以被多个进程绑定吗?如何理解?

  • 一个进程确实可以绑定多个端口号,这在网络编程中很常见,用于处理不同类型的网络请求。例如,Web服务器可以同时监听HTTP和HTTPS端口。
  • 然而,一个端口号在同一时间内只能被一个进程绑定,这是为了确保网络通信的准确性和稳定性。如果允许多个进程绑定到同一端口号,操作系统将无法正确地将网络请求路由到相应的进程,导致通信混乱和错误。因此,端口号在网络通信中扮演着至关重要的角色,作为服务或进程的唯一标识符。

1.2 初识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。

TCP的特性如下:

  • 传输层协议
  • 面向连接
  • 可靠传输
  • 面向字节流

TCP的应用场景主要包括:

复制代码
网页浏览:HTTP协议使用TCP来传输网页内容,保证数据的可靠性和顺序性。‌
文件传输:FTP协议使用TCP来传输文件,确保文件的完整性和正确性。
电子邮件:SMTP和POP3等协议使用TCP来传输邮件。‌
远程登录:Telnet协议使用TCP进行远程登录。

1.3 初识UDP协议

UDP(User Datagram Protocol 用户数据报协议)。特性如下:

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

UDP的应用场景主要包括:

复制代码
实时视频流和音频流传输:由于UDP的低延迟,它常用于视频流和音频流的实时传输,如在线直播、视频会议等。‌
网络游戏:UDP的快速传输和低延迟使其成为在线游戏中常用的协议,可以实现实时的游戏数据传输。
域名系统(DNS):UDP广泛用于域名系统中,用于域名解析和查询。

1.4 网络字节序

我们已经知道,内存中 的多字节数据相对于内存地址有 大端 小端 之分,磁盘文件中 的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流 同样有大端小端之分。如果两端主机的数据存储方式不一致,进行网络通信时如果不加以统一,接收方收到的数据将会是错误的信息。 那么应该如何定义网络数据流的地址呢?

  • 发送主机 通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机 把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

如图 高权值位 放 低地址 是大端 ;低权值位 放 低地址 是小端(简记"小小小")

可以调用以下接口做网络字节序主机字节序的转换:

cpp 复制代码
#include <arpa/inet.h>

  uint32_t htonl(uint32_t hostlong);

  uint16_t htons(uint16_t hostshort);

  uint32_t ntohl(uint32_t netlong);

  uint16_t ntohs(uint16_t netshort);

记忆方式:h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序;

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;

如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

1.5 socket编程接口

1.5.1 套接字编程的种类

种类 功能
域间套接字编程 用于本地通信,同一台机器内的多进程通信,域间套接字是网络套接字的子集
原始套接字编程 绕过网络层,直接访问底层,常用于编写网络工具,比如网络抓包,检测计算机是否连通
网络套接字编程 用于用户间的网络通信

由于网络通信有不同的场景,所以就有3种不同的套接字种类,所以理论上有3种套接字就需要3套接口。

但是网络设计者想将网络接口统一抽象化。 网络接口统一抽象化就要求参数类型是统一的,socket网络编程当中参数统一使用struct sockaddr, 函数内部会对这个结构体的前两个字节做判断:如果前两个字节的地址类型是AF_INET(如address->type = AF_INET)就使用网络套接字;如果前两个字节的地址类型是AF_UNIX就使用域间套接字。

1.5.2 sockaddr结构体

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16 位端口号和32位IP地址;
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容;
  • socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数(这种传入不同的数据类型能得到不同的网络地址格式的特性就是多态的特性)。

1.5.3 socket 常见API

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器) 
int socket(int domain, int type, int protocol); 

// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address, socklen_t address_len); 

// 开始监听socket (TCP, 服务器) 
int listen(int socket, int backlog); 

// 接收请求 (TCP, 服务器) 
int accept(int socket, struct sockaddr* address, socklen_t* address_len); 

// 建立连接 (TCP, 客户端) 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

1.5.4 地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位 的 IP 地址,用户层通常用点分十进制的字符串表示 IP 地址,以下接口可以用于 字符串 和 in_addr 之间的相互转换。
字符串转in_addr的函数:

in_addr转字符串的函数:

2. 编写UDP服务器与客户端

2.1 UDP服务器的创建

Step1:创建UDP socket

cpp 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
	
	// 创建套接字,创建用于通信的端点并返回描述符。
	int socket(int domain, int type, int protocol);
	// 参数:
	// domain:指定通信域,一般填写如下:
	// AF_UNIX, AF_LOCAL   Local communication       
	// AF_INET             IPv4 Internet protocols 
	// AF_INET6            IPv6 Internet protocols
	// type:socket数据类型->面向数据流SOCK_STREAM ->面向数据报 SOCK_DGRAM
	// protocol:该协议指定套接字使用的特定协议。通常protocol指定为0。
	// 返回值:On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.

填写后:

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0) {
	perror("socket");
	return;
}

Step2:绑定bind socket

cpp 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

绑定之前我们要创建 sockaddr_in 结构体

cpp 复制代码
#include <netinet/in.h>
#include <strings.h>
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 将一块内存空间清0
local.sin_family = AF_INET; 					// 指定通信域
local.sin_port = htons(_port); 					// 设置端口号
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 设置IP地址
// sin_family, sin_port, sin_addr这三项和结构体前三个字段是一一对应的
  • 关于sockaddr_in结构体内部的成员构成:

  • 主机字节序转网络字节序:

    主机发送数据给客户端或者服务器都是要连同自己的IP地址端口号 一起发送过去的,因为是网络通信,发送的数据势必要经过网络,因此IP地址和端口号是需要在网络上来回发送的,一定要保证是网络字节序才能确保对方准确的收到数据。

端口号的主机字节序转网络字节序API:

cpp 复制代码
uint16_t ntohs(uint16_t netshort);

IP地址 的主机字节序转网络字节序API:

形如"198.168.0.1"这种字符串风格的IP地址(点分十进制IP地址)是给我们用户层看的。而数据发送到网络当中的是4字节IP地址。

综上:IP端口的赋值(sin_addr),要考虑两点:1,从stirng转换为uint32_t;2,uint32_t必须为网络字节序

上面的两件事有一个接口替我们完成了:这个接口可以帮助我们将字符串风格的IP地址转化为4字节的网络字节序的IP地址

cpp 复制代码
// 字符串IP转网络IP
in_addr_t inet_addr(const char *cp);

至此我们的UDP服务器的初始化就完成了。

2.2 UDP服务器接收/发送数据

(一)服务器接受客户端的数据

服务器接受客户端的数据,会将数据保存在缓冲区buf 里面;还会将对端的套接字信息保存在输出型参数src_addr里面(包含对端的IP和端口号)。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
// sockfd:本地套接字
// buf:缓冲区
// flags:默认设置为0,阻塞等待
// src_addr是输出型参数:别人给我发了消息,我收到了消息之后,就会将对端的套接字信息保存在src_addr所指向的内存空间里。
// addrlen是输入型参数:获取套接字信息的结构体大小
// 接收成功返回接收到的字节数; 接受失败-1被返回, 并且可以通过errno查看错误信息; 当对等端执行有序关闭时,返回值将为0
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

使用示例如下:

cpp 复制代码
char buffer[1024];
while(true)
{
		// 读取客户端发来的数据
		struct sockaddr_in client; // 用于保存客户端的socket信息
		bzero(&client, sizeof(client));
		socklen_t len = sizeof(client);
		
		// client是输出型参数和len是输入型参数 - 输入:client缓冲区的大小 - 输出:实际读到的client
		ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);
		if (s < 0)
		{
		    perror("recvfrom");
		    continue;
		}
		// ...
}

(二)服务器发送给客户端的数据

服务端接收到客户端的数据后,将数据进行处理,最后将数据发回给客户端

发送数据的API接口是:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
// sockfd:本地套接字
// buf:要发送的数据的缓冲区  len:要发送的数据大小
// dest_addr是输入性参数:	传入的是客户端socket信息
// addrlen是输入性参数:		客户端socket信息的大小
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

在上述接口中:发送的数据buf,发送的对端在哪呢?在上面定义的struct sockaddr_in client结构体里,它里面包含了对端主机IP和端口号。还有,我们不需要再进行网络字节序的转化,因为client就是刚才从网络中收到的。

使用示例如下:

cpp 复制代码
char buffer[1024];
while(true)
{
		// 读取客户端发来的数据
		// ...

		// 将数据发回给客户端 
		buffer[s] = 0; // buffer是之前接收到客户端的数据, buffer[s] = 0是因为这里我将数据作为字符串处理, 末尾'\0'          
	    //  查看是哪个客户端
	    uint16_t client_port = ntohs(client.sin_port);      // 注意:这个端口号是从网络中来的
	    std::string client_ip = inet_ntoa(client.sin_addr); // 此IP是4字节的网络序列IP,我们需要转化为本主机字符串风格的IP,方便显示
	    printf("[%s:%d]# %s\n", client_ip.c_str(), client_port, buffer);
	
	    // 数据的处理过程(可替换)
	    std::string info = buffer;
	    std::string echo_string = "Server echo# " + info;
	    
	    // 将数据写回给client(第一个参数是网络文件描述符, 第2-3个参数是发送的内容及大小, 最后两个参数是发送给谁)
	    sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len); 
}

2.3 补充知识

2.3.1 查看网络状态

bash 复制代码
# 查看udp的网络状态
netstat -naup
netstat -nlup
netstat -aup
# 查看tcp的网络状态
netstat -natp
netstat -nltp
netstat -atp

带"n"表示能显示数字的就显示数字,不带表示能显示字符串就显示字符串。

"a"表示all;"u"表示udp;"t"表示tcp;"p"表示进程信息。

Proto表示协议;Recv-Q和Send-Q表示收发报文的个数;Local Adress表示本地地址(Ip+端口号);

Foreign Adress表示远端,里面的"0.0.0.0"和"*"表示我认为我能接受任何客户端发送的消息。

2.3.2 IP地址的绑定

我尝试绑定自己主机的IP,但是发生了绑定失败,造成这种情况的解释:

1.虚拟机下绑定是可以成功的;

2.云服务器上的IP并不是真正主机上的IP而是虚拟IP;

3.云服务器主机可能有多张网卡,那么就对应着多个IP,如果我们只bind一个固定的IP,就收不到其他网卡的消息了;

4.规定:云服务器禁止直接绑定公网IP,应该使用IP为0进行绑定,其含义是任意地址bind,凡是发送给我这台主机的数据,都要根据端口号向上交付,根据端口号分配)。

从今往后云服务器不再使用IP地址,而是直接用0来表示,如下图,当然也可以不用将主机字节序转网络字节序,因为0无所谓

2.3.3 关于端口号的绑定

0,1023\]这个区间是系统内定的端口号,不能由用户去绑定,这个区间的端口一般都由固定的应用层协议使用,如:http(80),https(443),ssh(22);但是也有1023区间外的,如:mysql(3306)。 如下图所示,演示了绑定1023端口号的场景: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f29cd8fcd444429893acd4e0afd79bd1.png)虽然超级用户可以绑定成功,但是不建议我们去绑定这个区间的端口号,我们可以尽量往大的去绑定。 ### 2.4 UDP客户端的创建 **step1:创建套接字** ```cpp int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd < 0) { std::cout << "socket error" << std::endl; exit(2); } ``` **step2:绑定** * 客户端需要绑定吗? 客户端一定需要绑定!因为服务器要和客户端上的进程互发消息,就要知道该进程对应的端口号(因为一个端口号只能被一个进程所绑定),所以绑定客户端的IP和端口是必须的。很多人说不用绑定这个说法是错的,只不过不需要我们去显示的绑定,一般由操作系统随机分配。 * 其实客户端的端口号是多少并不重要,只要保证在主机上的唯一性就可以。 因为将来客户端是主动向服务器发起的,一旦动态绑定了,端口号就会发送给服务端,此时服务器就知道了我的端口号,为了避免端口号和常用的服务端口号冲突,因此客户端的端口号不需要我们自己去显示的绑定。 * 为什么服务器端口号要显示的绑定呢? 因为将来用户是要来访问服务器的,我们总不能让操作系统给我们随机分配一个未绑定的端口吧,所以服务器要显示的绑定确定的服务器IP和端口号。 * 操作系统什么时候给我们bind呢? 当客户端首次发送消息给服务器的时候,操作系统会自动给客户端 bind 它的IP和端口号。 ### 2.5 UDP客户端接收/发送数据 上面讨论过,客户端的创建无需显示绑定,那么下面我们就直接进行数据的收发: ```cpp struct sockaddr_in server; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 点分十进制字符串->4bytes socklen_t len = sizeof(server); std::string message; char buffer[1024]; while(true) { // 给服务器发送消息 std::cout << "请输入你的信息# "; std::getline(std::cin, message); if(message == "quit") break; // 当client首次发送消息给服务器的时候,OS会自动给client bind它的IP和Port ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len); // 接收来自服务器的消息 struct sockaddr_in temp; // 这里可以不明确, 因为我不一定只接受指定的一台服务器发送的消息 socklen_t _len = sizeof(temp); ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &_len); if(s > 0) { buffer[s] = 0; std::cout << buffer << std::endl; // 打印收到服务器的消息 } } ``` ## 3. 编写TCP服务器与客户端 ### 3.1 TCP socket API详解 **(一)TCP服务器API** TCP的API也和UDP一样,也包含创建套接字(socket)、初始化sockaddr_in结构体和绑定套接字(bind)。不同的地方在于,TCP是面向连接的,服务器还有个监听的过程,监听到了客户端就要连接它。 😗 **Step3:监听 listen()** 😗 什么是监听?Tcp是面向连接的,服务器会一直等待着客户端的连接到来,这个过程叫监听。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c6e956a104a240e28b5c621b12407417.png) * 说明:listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5); * 返回值:listen()成功返回0,失败返回-1. 启动服务器,如果没有客户端连接,服务器会处于listen状态 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/26a6c317c3b44d4fb99b81d5de074ed3.png) 😗 **Step4:连接 accept()** 😗 * **参数的理解:** sockfd:处于监听状态的那个套接字;addr和addrlen:是输出型参数,用于获取客户端的套接字相关信息 * **理解accecpt的返回值:** 连接建立成功了返回文件描述符;失败返回-1. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a9d54b0e183c45e7b816caed77febc2c.png) * **两个sockfd的理解:** 一个是形参传入的套接字,一个是建立连接返回的套接字,我们应该用哪一个呢? 形参中的sockfd(被创建、被绑定、被监听的)的核心工作是将底层的连接获取上来;而建立连接返回的sockfd的核心工作是给后期数据进行IO操作的。 一般的,我们把第一个获取新连接的套接字叫做listensock;listensock的文件描述符一般是3,sockfd的文件描述符一般从4开始。并且随着客户端的连接到来越来越多,第一个sockfd只有一个,但是accept返回的sockfd会越来越多! **(二)TCP客户端API** 客户端需要进行套接字的创建,要bind但不需要显示的bind;因为TCP是面向连接的,最后相对于UDP多了一个向服务器发起连接的过程。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c5840046ba864e8283e6c68b78e4aeaa.png)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b81a947186a64a96b38a50dd91de7a73.png)客户端调用connect()连接远程服务器;connect()成功返回0,出错返回-1。 ### 3.2 简单的TCP echo客户端 ```cpp #include #include #include #include #include #include #include static void usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl; } // 命令行输入:./tcpclient serverip serverport int main(int argc, char *argv[]) { if(argc != 3) { usage(argv[0]); exit(1); } std::string serverip = argv[1]; uint16_t serverport = atoi(argv[2]); // step1:创建客户端套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { std::cerr << "socket error" << std::endl; return 1; } // 初始化sockaddr_in结构体 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr)); // step2:tcp客户端不需要显示的去绑定套接字, 由系统进行bind, 绑定随机端口号 // step3:向服务器发起连接 int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server)); if(n < 0) { std::cerr << "connect error..." << std::endl; return 2; } std::string message; while(true) { std::cout << "Please Enter# "; std::getline(std::cin, message); // 向服务器发送数据 write(sockfd, message.c_str(), message.size()); // 收到服务器发回来的消息 char inbuffer[4096]; int n = read(sockfd, inbuffer, sizeof(inbuffer)); if(n > 0) { inbuffer[n] = 0; // 字符串末尾\0 std::cout << inbuffer << std::endl; } } close(sockfd); return 0; } ``` ### 3.3 TCP服务器 #### 3.3.1 单进程版的TCP echo服务器 ```cpp const int defaultfd = -1; const std::string defaultip = "0.0.0.0"; const int backlog = 5; Log log; // 用于打印日志信息的类 enum { usageError = 1, SockError, BindError, ListenError }; class TcpServer { public: TcpServer(const uint16_t &port, const std::string &ip = defaultip) : _listensock(defaultfd), _serverport(port), _serverip(ip) {} ~TcpServer() {} void InitServer() { // step1:创建套接字 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { log(FATAL, "create socket failed, errno: %d, errstring: %s", errno, strerror(errno)); exit(SockError); } log(NORMAL, "create socket success, listensock: %d", _listensock); // 初始化sockaddr_in结构体 struct sockaddr_in local; memset(&local, sizeof(local), 0); local.sin_family = AF_INET; local.sin_port = htons(_serverport); inet_aton(_serverip.c_str(), &(local.sin_addr)); // step2:绑定套接字 if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { log(FATAL, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } log(NORMAL, "bind socket success, listensock: %d", _listensock); // step3:监听套接字,服务器等待连接的到来 if (listen(_listensock, backlog) < 0) { log(FATAL, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } log(NORMAL, "listen socket success, listensock: %d", _listensock); } void Run() { log(NORMAL, "TcpServer is running"); while (true) { struct sockaddr_in client; socklen_t len = sizeof(client); // step4:获取连接 int sockfd = accept(_listensock, (struct sockaddr *)&client, &len); if (sockfd < 0) { log(WARNING, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); continue; // 获取新连接失败就重新获取 } // 拿到客户端的ip和端口, 来自于accept的输出型参数 uint16_t clientport = ntohs(client.sin_port); char clientip[16]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // step5:根据新连接来进行通信 log(NORMAL, "get a new link..., sockfd: %d, client ip: %s, client port: %d\n", sockfd, clientip, clientport); // ============================= version 1 -- 单进程版 ===================== // Service(sockfd, clientip, clientport); close(sockfd); } } // 服务的编写 void Service(int sockfd, const std::string &clientip, const uint16_t &clientport) { char buffer[4096]; while (true) { // 收消息 ssize_t n = read(sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; // 字符串末尾\0 std::cout << "client say# " << buffer << std::endl; std::string echo_string = "Tcpserver echo# "; echo_string += buffer; // 发消息回去 write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { log(NORMAL, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd); break; } else { log(WARNING, "read error, sockfd: %d, client ip: %s, client port:%d", sockfd, clientip.c_str(), clientport); break; } } } private: int _listensock; uint16_t _serverport; std::string _serverip; }; ``` **异常测试:** **测试一:** 客户端突然关闭了,观察服务器的现象 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/32bd6ecc78864b2e8612e53611fb4855.png)可以看到客户端退出,服务端的`service`里的`read`会返回0,service终止,并关闭文件描述符 **测试二:** 两个客户端和服务器进行通信,这两个客户端能同时和服务器通信吗?还是只能一个一个来? 当客户端1先建立连接,那么客户端2就要等待客户端1退出之后,才能继续向服务器发送消息。因为这个一个单进程版的服务器,然而现实当中很少只有一个客户端和服务器进行通信。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f0d698c8493e4f62a7cad71164056ecc.png) #### 3.3.2 多进程版的TCP echo服务器 思想:父进程用于获取新连接,子进程和服务器进行IO操作 * 文件描述符的处理:子进程会继承父进程的文件描述符表,也就是父子进程各有一张文件描述符表,子进程在表中能看到父进程创建的文件描述符sockfd、listensock;因为子进程的工作只是对数据进行IO,它只需要sockfd,不需要使用listensock时,所以建议将它关闭;而父进程的工作只是获取新连接,不需要sockfd,一定要将它关闭,不然一旦父进程获取新的套接字,他就要重新建立新的连接,获取新的文件描述符,父进程的文件描述符表会越用越少! ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/126d4ea46e864f80a00a0621a01e3466.png)如上图,为父子进程创建的文件描述符,父进程将sockfd关闭了,sockfd这个文件会被释放吗?子进程还能执行相应的服务吗?不会有影响,因为sockfd所对应的`struct file`结构体里包含了引用计数,只有当引用计数为0时,文件才会被释放。 **下面是多进程版的服务器核心部分:** ```cpp // ==================================== version 2 -- 多进程版本 ============================= // pid_t id = fork(); if(id == 0) { // 子进程 close(_listensock); if(fork() > 0) exit(1); // 子进程创建孙子进程并退出 // 孙子进程提供服务 Service(sockfd, clientip, clientport); close(sockfd); exit(0); } // 父进程 close(sockfd); pid_t rid = waitpid(id, nullptr, 0); (void)rid; ``` 观察以上代码,思考为什么子进程也要`fork`创建孙子进程? * 首先如果是正常的父进程阻塞等待新连接到来,子进程去执行IO操作,那么只有子进程执行的服务结束,父进程才能够继续获取新连接,这样和单进程并没有什么区别。 * 如果父进程设置为非阻塞等待,子进程执行服务,但是非阻塞轮巡中,父进程会创建越来越多的子进程,我们还得记录子进程pid,若不想记录可以将id设置成-1,但这都不是比较好的做法。 * 推荐的做法:添加`if(fork() > 0) exit(1);`,其含义是:子进程创建了孙子进程,子进程立马退出,退出的同时,父进程回收子进程,父进程无需阻塞就可以继续去执行获取连接操作。从而达到了父进程和孙子进程的并发访问。 需不需要对孙子进程`waitpid`呢?不需要。子进程是孙子进程的父进程,子进程提前退出,此时孙子进程是孤儿进程,一旦孙子进程结束,它会被操作系统(1号进程)领养,由操作系统回收。 如下图为测试多进程版的服务器,建立了两个客户端1和2,他们分别同时向服务器发起连接,连接建立完毕再纷纷关闭客户端连接,观察现象。 通过打印进程`./tcpserver`相关信息,发现父进程最终未退出,孙子进程最终被操作系统所领养了。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0c752b69b90c4196968b03ac3f7d8c53.png)最后思考一下:我可以在服务端Run方法的开头就fork吗?这样多进程的每一个进程就都能执行接受新连接和IO的操作了。 可以,但比较麻烦,因为多进程对listensock是同时可见的,他们会竞争的访问listensock这同一块资源,此时我们要对多进程进行加锁。 #### 3.3.3 多线程版的TCP echo服务器 多进程版的服务器已经能并发的处理多个客户端的数据了。但实际上,创建进程的代价是非常大的。 多线程版的服务器核心思想是:接收到一个新连接,就创建一个新线程,新线程去执行相应的服务,主线程回到循环开始重新获取新连接。 下面给出代码,并注意细节问题: 细节一:主线程不需要等待,它有别的事情要做,它要获取新连接,新线程要取执行相应的服务。也就是主线程和子线程能够并发执行,我们可以用线程的分离的方法由操作系统回收子线程,这样主线程就不用阻塞的join子线程了。 细节二:在多线程中,还需要向多进程一样,关闭没有必要的文件描述符吗?千万不能关,因为我目前还是单进程,文件描述符并没有多余,也就是说引用计数为1,多线程是共用同一张文件描述符表的。 细节三:在类内的静态成员函数如何调用类的非静态成员函数呢?想办法通过this指针的传递。 ```cpp #pragma once #include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" const int defaultfd = -1; const std::string defaultip = "0.0.0.0"; const int backlog = 5; // 一般不要设置的太大 Log log; enum { usageError = 1, SockError, BindError, ListenError }; class TcpServer; class ThreadData { public: ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *ts) : sockfd(fd), clientip(ip), clientport(port), tsvr(ts) {} public: int sockfd; uint16_t clientport; std::string clientip; TcpServer *tsvr; }; class TcpServer { public: TcpServer(const uint16_t &port, const std::string &ip = defaultip) : _listensock(defaultfd), _serverport(port), _serverip(ip) {} ~TcpServer() {} void InitServer() { // step1:创建套接字 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { log(FATAL, "create socket failed, errno: %d, errstring: %s", errno, strerror(errno)); exit(SockError); } log(NORMAL, "create socket success, listensock: %d", _listensock); struct sockaddr_in local; memset(&local, sizeof(local), 0); local.sin_family = AF_INET; local.sin_port = htons(_serverport); inet_aton(_serverip.c_str(), &(local.sin_addr)); // step2:绑定 if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { log(FATAL, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } log(NORMAL, "bind socket success, listensock: %d", _listensock); // step3:监听 if (listen(_listensock, backlog) < 0) { log(FATAL, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } log(NORMAL, "listen socket success, listensock: %d", _listensock); } static void *Routinue(void *args) { pthread_detach(pthread_self()); // 把线程自己设置为分离状态, 子线程由OS去释放,主进程就不wait它了 ThreadData *td = static_cast(args); while(true) { td->tsvr->Service(td->sockfd, td->clientip, td->clientport); delete td; return nullptr; } } void Run() { log(NORMAL, "TcpServer is running"); while (true) { // step4:获取新连接 struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(_listensock, (struct sockaddr *)&client, &len); if (sockfd < 0) { log(WARNING, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); continue; // 获取新连接失败就重新获取 } // 通过client拿到客户端的ip和端口 uint16_t clientport = ntohs(client.sin_port); char clientip[16]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 根据新连接来进行通信 log(NORMAL, "get a new link..., sockfd: %d, client ip: %s, client port: %d\n", sockfd, clientip, clientport); // =========================== version 3 -- 多线程版本 =========================== // ThreadData *td = new ThreadData(sockfd, clientip, clientport, this); pthread_t tid; pthread_create(&tid, nullptr, Routinue, td); } } void Service(int sockfd, const std::string &clientip, const uint16_t &clientport) { // ... } private: int _listensock; uint16_t _serverport; std::string _serverip; }; ``` 下面我通过telnet充当客户端向服务器发起连接,通过浏览器向服务器发起请求,最终逐渐推出客户端。查看线程`tcpserver`,他们的pid都是相同的,说明他们是同一个进程的不同线程。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/13d03806964c4a3b8dc1d910a47cef65.png)但是多线程版的服务器也有缺陷:以上的场景适用于微型服务的应用,但随着用户的连接增多,线程数也会越来越多。 #### 3.3.4 线程池版的TCP echo服务器 提前创建线程,在客户端到来时,线程能很快马上去执行服务;我们不提供长时间的服务,提供短时间服务,也就是客户端发送一个消息我就给出一个结果,然后关闭sockfd;最后还要限定一个线程上限; 主线程只需要注册一个任务,对于任务的处理交给线程池,主线程继续去获取新连接,注册新任务。客户端向服务器发起请求,请求执行完毕连接就关闭。 **多线程版的服务器:** ```cpp #pragma once #include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" #include "ThreadPool.hpp" #include "Task.hpp" const int defaultfd = -1; const std::string defaultip = "0.0.0.0"; const int backlog = 5; // 一般不要设置的太大 Log log; enum { usageError = 1, SockError, BindError, ListenError }; class TcpServer { public: TcpServer(const uint16_t &port, const std::string &ip = defaultip) : _listensock(defaultfd), _serverport(port), _serverip(ip) {} ~TcpServer() {} void InitServer() { // step1:创建套接字 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { log(FATAL, "create socket failed, errno: %d, errstring: %s", errno, strerror(errno)); exit(SockError); } log(NORMAL, "create socket success, listensock: %d", _listensock); struct sockaddr_in local; memset(&local, sizeof(local), 0); local.sin_family = AF_INET; local.sin_port = htons(_serverport); inet_aton(_serverip.c_str(), &(local.sin_addr)); // step2:绑定 if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { log(FATAL, "bind error, errno: %d, errstring: %s", errno, strerror(errno)); exit(BindError); } log(NORMAL, "bind socket success, listensock: %d", _listensock); // step3:监听 if (listen(_listensock, backlog) < 0) { log(FATAL, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } log(NORMAL, "listen socket success, listensock: %d", _listensock); } void Run() { // 启动线程池 ThreadPool::GetInstance()->start(); log(NORMAL, "TcpServer is running"); while (true) { // step4:获取新连接 struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(_listensock, (struct sockaddr *)&client, &len); if (sockfd < 0) { log(WARNING, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); continue; // 获取新连接失败就重新获取 } // 通过client拿到客户端的ip和端口 uint16_t clientport = ntohs(client.sin_port); char clientip[16]; inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 根据新连接来进行通信 log(NORMAL, "get a new link..., sockfd: %d, client ip: %s, client port: %d\n", sockfd, clientip, clientport); // =========================== version 4 -- 线程池版本 =========================== // Task t(sockfd, clientip, clientport); // 构建一个任务 ThreadPool::GetInstance()->Push(t); // 将任务push到线程池里 } } private: int _listensock; uint16_t _serverport; std::string _serverip; }; ``` **任务类:** ```cpp #pragma once #include #include #include "log.hpp" extern Log log; enum{ DIV_ZERO = 1, MOD_ZERO, UNKNOWN }; class Task { public: Task(int sockfd, const std::string &clientip, const uint16_t &clientport) : _sockfd(sockfd), _clientip(clientip), _clientport(clientport) {} void run() { char buffer[4096]; // 收消息 ssize_t n = read(_sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; // 字符串末尾\0 std::cout << "client say# " << buffer << std::endl; std::string echo_string = "Tcpserver echo# "; echo_string += buffer; // 发消息回去 int n = write(_sockfd, echo_string.c_str(), echo_string.size()); if(n < 0) { log(WARNING, "write error, errno: %d, strerror: %s", errno, strerror(errno)); } } else if (n == 0) { log(NORMAL, "%s:%d quit, server close sockfd: %d", _clientip.c_str(), _clientport, _sockfd); } else { log(WARNING, "read error, sockfd: %d, client ip: %s, client port:%d", _sockfd, _clientip.c_str(), _clientport); } // 消息处理完毕, 关闭文件描述符 close(_sockfd); } void operator()() { run(); } ~Task() {} private: int _sockfd; std::string _clientip; uint16_t _clientport; }; ``` **线程池类:** ```cpp #pragma once #include #include #include #include #include struct ThreadInfo { pthread_t tid; std::string threadname; }; static const int defaultnum = 5; template class ThreadPool { public: void Lock() { pthread_mutex_lock(&_mutex); } void Unlock() { pthread_mutex_unlock(&_mutex); } void Wakeup() { pthread_cond_signal(&_cond); } // 检测任务队列(临界资源)没有任务就休眠 void Sleep() { pthread_cond_wait(&_cond, &_mutex); } bool IsQueueEmpty() { return _tasks.empty(); } std::string GetThreadName(pthread_t tid) { for (const auto &thread : _threads){ if (tid == thread.tid) return thread.threadname; } return "None"; } public: // 注意类内成员函数隐含this指针,如果需要传参给pthread_create就需要放在类外 或者加 static static void *HandlerTask(void *args) { ThreadPool *tp = static_cast *>(args); std::string name = tp->GetThreadName(pthread_self()); while (true){ // 检测任务栏队列是否有任务 tp->Lock(); while (tp->IsQueueEmpty()) tp->Sleep(); // 没有任务 T t = tp->Pop(); // 有任务: 取任务并pop tp->Unlock(); // 执行任务 t(); } } // 开始构建线程池 void start() { int nums = _threads.size(); for (int i = 0; i < nums; i++){ _threads[i].threadname = "thread-" + std::to_string(i + 1); pthread_create(&(_threads[i].tid), nullptr, HandlerTask, this); } } void Push(const T &task) { Lock(); _tasks.push(task); Wakeup(); // 新增了任务,需要将线程唤醒去执行任务了 Unlock(); } T Pop() // 这里不能加锁的原因是HandlerTask里加锁了 { T t = _tasks.front(); _tasks.pop(); return t; } static ThreadPool* GetInstance() { if(nullptr == _tp){ if(nullptr == _tp){ pthread_mutex_lock(&_lock); std::cout << "log: Singleton create one first" << std::endl; _tp = new ThreadPool(); } pthread_mutex_unlock(&_lock); } return _tp; } private: ThreadPool(int num = defaultnum) : _threads(num) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } ThreadPool(const ThreadPool& td) = delete; const ThreadPool& operator=(const ThreadPool& td) = delete; private: std::vector _threads; std::queue _tasks; pthread_mutex_t _mutex; pthread_cond_t _cond; static ThreadPool* _tp; static pthread_mutex_t _lock; }; template ThreadPool *ThreadPool::_tp = nullptr; template pthread_mutex_t ThreadPool::_lock = PTHREAD_MUTEX_INITIALIZER; ``` 我这里线程池对线程的上限设置成了5,测试如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ce61e915d86348538eb089d2d5dcaee2.png) ## 4. 守护进程化 ### 4.1 前台进程和后台进程 在计算机科学中,特别是在操作系统和应用程序管理的上下文中,前台进程(Foreground Process)和后台进程(Background Process)是两个重要的概念,它们主要根据与用户交互的方式和优先级进行区分。 > **前台进程(Foreground Process)** > > 前台进程是指那些与用户当前操作直接相关,需要用户持续注意和交互的进程。这些进程通常**具有更高的优先级** ,以确保它们能够及时地响应用户的操作。**前台进程通常在用户界面中有一个可见的元素(如窗口、对话框等),这些元素会接收用户的输入,并显示输出给用户。** 例如,当你打开一个文本编辑器并开始编辑文档时,这个文本编辑器就是一个前台进程。 > **后台进程(Background Process)** > > 与前台进程相对,后台进程是指那些不需要用户持续注意和交互,但仍然在运行的进程。**这些进程通常具有较低的优先级** ,以便在不影响前台进程的情况下运行。后台进程通常执行一些辅助任务,如打印作业、数据备份、病毒扫描等。由于这些任务不需要用户即时关注,因此它们可以在用户执行其他任务时,在后台默默执行。后台进程可能没有用户界面,或者即使有,也是最小化或隐藏的状态。 > **区别:** > > * 用户交互:前台进程需要用户直接交互,而后台进程则不需要。 > * 优先级:**前台进程通常具有更高的优先级**,以确保它们能够及时地响应用户的操作;而后台进程的优先级则较低。 > * 有无标准输入:**前台进程通常能进行标准输入** ,即和键盘关联;而**后台进程通常不能进行标准输入** ,即无键盘关联;键盘文件只有一个,因此**前台进程只有一个**。 > * 功能:前台进程通常执行用户当前需要关注的任务,如编辑文档、浏览网页等;而后台进程则执行一些辅助性的、用户不需要即时关注的任务。 关于前台进程和后台进程可以借助下图来理解: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3d74bf1a55324a2dbb34d1fb2d589b23.png) ### 4.2 使用方法 下面用以下程序做测试: ```cpp #include #include int main() { while(1) { std::cout << "running ..." << std::endl; sleep(1); } return 0; } ``` 源文件编译生成可执行程序: ```bash [Pau@VM-16-2-centos ~]$ g++ -o process process.cc ``` 准备工作已做好,下面开始拿下面命令做实验: * 将进程**添加为后台进程**(在可执行文件后+\&): ```bash ./process >> log.txt & # 进程输出的数据重定向到log.txt文件是为了避免输出到显示屏造成干扰 ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/73901d73cd234129bc3f5453d99846fd.png) * 查看当前的后台进程: ```bash jobs ``` * 将后台任务提到前台: ```bash fg +任务号 # 后台任务提到前台后,可以搭配ctrl+c将进程释放掉 # 其中任务号是jobs里[]里面的数字 ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/aebbeef4457849cdab8b79f64ea07bee.png) * 将前台进程切换回后台进程: ```bash ctrl+z # ctrl+z对应的是SIGSTOP信号,操作系统会将暂停的进程切换到后台,将bash进程切换回前台 ``` * 让后台暂停的任务继续运行起来: ```bash bg +任务号 # bg(background的缩写) ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/15d74798e2e44f0da8638846156b6caf.png) ### 4.3 进程组 我们在同一个会话当中创建三个进程并添加到后台进程。 ```bash [Pau@VM-16-2-centos ~]$ ./process >> log.txt & [1] 26964 [Pau@VM-16-2-centos ~]$ sleep 10000 | sleep 20000 | sleep 30000 & [2] 1046 ``` 查看进程组id,发现process的pid和进程组id是一样的,也就是说它自成进程组;三个sleep进程的进程组id是相同的,并且第一个sleep进程pid等于进程组id,第一个sleep进程称为组长进程。 ```bash [Pau@VM-16-2-centos ~]$ ps -axj | head -1 && ps -axj | grep -Ei 'process|sleep' ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9d7e6edf50434bbfbb3c108dd9e3537d.png) 其实我们可以发现:bash进程pid = 进程组id = session id,也就是说bash进程实际上就是我们创建的会话 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ddda2f8919e04ce5bb8b76fa33d9b174.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/aea48280ee5c4e70a229b3dbe875483a.png) 由上可知,每创建一个会话,其实就是创建一个bash进程,如果在这个会话当中创建了多个后台任务,然后我们关闭会话(bash退出),会发生什么呢? 会有两个现象: 现象一:用户注销重新登陆后,后台进程仍然存在,后台进程被操作系统领养了,其PPID为1。后台进程的PPID改变了,也就是后台进程受到了用户登录和退出的影响。 现象二:用户注销重新登陆后,后台进程不再存在。也就是用户登录和退出之后,后台进程被干掉了,这也不是我们想要的。 **如果我们不想让后台进程受到用户登录和退出的影响,并且独立于控制终端,能在后台运行,并且通常在系统启动时自动启动,直到系统关闭时才停止。这就是下面我们要讲的守护进程。** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f767c5ff781f4d62ab7349545c670f8d.png) ### 4.4 守护进程 > 守护进程(Daemon Process)是一种在Unix、Linux以及类Unix操作系统中运行的后台进程。它的主要特点是独立于控制终端,在后台运行,并且通常在系统启动时自动启动,直到系统关闭时才停止。守护进程通常用于执行系统级的任务,如网络服务、文件系统的维护、系统日志记录等。 创建守护进程的API(注意:不能让组长进程成为守护进程) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5d75dee01e7c4a3595f0c1d61b928ee5.png)创建守护进程通常需要执行以下步骤: * 1. 创建子进程:父进程首先创建一个子进程,然后父进程退出。因为父进程是第一个进程,不允许组长进程守护进程化,让其子进程成为"孤儿"进程,从而被init进程(PID为1的进程,所有孤儿进程的父进程)收养,进而让子进程守护进程化。 * 2. 在子进程中创建新会话:通过调用setsid()函数,子进程可以创建一个新的会话,并成为该会话的领头进程。这样做可以确保子进程完全独立于原来的控制终端。 * 3. 改变当前工作目录:守护进程通常会将其工作目录更改为根目录(/),以避免占用可卸载的文件系统。 > 查看当前进程的工作目录: `bash ls /proc/进程pid -l # 找到目录下的cwd目录 `更改当前进程的工作目录: > > ```cpp > chdir - change working directory > > #include > int chdir(const char *path); > ``` * 4. 重设文件掩码:守护进程通常会调用umask(0)来设置文件创建掩码,以确保守护进程创建的文件具有适当的权限。 * 5. 关闭文件描述符:守护进程通常会关闭从父进程继承来的所有文件描述符,以避免在不需要的文件上浪费资源。 > 通常文件描述符并不是直接关闭,而是重定向到`/dev/null`。 > **什么是/dev/null?** > > /dev/null 是一个特殊的设备文件,通常被称为空设备或黑洞。在Unix和类Unix系统(如Linux和macOS)中,向 /dev/null 写入任何数据都会被系统丢弃,读取它则会立即返回文件结束(EOF)。这意味着,你可以将任何输出重定向到 /dev/null 来丢弃它,或者将 /dev/null 用作输入来避免读取任何实际数据。 * 6. 处理SIGCHLD信号:守护进程通常会忽略SIGCHLD信号,因为子进程的终止状态将由系统处理,而守护进程不需要关心。 守护进程的代码如下: ```cpp #include #include #include #include #include #include #include #include // 所有的linux系统都提供了字符垃圾处理文件,凡是往该文件写都将被丢弃 const std::string nullfile = "/dev/null"; void Deamon(const std::string &cwd = "") { // 1. 忽略其他异常信号 signal(SIGCLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); signal(SIGSTOP, SIG_IGN); // 2. 创建子进程, 将子进程自成会话 if (fork() > 0) exit(0); setsid(); // 3. 更改当前调用进程的工作目录 if (!cwd.empty()) chdir(cwd.c_str()); // 4. 标准输入、标准输出、标准错误全部重定向到/dev/null int fd = open(nullfile.c_str(), O_RDWR); // 读写方式打开 if(fd > 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); } } ``` 在我们上面写的process.cc中加上守护进程化: ```cpp #include #include #include "Deamon.hpp" int main() { Deamon(); while(1) { std::cout << "running ..." << std::endl; sleep(1); } return 0; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f3915c9e21fd4726accbf884830f9823.png)可以发现我们的进程的父进程是1号进程,并且独立于终端。进一步我们可以查看到它的当前工作目录和重定向的文件。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/178e88c2000f4b5490b923e3d8a94cca.png)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f0ed808cee434130b1e4253dd33e857c.png) 当然系统当中就存在了对应的调用守护进程的接口,我们直接用就行 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/41fdf627a3204d249e7c60bc05ff9804.png)综上,我们写的TCP服务器也可以加上守护进程为我们提供长时间的服务(注意:命名后台服务时,一般以d为结尾,比如mysql服务是mysqld)。

相关推荐
shykevin1 小时前
python开发Streamable HTTP MCP应用
开发语言·网络·python·网络协议·http
tmacfrank2 小时前
网络编程中的直接内存与零拷贝
java·linux·网络
数据与人工智能律师4 小时前
虚拟主播肖像权保护,数字时代的法律博弈
大数据·网络·人工智能·算法·区块链
QQ2740287564 小时前
Soundness Gitpod 部署教程
linux·运维·服务器·前端·chrome·web3
qwfys2004 小时前
How to configure Linux mint desktop
linux·desktop·configure·mint
南方以南_4 小时前
Ubuntu操作合集
linux·运维·ubuntu
purrrew5 小时前
【Java ee初阶】HTTP(2)
网络·网络协议·http
冼紫菜5 小时前
[特殊字符]CentOS 7.6 安装 JDK 11(适配国内服务器环境)
java·linux·服务器·后端·centos
Chuncheng's blog6 小时前
RedHat7 如何更换yum镜像源
linux
爱莉希雅&&&6 小时前
shell脚本之条件判断,循环控制,exit详解
linux·运维·服务器·ssh