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

【Linux】TCP的服务端(守护进程) + 客户端

文章目录

  • 📖 前言
  • 1. 服务端基本结构
    • 1.1 类成员变量:
    • 1.2 头文件
    • 1.3 初始化:
      • 1.3 - 1 全双工与半双工
      • 1.3 - 2 inet_aton
      • 1.3 - 3 listen
  • 2. 服务端运行接口
    • 2.1 accept:
    • 2.2 服务接口:
  • 3. 客户端
    • 3.1 connect:
    • 3.2 客户端的实现:
  • 4. 提供服务
    • 4.1 单进程版本:
    • 4.2 多进程1.0版本:
    • 4.3 多进程1.1版本:
    • 4.4 多线程2.0版本:
    • 4.5 线程池3.0版本:
    • 4.6 执行客户端指令:
  • 5. 守护进程
    • 5.1 进程组&&会话:
    • 5.2 引入守护进程:
    • 5.3 实现:
    • 5.4 守护进程化的剩余两种方法:

📖 前言

上一节,我们用了udp写了一个服务端和客户端之间通信的代码,只要函数了解认识到位,上手编写是很容易的。
本章我们开始编写tcp的服务端和客户端之前通信的代码,要认识一批新的接口,并将我们之前学习的系统知识加进来,做到融会贯通…

代码详情:👉 Gitee


1. 服务端基本结构

对于TCP服务器和UDP服务器的初始化接口,确实有一些相似之处,但是它们在选择字节流进行初始化方面存在一些区别。

  • 首先,无论是TCP服务器还是UDP服务器,都需要进行套接字的创建、绑定和监听操作。这些初始化步骤是相同的。
  • 区别在于,TCP服务器使用字节流(byte stream) 进行数据传输,而UDP服务器使用数据报(datagram) 进行数据传输。
  • 对于UDP协议,任何人都可以向服务器发送数据报,而且不需要等待服务器响应。UDP协议是无连接的传输协议,数据报发送出去后就结束。
  • TCP协议是面向连接的传输协议,需要先建立连接才能进行数据传输,并且在连接建立、数据传输和断开连接的过程中需要互相响应。

1.1 类成员变量:

class Task
{// ....private:int sock_;        // 给用户提供IO服务的sockuint16_t port_;   // client portstd::string ip_;  // client ipcallback_t func_; // 回调方法
};

1.2 头文件

因为每个源文件都要包好多相同的头文件,所以我们将要用到的头文件一并打包在一个头文件里:

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"#define SOCKET_ERR 1
#define BIND_ERR   2
#define LISTEN_ERR 3
#define USAGE_ERR  4
#define CONN_ERR   5#define BUFFER_SIZE 1024

一般涉及到struct sockaddr_in,都要包含这两个头文件:
在这里插入图片描述

1.3 初始化:

TCP是面向字节流的:

在这里插入图片描述

1.3 - 1 全双工与半双工

全双工(Full Duplex)和半双工(Half Duplex)是通信中两种不同的传输模式:

  • 全双工是指通信双方可以同时进行双向的数据传输。
    • 在全双工模式下,通信双方的发送和接收操作是独立进行的,彼此之间不会互相干扰。
    • 这种模式可以实现实时的双向通信,类似于我们平时打电话或进行视频通话时的交流方式。
  • 半双工是指通信双方在同一时间内只能进行单向的数据传输。
    • 在半双工模式下,通信双方轮流地进行发送和接收操作,不能同时进行。
    • 当一方发送数据时,另一方只能等待接收,反之亦然。
    • 这种模式类似于对讲机的使用方式,一方讲话时,另一方只能听取,无法即时回应。

套接字和管道:

  • 管道只能通过一个文件描述符读,一个文件描述符写,所以叫做单向管道。
  • 而在TCP中读写用的都是一个套接字fd,UDP在读写时用的也是一个套接字。
  • TCP/UDP都支持全双工。

1.3 - 2 inet_aton

int inet_aton(const char *cp, struct in_addr *inp);
  • 它的作用是将一个点分十进制的IP地址字符串(cp)转换为网络字节序的二进制数,并将结果存储在in_addr结构体(inp)中。
  • 因此,inet_aton函数的第一个参数是要转换的IP地址字符串,第二个参数是存储转换结果的结构体指针。
  • 函数的返回值是一个整数,表示转换是否成功。如果转换成功,返回值为非零;如果转换失败,返回值为零。

1.3 - 3 listen

listen函数用于将一个已经建立连接的套接字(通常是一个服务端的套接字)标记为被动模式,开始监听来自客户端的连接请求。

在这里插入图片描述

它接受两个参数:sockfd是要设置为被动模式的套接字文件描述符,backlog是指定等待连接队列的最大长度。

accept第一个参数监听到了之后,然后返回一个值之后,再继续去监听。

listen的第二个参数我们以后再讲…

监听socket,为何要监听呢?

  • 因为udp是无连接的(通信可以,但是不用建立连接,直接发消息就可以了),而tcp是面向连接的!
  • 面向就是在做任何事之前要先干什么这就是面向的意思,面向连接就是在做其他工作之前先把连接建立好。
  • 不管有没有客户端连接,得让服务器将来任何时候被别人连接,所以要将套接字设置成监听状态。

下面的初始化就和之前udp的初始化大差不差了…

void init()
{// 1. 创建socketlistenSock_ = socket(PF_INET, SOCK_STREAM, 0);if (listenSock_ < 0){logMessage(FATAL, "socket: %s", strerror(errno));exit(SOCKET_ERR);}logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);// 2. bind绑定// 2.1 填充服务器信息struct sockaddr_in local; // 用户栈memset(&local, 0, sizeof local);local.sin_family = PF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 2.2 本地socket信息,写入sock_对应的内核区域if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0){logMessage(FATAL, "bind: %s", strerror(errno));exit(BIND_ERR);}logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);// 3. 监听socket,为何要监听呢?tcp是面向连接的!if (listen(listenSock_, 5 /*后面再说*/) < 0){logMessage(FATAL, "listen: %s", strerror(errno));exit(LISTEN_ERR);}logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);// 走到这就意味着允许别人来连接你了// 4. 加载线程池// tp_ = ThreadPool<Task>::getInstance();
}

2. 服务端运行接口

2.1 accept:

accept函数用于接受客户端连接的请求。它被用于一个已经处于被动监听状态的套接字(通常是服务端的套接字)。
当有新的客户端连接请求到达时,accept函数将会返回一个新的套接字文件描述符,此后服务端就可以通过这个新的套接字与客户端进行通信。

在这里插入图片描述

  • sockfd表示要接受连接的套接字文件描述符。
  • addr指向保存客户端地址信息的结构体指针(可以传入NULL)。
  • addrlen表示addr结构体的长度。

后面两个参数和recvfrom后两个参数的含义一模一样,是想拿到是哪个客户端连接的。

  • 第一个参数sockfd是套接字描述符: 用来获取新连接的套接字,叫做监听socket
  • 这个监听套接字负责监听指定的网络地址和端口,等待客户端的连接请求。
  • 返回值是一个套接字描述符: 主要是为用户提供网络服务的socket,主要是进行IO
  • 当有客户端发起连接请求时,accept()函数就会返回一个新的套接字。
  • 这个新的套接字与客户端的套接字建立连接,用于后续的数据传输。
    在这里插入图片描述

accept函数的阻塞:

  • accept函数是在网络编程中用于接受客户端连接的函数。
  • 当调用accept函数时,如果有客户端连接请求到达,它会立即返回一个新的套接字来与该客户端进行通信。
  • 如果没有客户端连接请求到达,accept函数将会阻塞,即一直等待直到有新的连接请求到达为止。

在阻塞状态下,程序会停止执行后续代码,直到有新的连接请求到达或者发生错误。因此,可以将accept函数放在一个循环中,反复接受多个客户端连接。需要注意的是,在某些情况下,可以通过设置套接字为非阻塞模式来避免accept函数的阻塞,这样程序可以继续执行其他操作。

void loop()
{tp_->start();logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 4. 获取连接, accept 的返回值是一个新的socket fd ??// 4.1 listenSock_: 监听 && 获取新的链接-> sock// 4.2 serviceSock: 给用户提供新的socket服务int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取链接失败logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);continue;}// 4.1 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 提供服务....}
}

2.2 服务接口:

提供的服务,将小写转成大写:

// 大小写转化服务
// TCP && UDP: 支持全双工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串if (s > 0){// read successinbuffer[s] = '\0';if (strcasecmp(inbuffer, "quit") == 0){logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);// 可以进行大小写转化了for (int i = 0; i < s; i++){if (isalpha(inbuffer[i]) && islower(inbuffer[i]))inbuffer[i] = toupper(inbuffer[i]);}logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);write(sock, inbuffer, strlen(inbuffer));}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里,一定是client退出了,服务到此结束close(sock); logMessage(DEBUG, "server close %d done", sock);
}

recvfromsendto是专门针对udp发送用户数据报的,它是一 个固定大小的报文,在那里它是专函数专用的,专门为udp提供的。而tcp就通用的多,因为tcp是流式服务,我们这里直接可以当做是处理文件的方式来进行读写。

如果一个进程对应的文件fd,打开了没有被归还,这种现象叫做文件描述符泄漏!

  • 如果不关,来一个客户端打开一个文件描述符,会导致该服务端进程可用文件描述符越来越少。
  • 文件描述符表是有上限的,时间一久,会导致服务器无法获取新连接,申请文件描述符时发现所有文件描述符都被占用了。
  • 此时服务器就无法对外提供服务了。

3. 客户端

3.1 connect:

connect是一个系统调用函数,用于建立与远程主机的连接。它通常用于创建客户端套接字,并将其连接到服务器套接字。
在这里插入图片描述

  • sockfd:套接字文件描述符,由socket函数创建获得。
  • addr:指向远程主机的地址结构体的指针,可以是struct sockaddr_instruct sockaddr_in6
  • addrlen:远程主机地址结构体的长度。

connect 会自动帮我们进行bind!

connect函数通过sockfdaddr参数指定的地址信息,将本地套接字与远程主机的套接字连接起来。如果连接成功,返回0;如果连接失败,返回-1,并设置全局变量errno表示错误类型。

在这里插入图片描述
注意:在使用connect函数之前,必须先创建一个套接字,并确保套接字是可用的,可以使用socket函数进行创建。

三个问题:

  • 客户端需要绑定吗?需要但是不需要自己显示的bind!
  • 需要监听吗?不需要,监听是让别人来连你,作为客户端不用被连!
  • 需要accept吗?都没人来连你,根本不需要获取连接!

3.2 客户端的实现:

有了上面的分析,再加上之前udp编写的基础,我们很容易就能将tcp的客户端编写完成:

#include "util.hpp"volatile bool quit = false;static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"<< std::endl;
}// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERR);}std::string serverIp = argv[1];uint16_t serverPort = atoi(argv[2]);// 1. 创建socket SOCK_STREAMint sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. connect,发起链接请求,你向谁发起请求呢??当然是向服务器发起请求喽!// 2.1 先填充需要连接的远端主机的基本信息struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(serverPort);inet_aton(serverIp.c_str(), &server.sin_addr);// 2.2 发起请求,(隐性的概念)connect 会自动帮我们进行bind!if (connect(sock, (const struct sockaddr *)&server, sizeof server) != 0){std::cerr << "connect: " << strerror(errno) << std::endl;exit(CONN_ERR);}std::cout << "info : connect success: " << sock << std::endl;std::string message;while (!quit){message.clear();std::cout << "请输入你的消息>>> ";std::getline(std::cin, message);if (strcasecmp(message.c_str(), "quit") == 0)quit = true;// 向服务器发消息ssize_t s = write(sock, message.c_str(), message.size());std::cout << "read before" << std::endl;if (s > 0){message.resize(1024);ssize_t s = read(sock, (char *)(message.c_str()), 1024);if (s > 0)  message[s] = 0;std::cout << "Server Echo>>> " << message << std::endl;}else if (s <= 0){break;}}close(sock);return 0;
}

日志重定向:

之前我们将日志全部都打印在显示器上,这次我们将日志全部都打印到一个文件中,方便以后查看:

在这里插入图片描述

客户端连接服务器:

在这里插入图片描述


4. 提供服务

4.1 单进程版本:

// 提供服务, echo -> 小写 -> 大写
// 0.0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accepttransService(serviceSock, peerIp, peerPort);

我们不重定向,方便我们进行实验。

实验结果:

在这里插入图片描述
在这里插入图片描述
如果ctrl + c杀掉客户端进程的话:

  • ctrl + c异常终止的话,文件是只有这个进程打开的,文件的生命周期是随进程的。
  • 如果强制的将客户端ctrl + c掉,操作系统会自动的关闭掉进程所对应的文件描述符。
  • 进程退出,PCB被文件释放,文件描述符表被释放,文件指针指向的struct file结构体引用计数减减。
  • 因为只有一个指向文件结构体,就减到0,操作系统自动关闭这个文件描述符。
  • 已关闭该文件,服务端读文件就会读到0,就类似于读到文件结束。

多个客户端连接服务器(有问题的):

我们发现一个客户端连接服务器的时候,客户端可以正常的显示出服务器处理过的结果。
但是,一旦我们有两个或者两个以上的客户端连接服务器就会出问题:新连接的客户端会卡在那里。

在这里插入图片描述
原因解释(看我笔记吧):

在这里插入图片描述

4.2 多进程1.0版本:

// 1.0 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的!
pid_t id = fork();
assert(id != -1);
if(id == 0)
{close(listenSock_); // 建议关掉transService(serviceSock, peerIp, peerPort);exit(0); // 任务处理完就退出,进入僵尸
}
// 父进程 -- 父进程不用对外提供服务
close(serviceSock); // 这一步是一定要做的!
// waitpid(); 默认是阻塞等待!WNOHANG

(服务函数放在类内,类外都行)

  • 子进程也会把曾经父进程打开的listen套接字继承下去。
  • 通过创建子进程,让其去做父进程代码的一部分。
  • close(listenSock_);建议关掉。
    • 万一子进程将listenSock_文件描述符给写了,可能影响将来accept
  • close(serviceSock);这一步是一定要做的!
    • 如果父进程不关掉,那么随着连接来的客户端的增多,父进程可用的文件描述符就会越来越少。
    • 父进程获取servicSock文件描述符是为了让子进程继承下去,自己是不用的,就不应该继续占着,如果不关闭,最后可能导致文件描述符泄漏的问题。

我们知道子进程退出之后就会进入僵尸状态!等待父进程回收!
那我们敢让父进程阻塞式等待吗,显然是不能!因为我们的目的是让服务器并发起来,现在还阻塞着。

如果用非阻塞等待WNOHANG,这是可以的,我们要所有子进程的PID保存起来,非阻塞等待的时候每一次都要轮询所有的子进程,但是比较麻烦。👉 进程等待复习 - 传送门

或者直接忽略SIGCHLD

// 不用等子进程了
// signal(SIGCHLD, SIG_IGN); // only Linux

忽略SIGCHLD,👉 复习传送门。

4.3 多进程1.1版本:

// 1.1 版本 -- 多进程版本  -- 这样写也是可以的
// 爷爷进程
pid_t id = fork();
if(id == 0)
{// 爸爸进程close(listenSock_);// 建议关掉// 又进行了一次fork,让 爸爸进程if(fork() > 0) exit(0);// 孙子进程 -- 就没有爸爸 -- 就变成了孤儿进程 -- 被系统领养 -- 孙子进程就交给了系统来回收transService(serviceSock, peerIp, peerPort);exit(0);
}// 父进程
close(serviceSock); // 这一步是一定要做的!
// 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
pid_t ret = waitpid(id, nullptr, 0); // 就用阻塞式
assert(ret > 0);
(void)ret;
  • 我们这里用到了 爷爷、爸爸、孙子 三个进程。
  • 爷爷进程创建爸爸进程,爸爸进程再创建孙子进程。
  • 只不过爸爸进程在创建完孙子进程之后直接就退出,由爷爷进程对其进行回收。
  • 将服务任务交由孙子进程去做。

孙子进程,没有了父进程,就变成了孤儿进程,被系统领养,孙子进程就交给了系统来回收,就不用我们来回收了。

子进程是从fork函数开始执行的。👉 复习传送门

(服务函数放在类内,类外都行)

4.4 多线程2.0版本:

因为我们是线程函数是设置在类内的方法,所以成员函数第一个参数是隐藏的this指针,我们要设置成静态的。
静态成员函数里要想获取到类内成员变量的话,还要搞一些获取类内成员的接口,我们直接将现这些数据封装一下:

// 先声明一下
class ServerTcp;class ThreadData
{
public:uint16_t clientPort_;std::string clinetIp_;int sock_;ServerTcp *this_;public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts): clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts){}
};

线程函数:

// 类内方法,形参默认带有this指针
static void *threadRoutine(void *args)
{pthread_detach(pthread_self()); // 设置线程分离ThreadData *td = static_cast<ThreadData*>(args);td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);delete td;return nullptr;
}

(此时服务函数放在了类里面)

// 2.0 版本 -- 多线程
ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// 不可进行线程等待,一等待,主线程就阻塞了,只能用线程分离
  • 这里不需要进行关闭文件描述符吗??不需要啦!!
  • 多线程是会共享文件描述符表的!

不可进行线程等待(pthread_join),一等待,主线程就阻塞了,只能用线程分离。

4.5 线程池3.0版本:

Task任务需要我们重写:

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"class Task
{
public:// 下面两个等价// typedef std::function<void (int, std::string, uint16_t)> callback_t;using callback_t = std::function<void (int, std::string, uint16_t)>;
public:Task():sock_(-1), port_(-1){}Task(int sock, std::string ip, uint16_t port, callback_t func): sock_(sock), ip_(ip), port_(port), func_(func){}void operator () (){logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\pthread_self(), ip_.c_str(), port_);func_(sock_, ip_, port_);logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\pthread_self(), ip_.c_str(), port_);}~Task(){}
private:int sock_;        // 给用户提供IO服务的sockuint16_t port_;   // client portstd::string ip_;  // client ipcallback_t func_; // 回调方法
};

交给线程池处理:

// 3.0 版本 -- 线程池
// transService服务在类外
Task t(serviceSock, peerIp, peerPort, transService);
tp_->push(t);

(服务函数放在类外)

我们在初始化服务器的方法的最后,加了一个启动线程池。👉 线程池 - 复习
还需要再loop函数循环之前,将线程池中的线程加载好。

我们将服务方法通过Task打包封装一下加载进线程池当中,然后Task有个仿函数里面就是调用回调函数。

在这里插入图片描述
之前我们在学C++11的时候,学过bind,我们这里可以用起来:

Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
tp_->push(t);

bind不熟悉的看过来,👉 复习传送门

(服务函数放在类内)

4.6 执行客户端指令:

popen函数:

在这里插入图片描述
第一件事情,创建管道,第二件事情,fork会自动帮我们创建子进程,让子进程去执行command代码,子进程执行完了之后,让父进程通过文件能够读到结果。

具体来说,popen函数会创建一个管道,其中写入端口(write end)被父进程保留,而读出端口(read end)被子进程保留。然后,popen函数调用fork创建一个新的子进程,该子进程会继承父进程的文件描述符,包括管道的读写端口。匿名管道用于在父进程和子进程之间进行双向通信。

void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char command[BUFFER_SIZE];while (true){ssize_t s = read(sock, command, sizeof(command) - 1); // 我们认为我们读到的都是字符串if (s > 0){command[s] = '\0';logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);// 考虑安全std::string safe = command;if ((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink"))){break;}// 我们是以r方式打开的文件,没有写入// 所以我们无法通过dup的方式得到对应的结果FILE *fp = popen(command, "r");if (fp == nullptr){logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));break;}char line[1024];while (fgets(line, sizeof(line) - 1, fp) != nullptr){write(sock, line, strlen(line));}pclose(fp);logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0, 代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里,一定是client退出了,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!logMessage(DEBUG, "server close %d done", sock);
}

同样的也是通过线程池的方式提供服务:

Task t(serviceSock, peerIp, peerPort, execCommand);
tp_->push(t);

(服务函数放在类外)

备注:

如果我们设置了对应的任务是死循环,那么线程池提供服务,就显得有不太合适了,我们应该给线程池抛入的任务是短任务。


5. 守护进程

5.1 进程组&&会话:

进程组:

进程组是一个或多个相关联的进程的集合。在Linux操作系统中,进程组是进程管理的一种机制,用于管理共享相同终端或终端会话的一组进程。

进程组组长:

  • 每个进程组都有一个组长进程(Group Leader Process),它的进程ID与进程组ID相同。
  • 组长进程可以通过调用setpgid()系统调用将其他进程加入到自己的进程组中。
  • 组长进程通常是第一个创建进程组的进程,或者通过调用setpgid()将其他进程加入到自己所在的进程组中的进程。
  • 组长进程拥有一些特殊的权限和责任,例如可以向整个进程组发送信号、管理进程组的终止状态等。* 同时,组长进程也有责任确保该进程组中的所有进程得到正确的处理和管理。

需要注意的是:

  • 一个进程可以同时属于多个进程组,但一个进程只能担任一个进程组的组长。
  • 当组长进程终止时,其所在的进程组中的所有进程都将收到SIGHUP(hangup)信号,除非它们已经忽略了该信号或者通过调用signal()函数将其重置。

会话:

当用户登录到Linux系统后,系统为其创建一个会话(由多个进程组构成),并为其分配一个唯一的会话标识符(Session ID)。这个会话可以持续到用户注销或与系统断开连接为止。

在这里插入图片描述
首进程:

  • 在一个会话(Session) 中,首进程(Session Leader)是创建该会话的进程。
  • 当一个新的会话被创建时,通常由一个特定的进程作为首进程。
  • 首进程负责创建并管理该会话中的其他进程。

首进程具有以下特点:

  1. 首进程是会话的领导者,它拥有该会话的控制权和权限。首进程的进程ID (PID) 与会话ID (SID) 相同。
  2. 首进程可以创建或终止会话中的其他进程,并对它们进行管理和控制。
  3. 首进程通常是用户登录系统后启动的shell进程(如Bash、Zsh、 Csh 等), 它会创建一个新的会话并成为该会话的首进程。
  4. 首进程还负责设置会话的相关属性,如控制终端、信号处理等。
  5. 如果首进程退出或终止,整个会话将结束,会话中的所有其他进程也会被终止。
  • 如果我们自己在新启进程或者启动进程组,一定是属于bash自己的会话。

需要注意的是:

  • 一个会话可以包含多个进程组,而首进程只是会话中的一个特定进程, 它并不一定是进程组的领导者。
  • 进程组的领导者可以通过调用setpgid()来改变自己所属的进程组,但这并不会影响首进程的身份和权限。

我们平时使用电脑卡的原因可能是,因为在本次登录过程中,起了很多个任务,这些任务都属于同一个会话,所以在卡的时候,进行注销操作,本质是将会话内部所有进程组全部删掉。

  • 任何时刻,只能有一个前台进程组,而且必须要有一个前台进程组,有0个或者多个后台进程组。

在这里插入图片描述
我们将一个任务启动到前台,bash命令行解释器自己就将自己投递到后台了,就没有办法接收输入了。所以我们再次输入指令时,就不会有任何响应。

会话&&进程组,举个栗子:

在这里插入图片描述
起了三个进程,其中第一个进程一般都是进程组的组长。SID是当前进程的会话ID。三个进程会话属于同一个,就是bash。

在这里插入图片描述
bash自己就是个进程,自己就是组长,就是会话当中的话首进程,自成一组。

当bash启动时,它会成为一个新的进程,并且作为会话的首进程。Bash进程的进程ID(PID)和进程组ID(PGID)会话ID(SID)通常是相同的。

5.2 引入守护进程:

我们提供的网络服务能不能属于bash这会话呢?

  • 比如在登录的状态新起了一个网络服务器,创建好之后,再派生子进程也属于当前会话。
  • 所以就不能让网络服务器属于当前会话内容,要不然会受会话的用户登录和注销的影响(不一定会退出)。

所有会话内的进程fork创建子进程,一般而言依旧属于当前会话!!

当我们登录时,以bash为首给我们构建一个会话,在bash命令中可以输入各种前台任务,或者取地址变成后台任务,变成后台进程。
后台进程任务依旧属于当前会话,登录登出可能会影响当前进程组。
起一个后台任务,退出,然后重新登录,这个后台任务可能还在,是会受到登录登出的影响的,所以要设置新的会话。

  • 当我们有网络服务的时候就应该脱离会话,让它独立在计算机里让其形成自己的新会话。

自成进程组,自成会话的周而复始运行的这一类进程,我们称之为:守护进程 or 精灵进程。

一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则一直会在运行。

5.3 实现:

在这里插入图片描述
谁调用这个函数,谁就成为一个新的会话,并且成为新会话内进程组的组长。

返回值:

在这里插入图片描述
哪个进程调用它,哪个进程的pid会被返回,失败就返回-1,并且错误码被设置。

但是有个要求,调用setsid的这个进程,不能是进程组的组长,进程组组长一调用setsid,调用就失败。

注意:

  • 必须调用一个函数setsid():将调用进程设置成为独立的会话。
  • 进程组的组长,不能调用setsid();
  • 我如何不成为组长呢?
    • 可以成为进程组内的第二个进程!
    • 常规做法,fork()子进程,子进程就不再是组长,就可以调用setsid()了。

管道:如果写端一直在写,读端关闭,写端会被终止,被信号终止SIGPIPE

  • 如果server给client写入,但是client已经关了,就相当于向一个不存在的文件描述符写入,那么此时会出现什么问题呢?
    • server也会收到SIGPIPE信号!!
  • 当进程收到SIGPIPE信号时,如果未对该信号进行处理,进程将以异常退出的形式终止。

所以,我们忽略掉SIGPIPE信号,当然这只是选做。

#pragma once#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void daemonize()
{int fd = 0;// 1. 忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);// 2. 更改进程的工作目录// chdir();// 3. 让自己不要成为进程组组长if (fork() > 0)exit(0); // 父进程退出了管都不用管,因为父进程有自己的父进程(bash)// 4. 设置自己是一个独立的会话 -- 不受登录退出的影响setsid();// 5. 重定向0, 1, 2if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3{dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 6. 关闭掉不需要的fd -- 别浪费了就关掉了if(fd > STDERR_FILENO)close(fd);}// 3, 4, 5这三步,是网络服务器写守护进程时,必写的三步// close(0,1,2);这种做法严重不推荐
}

重定向的原因:

daemonize();函数是在网络服务器启动之前调用的。

  • 那么serverTcp服务器里一定有大量的cout cerr打印日志的行为。
  • 我们调用setsid,创建一个会话,已经和终端没有关系了。
  • 一个会话绑定上终端(命令行界面),是一个字符式的模拟终端。
  • Linux中是bash进程和终端强关联,终端也是文件是被打开的,当前的进程就可以从终端里读,向终端里写入。
  • 一旦将进程转变为守护进程,它就会与终端断开关联。
  • 也就是说它不再与终端会话相关联,这意味着守护进程不再受用户登录或注销的影响,并且不会与用户直接交互。
  • 在setsid里就自动将进程和终端去关联了,去关联之后就不能使用cin cout cerr。
  • 因为0,1,2描述符已经和终端去关联了,后续程序一写就退出了。
  • 一旦重定向之后拦截输入输出,只是放在dev/nul/里丢弃掉了。
    • dev/nul/是Linux中的数据垃圾桶,或者叫信息黑洞。

在一些早期的Unix系统中,关闭0、1、2代表关闭了所有与终端相关的文件描述符,这样就成为了守护进程的一种惯例做法。但是在现代系统中,关闭0、1、 2是一种错误的实践,因为它们可能被其他进程使用,而且这些文件描述符对于守护进程的运行是非常重要的。这样可以避免程序在后续的执行中,因为无法访问标准流而出现问题,并确保它能够正常地向用户提供服务。

效果:

守护进程在命名时通常以d结尾。

在这里插入图片描述

5.4 守护进程化的剩余两种方法:

  • 第二种方法:

如果不想手动写守护进程,系统自带了函数接口:

在这里插入图片描述

  • 第三种方法:nohup:

在这里插入图片描述

  • 自己自成一个进程组,当前的会话依旧是属于3154,这个进程依旧是在本会话内部。
  • 形成的一个并非是守护进程,但是已经和守护进程是一样的了。
  • 虽然依就是属于3154这个会话,但是设置了nohup,就是不受用户登录和注销的影响了。

3154就是个bash:

在这里插入图片描述
退出后再登录:

在这里插入图片描述
此时已经是一个独立的会话了,成了一个孤儿进程。

相关文章:

【Linux】TCP的服务端(守护进程) + 客户端

文章目录 &#x1f4d6; 前言1. 服务端基本结构1.1 类成员变量&#xff1a;1.2 头文件1.3 初始化&#xff1a;1.3 - 1 全双工与半双工1.3 - 2 inet_aton1.3 - 3 listen 2. 服务端运行接口2.1 accept&#xff1a;2.2 服务接口&#xff1a; 3. 客户端3.1 connect&#xff1a;3.2 …...

1.7. 找出数组的第 K 大和原理及C++实现

题目 给你一个整数数组 nums 和一个 正 整数 k 。你可以选择数组的任一 子序列 并且对其全部元素求和。 数组的 第 k 大和 定义为&#xff1a;可以获得的第 k 个 最大 子序列和&#xff08;子序列和允许出现重复&#xff09; 返回数组的 第 k 大和 。 子序列是一个可以由其他数…...

基于微信小程序的付费自习室

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 文章目录 1 简介2 技术栈3 需求分析3.1用户需求分析3.1.1 学生用户3.1.3 管理员用户 4 数据库设计4.4.1 E…...

纪念在CSDN的2048天

时间真快&#xff5e;...

云原生Kubernetes:简化K8S应用部署工具Helm

目录 一、理论 1.HELM 2.部署HELM2 3.部署HELM3 二、实验 1.部署 HELM2 2.部署HELM3 三、问题 1.api版本过期 2.helm初始化报错 3.pod状态为ImagePullBackOff 4.helm 命令显示 no repositories to show 的错误 5.Helm安装报错 6.git命令报错 7.CentOS 7 下git c…...

qml保姆级教程五:视图组件

&#x1f482; 个人主页:pp不会算法v &#x1f91f; 版权: 本文由【pp不会算法v】原创、在CSDN首发、需要转载请联系博主 &#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 QML系列教程 QML教程一&#xff1a;布局组件 文章目录 列表视图ListVi…...

2310d编译不过

struct A {this(int[] data) safe { a data; }int[] a; }void main() safe {int[3] test [1, 2, 3];A a A(test); }应该给data参数加上return scope.或让构造器为模板参数来推导,否则,构造器可以把栈分配切片赋值给全局变量....

CleanMyMac X4.14.1最新版本下载

CleanMyMac X是一个功能强大的Mac清理软件&#xff0c;它的设计理念是提供多个模块&#xff0c;包括垃圾清理、安全保护、速度优化、应用程序管理和文档管理粉碎等&#xff0c;以满足用户的不同需求。软件的界面简洁直观&#xff0c;让用户能够轻松进行日常的清理操作。 使用C…...

芯驰D9评测(3)--建立开发环境

1. 建立交叉编译链接环境 官网下载的SDK包中就有交叉工具链&#xff0c;米尔提供的这个 SDK 中除了包含各种源代码外还提供了必要的交叉工具链&#xff0c;可以直接用于编译应用程序等。 用户可以直接使用次交叉编译工具链来建立一个独立的开发环境&#xff0c;可单独编译…...

阿里云服务器IP地址查询方法(公网IP和私网IP)

阿里云服务器IP地址在哪查看&#xff1f;在云服务器ECS管理控制台即可查看&#xff0c;阿里云服务器IP地址包括公网IP和私有IP地址&#xff0c;阿里云百科分享阿里云服务器IP地址查询方法&#xff1a; 目录 阿里云服务器IP地址查询 阿里云服务器IP地址查询 1、登录到阿里云服…...

第47节——使用bindActionCreators封装actions模块

一、什么是action creators 1、概念 在Redux中&#xff0c;Action Creators是一种函数&#xff0c;它用于创建一个描述应用程序状态变化的action对象。Action对象是一个普通JavaScript对象&#xff0c;它包含一个描述action类型的字符串属性&#xff08;通常称为“type”&…...

QT、c/c++通过宏自动判断平台

QT、c/c通过宏自动判断平台 Chapter1 QT、c/c通过宏自动判断平台 Chapter1 QT、c/c通过宏自动判断平台 原文链接&#xff1a;https://blog.csdn.net/qq_32348883/article/details/123063830 背景 为了更好的进行跨平台移植、编译、调试。 具体操作 宏操作 #ifdef _WIN32//d…...

对比表:阿里云轻量应用服务器和服务器性能差异

阿里云服务器ECS和轻量应用服务器有什么区别&#xff1f;轻量和ECS优缺点对比&#xff0c;云服务器ECS是明星级云产品&#xff0c;适合企业专业级的使用场景&#xff0c;轻量应用服务器是在ECS的基础上推出的轻量级云服务器&#xff0c;适合个人开发者单机应用访问量不高的网站…...

中国1km分辨率月最低温和最高温度数据集(1901-2020)

简介&#xff1a; 中国1km分辨率月最低温度数据集&#xff08;1901-2020&#xff09;是根据CRU发布的全球0.5气候数据集以及WorldClim发布的全球高分辨率气候数据集&#xff0c;通过Delta空间降尺度方案在中国地区降尺度生成的。使用了496个独立气象观测点数据进行验证&#x…...

EasyX图形库note4,动画及键盘交互

大家好&#xff0c;这里是Dark Flame Master&#xff0c;专栏从这篇开始就会变得很有意思&#xff0c;我们可以利用今天所学的只是实现很多功能&#xff0c;同样为之后的更加好玩的内容打下基础&#xff0c;从这届开始将会利用所学的知识制作一些小游戏&#xff0c;废话不多说&…...

C++设计模式-原型(Prototype)

目录 C设计模式-原型&#xff08;Prototype&#xff09; 一、意图 二、适用性 三、结构 四、参与者 五、代码 C设计模式-原型&#xff08;Prototype&#xff09; 一、意图 用原型实例指定创建对象的种类&#xff0c;并且通过拷贝这些原型创建新的对象。 二、适用性 当…...

[补题记录] Atcoder Beginner Contest 322(E)

URL&#xff1a;https://atcoder.jp/contests/abc322 目录 E Probelm/题意 Thought/思路 Code/代码 E Probelm/题意 有 N 个改进计划&#xff0c;每个计划可以执行一次&#xff1b;有 K 个参数&#xff0c;每个计划可以将所有参数提升固定值&#xff0c;即计划 i 可以为第…...

目标检测算法改进系列之Backbone替换为FocalNet

FocalNet 近些年&#xff0c;Transformers在自然语言处理、图像分类、目标检测和图像分割上均取得了较大的成功&#xff0c;归根结底是自注意力&#xff08;SA &#xff1a;self-attention&#xff09;起到了关键性的作用&#xff0c;因此能够支持输入信息的全局交互。但是由于…...

buuctf-[BSidesCF 2020]Had a bad day 文件包含

打开环境 就两个按钮&#xff0c;随便按按 url变了 还有 像文件包含&#xff0c;使用php伪协议读取一下&#xff0c;但是发现报错&#xff0c;而且有两个.php,可能是自己会加上php后缀 所以把后缀去掉 /index.php?categoryphp://filter/convert.base64-encode/resourcei…...

Elasticsearch:什么时候应该考虑在 Elasticsearch 中添加协调节点?

仅协调节点&#xff08;coordinating only nodes&#xff09;充当智能负载均衡器。 仅协调节点的这种特殊角色通过减轻数据和主节点的协调责任&#xff0c;为广泛的集群提供了优势。 加入集群后&#xff0c;这些节点与任何其他节点类似&#xff0c;都会获取完整的集群状态&…...

Dubbo3应用开发—Dubbo注册中心引言

Dubbo注册中心引言 什么是Dubbo注册中心 Dubbo的注册中心&#xff0c;是Dubbo服务治理的⼀个重要的概念&#xff0c;他主要用于 RPC服务集群实例的管理。 注册中心的运行流程 使用注册中心的好处 可以有效的管理RPC集群的健康情况&#xff0c;动态的上线或者下线服务。让我…...

AS环境,版本问题,android开发布局知识

项目模式下有一个build.gradle,每个模块也有自己的build.gradle Android模式下有多个build.gradle&#xff0c;汇总在一起。&#xff08;都会有标注是哪个模块下的&#xff09; C:\Users\Administrator\AndroidStudioProjects 项目默认位置 Java web项目与android项目的区别…...

OpenCV查找和绘制轮廓:findContours和drawContours

1 任务描述&#xff1a; 绘制图中粗线矩形的2个边界&#xff0c;并找到其边界的中心线 图1 原始图像 2.函数原型 findContours( InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, …...

毕设-原创医疗预约挂号平台分享

医疗预约挂号平台 不是尚医通项目&#xff0c;先看项目质量&#xff08;有源码论文&#xff09; 项目链接&#xff1a;医疗预约挂号平台git地址 演示视频&#xff1a;医疗预约挂号平台 功能结构图 登录注册模块&#xff1a;该模块具体分为登录和注册两个功能&#xff0c;这些…...

PLL锁相环倍频原理

晶振8MHz&#xff0c;但是处理器输入可以达到72MHz&#xff0c;是因为PLL锁相环提供了72MHz。 锁相环由PD&#xff08;鉴相器&#xff09;、LP&#xff08;滤波器&#xff09;、VCO&#xff08;压控振荡器&#xff09;组成。 处理器获得的72MHz并非晶振提供&#xff0c;而是锁…...

POJ 2886 Who Gets the Most Candies? 树状数组+二分

一、题目大意 我们有N个孩子&#xff0c;每个人带着一张卡片&#xff0c;一起顺时针围成一个圈来玩游戏&#xff0c;第一回合时&#xff0c;第k个孩子被淘汰&#xff0c;然后他说出他卡片上的数字A&#xff0c;如果A是一个正数&#xff0c;那么下一个回合他左边的第A个孩子被淘…...

阿里云服务器镜像系统Anolis OS龙蜥详细介绍

阿里云服务器Anolis OS镜像系统由龙蜥OpenAnolis社区推出&#xff0c;Anolis OS是CentOS 8 100%兼容替代版本&#xff0c;Anolis OS是完全开源、中立、开放的Linux发行版&#xff0c;具备企业级的稳定性、高性能、安全性和可靠性。目前阿里云服务器ECS可选的Anolis OS镜像系统版…...

数学建模Matlab之基础操作

作者由于后续课程也要学习Matlab&#xff0c;并且之前也进行了一些数学建模的练习&#xff08;虽然是论文手&#xff09;&#xff0c;所以花了几天零碎时间学习Matlab的基础操作&#xff0c;特此整理。 基本运算 a55 %加法&#xff0c;同理减法 b2^3 %立方 c5*2 %乘法 x 1; …...

[计算机入门] Windows附件程序介绍(工具类)

3.14 Windows附件程序介绍(工具类) 3.14.1 计算器 Windows系统中的计算器是一个内置的应用程序&#xff0c;提供了基本的数学计算功能。它被设计为一个方便、易于使用的工具&#xff0c;可以满足用户日常生活和工作中的基本计算需求。 以下是计算器程序的主要功能&#xff1a…...

队列(循环数组队列,用队列实现栈,用栈实现队列)

基础知识 队列(Queue):先进先出的数据结果,底层由双向链表实现 入队列:进行插入操作的一端称为队尾出队列:进行删除操作的一端称为对头 常用方法 boolean offer(E e) 入队 E(弹出元素的类型) poll() 出队 peek() 获取队头 int size 获取队列元素个数 boolean isEmpty(…...

建网站备案需要的材料/百度信息流推广平台

01 Deep-SORT-YOLOv4 使用Tensorflow进行人员检测和跟踪将YOLO v3替换成了YOLO v4&#xff0c;并添加了用于异步处理的选项&#xff0c;这大大提高了FPS。但是&#xff0c;使用异步处理时FPS监视将被禁用&#xff0c;因为它不准确。从本文中提取了算法&#xff0c;并将其实现到…...

给一个网站怎么做安全测试/地推app

一、 v-html指令1.1.说明假如我们从服务器请求回来的是一个HTML的代码片段如果我们想将html原样输出&#xff0c;就是用{{}}就可以了如果我们希望浏览器解析html后展示&#xff0c;那就要使用到v-html了1.2.怎么做{{zimug}}const app new Vue({el: #app,data: {zimug: 七维度工…...

做点效果图赚钱的网站/写文章免费的软件

input()、raw_input()都可以输入提示语 sys.stdin.readline()不能输入提示语 raw_input()可输入任何字符&#xff0c;enter后&#xff0c; 之前输入的任何字符包括空白符都被读取转换成字符串&#xff0c;但不会读取enter产生的换行符‘\n’ input()只能输入 “数字或者已经赋值…...

微信客户端小程序/站长工具seo查询软件

1&#xff0e; 什么是卡尔曼滤波器(What is the Kalman Filter?)在学习卡尔曼滤波器之前&#xff0c;首先看看为什么叫“卡尔曼”。跟其他著名的理论(例如傅立叶变换&#xff0c;泰勒级数等等)一样&#xff0c;卡尔曼也是一个人的名字&#xff0c;而跟他们不同的是&#xf…...

厦门市工程建设项目网上办事大厅/seo搜索引擎优化步骤

一、需要先使用Hbuilder打包出来h5的版本 二、点击下方的index.html 编辑 三、我目前以腾讯统计为例 毕竟腾讯统计没有广告 https://mta.qq.com/ 可在官网申请 复制统计代码 <script>var _mtac {};(function() {var mta document.createElement("script&quo…...

用dede做网站后台/昆明seo关键词

Android LocalActivityManager的用法 在开发中会碰到在一个activity中的局部&#xff08;或者是activity的Fragment中&#xff09;显示其他的activity 的内容&#xff0c;这时就用到了LocalActivityManager类。 假设这个容器是一个LinearLayout&#xff0c;id是linearContainer…...