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

简易TCP网络程序

目录

1. TCP 和 UDP 的基本区别

2. TCP 中的 listen、accept 和 connect

3. UDP 中的区别:没有 listen、accept 和 connect

4. 总结对比:

2.字符串回响

2.1.核心功能

2.2 代码展示

1. server.hpp 服务器头文件

2. server.cpp 服务器源文件

3. client.hpp 客户端头文件

4. client.cpp 客户端源文件

5. Makefile 文件

6. 服务器端初始化代码

7. 服务器端业务逻辑代码

8.客户端代码实现

3. 多进程版服务器实现

3.1 多进程版服务器的核心功能

1. 创建子进程处理连接

2. 服务器的工作流程

3.2 创建子进程

3.3 设置非阻塞

4. 多线程版服务器

4.1 核心功能

4.2 使用原生线程库

5.守护进程

5.1.会话、进程组、进程

5.2.守护进程化


前言:

当我们使用 TCPUDP 协议进行网络编程时,尽管都使用套接字(socket)进行通信,但它们之间存在一些重要的区别。特别是关于如何建立连接、如何处理客户端请求以及如何进行数据传输,TCP和UDP有着根本性的不同。以下是详细的对比,重点讨论 listenacceptconnect 等函数在 TCP 和 UDP 中的差异。

1. TCP 和 UDP 的基本区别

  • TCP(传输控制协议) 是面向连接的协议。在通信之前,客户端和服务器需要建立连接,确保可靠传输。

  • UDP(用户数据报协议) 是无连接的协议,不需要建立连接,也不保证数据传输的可靠性。每个数据包是独立的,不需要在发送前确认目标是否可达。

2. TCP 中的 listenacceptconnect
  • listen
    TCP 中,listen 函数是用来将服务器端的套接字设置为“监听状态”,它等待客户端的连接请求。这个函数需要在创建套接字并绑定端口后调用。listen 通常传入一个参数(backlog),它表示服务器端能够排队的连接请求的数量如果队列已满,新的连接请求会被拒绝。

    int listen(int sockfd, int backlog);
    • sockfd 是服务器端用于监听的套接字。

    • backlog 是连接请求的队列长度。

  • accept
    acceptTCP 中用于接受客户端连接请求的函数。当客户端请求连接时,accept 函数会阻塞,直到有客户端请求到来,且成功建立连接。accept 返回一个新的套接字,这个新的套接字用于和客户端进行通信。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • sockfd 是服务器端监听套接字。

    • addr 是指向客户端地址结构的指针(可以用来获取客户端的IP和端口)。

    • addrlen 是地址结构的大小。

  • connect
    connect 是 TCP 中客户端用来请求与服务器建立连接的函数。客户端使用 connect 函数连接到服务器的 IP 地址和端口,建立连接后,客户端就可以与服务器进行通信。此函数是阻塞的,直到连接成功或超时。

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • sockfd 是客户端的套接字。

    • addr 是服务器的地址信息(IP 和端口)。

    • addrlen 是地址结构的大小。

3. UDP 中的区别:没有 listenacceptconnect
  • listenaccept 在 UDP 中没有意义
    UDP 是无连接的,不需要等待连接或接受连接请求。每个数据包(数据报)都是独立的,发送方和接收方不需要在发送数据之前进行握手或连接确认。因此,UDP 协议中没有 listenaccept 函数。客户端可以直接使用 sendtorecvfrom 来发送和接收数据。

  • connect 在 UDP 中的作用
    虽然 UDP 是无连接的协议,但是在实际应用中,connect 也可以在 UDP 中使用,但它的作用与 TCP 不同。通过 connect,UDP 套接字可以绑定一个目标地址,这样就不需要每次发送数据时都指定目标地址了connect 后,发送和接收数据时,默认的目标地址就是连接时指定的地址。

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • sockfd 是客户端的套接字。

    • addr 是服务器的地址信息。

    • addrlen 是地址结构的大小。

    但值得注意的是,UDP 套接字与 TCP 套接字不同,使用 connect 后,依然是无连接的,即发送和接收数据不需要三次握手和连接管理。

4. 总结对比:
函数TCPUDP
listen服务器使用,设置为监听状态,准备接收连接请求。没有类似的功能,UDP 无连接。
accept服务器使用,接收连接请求并返回一个新的套接字用于通信。没有类似的功能,UDP 无连接。
connect客户端使用,主动与服务器建立连接。可用来指定目标地址,之后的通信会自动使用这个地址,但不需要建立连接。

2.字符串回响

2.1.核心功能

字符串回响程序类似于 echo 指令,客户端向服务器发送消息,服务器在收到消息后会将消息发送给客户端,该程序实现起来比较简单,同时能很好的体现 socket 套接字编程的流程

2.2 代码展示
1. server.hpp 服务器头文件
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "error_codes.hpp"namespace server_namespace
{const uint16_t DEFAULT_PORT = 8888; // 默认端口号class TCPServer{public:TCPServer(const uint16_t port = DEFAULT_PORT): port_(port){}~TCPServer() {}void initializeServer();void runServer();private:int serverSocket_; // 套接字uint16_t port_;    // 端口号};
}
2. server.cpp 服务器源文件
#include <memory>
#include "server.hpp"using namespace std;
using namespace server_namespace;int main()
{unique_ptr<TCPServer> serverInstance(new TCPServer());serverInstance->initializeServer();serverInstance->runServer();return 0;
}
3. client.hpp 客户端头文件
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "error_codes.hpp"namespace client_namespace
{class TCPClient{public:TCPClient(const std::string& serverIP, const uint16_t port): serverIP_(serverIP), serverPort_(port){}~TCPClient() {}void initializeClient();void startClient();private:int clientSocket_; // 套接字std::string serverIP_; // 服务器IP地址uint16_t serverPort_; // 服务器端口号};
}
4. client.cpp 客户端源文件
#include <memory>
#include "client.hpp"using namespace std;
using namespace client_namespace;void showUsage(const char *program)
{cout << "Usage:" << endl;cout << "\t" << program << " ServerIP ServerPort" << endl;
}int main(int argc, char *argv[])
{if (argc != 3){showUsage(argv[0]);return USAGE_ERROR;}string ip(argv[1]);uint16_t port = stoi(argv[2]);unique_ptr<TCPClient> clientInstance(new TCPClient(ip, port));clientInstance->initializeClient();clientInstance->startClient();return 0;
}
5. Makefile 文件
.PHONY: all
all: server clientserver: server.cppg++ -o $@ $^ -std=c++11client: client.cppg++ -o $@ $^ -std=c++11.PHONY: clean
clean:rm -rf server client
6. 服务器端初始化代码
void TCPServer::initializeServer()
{// 创建套接字serverSocket_ = socket(AF_INET, SOCK_STREAM, 0);if (serverSocket_ == -1){std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;exit(SOCKET_ERROR);}std::cout << "Socket created successfully: " << serverSocket_ << std::endl;// 绑定IP地址与端口号struct sockaddr_in serverAddr;memset(&serverAddr, 0, sizeof(serverAddr)); // 清零serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址serverAddr.sin_port = htons(port_);if (bind(serverSocket_, (const sockaddr*)&serverAddr, sizeof(serverAddr)) < 0){std::cerr << "Binding IP and Port failed: " << strerror(errno) << std::endl;exit(BIND_ERROR);}// 监听连接if (listen(serverSocket_, 32) < 0){std::cerr << "Listen failed: " << strerror(errno) << std::endl;exit(LISTEN_ERROR);}std::cout << "Server is now listening on port " << port_ << std::endl;
}
7. 服务器端业务逻辑代码
void TCPServer::runServer()
{while (true){// 接受客户端连接请求struct sockaddr_in clientAddr;socklen_t clientLen = sizeof(clientAddr);int clientSocket = accept(serverSocket_, (struct sockaddr*)&clientAddr, &clientLen);if (clientSocket < 0){std::cerr << "Accept failed: " << strerror(errno) << std::endl;continue;}std::string clientIP = inet_ntoa(clientAddr.sin_addr);uint16_t clientPort = ntohs(clientAddr.sin_port);std::cout << "Accepted connection from " << clientIP << ":" << clientPort << std::endl;// 处理客户端请求handleClientRequest(clientSocket, clientIP, clientPort);}
}void TCPServer::handleClientRequest(int clientSocket, const std::string& clientIP, uint16_t clientPort)
{char buffer[1024];std::string clientInfo = clientIP + ":" + std::to_string(clientPort);while (true){ssize_t bytesRead = read(clientSocket, buffer, sizeof(buffer) - 1);if (bytesRead > 0){buffer[bytesRead] = '\0';std::cout << "Received from " << clientInfo << ": " << buffer << std::endl;write(clientSocket, buffer, bytesRead);  // 回显}else if (bytesRead == 0){std::cout << "Client " << clientInfo << " disconnected" << std::endl;close(clientSocket);break;}else{std::cerr << "Read failed: " << strerror(errno) << std::endl;close(clientSocket);break;}}
}
8.客户端代码实现
void TCPClient::initializeClient()
{clientSocket_ = socket(AF_INET, SOCK_STREAM, 0);if (clientSocket_ == -1){std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;exit(SOCKET_ERROR);}std::cout << "Client socket created successfully: " << clientSocket_ << std::endl;
}void TCPClient::startClient()
{struct sockaddr_in serverAddr;memset(&serverAddr, 0, sizeof(serverAddr)); // 清零serverAddr.sin_family = AF_INET;inet_aton(serverIP_.c_str(), &serverAddr.sin_addr);serverAddr.sin_port = htons(serverPort_);if (connect(clientSocket_, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0){std::cerr << "Connection failed: " << strerror(errno) << std::endl;exit(CONNECT_ERROR);}std::cout << "Connected to server " << serverIP_ << ":" << serverPort_ << std::endl;// 通信部分char buffer[1024];while (true){std::string msg;std::cout << "Enter message to send: ";std::getline(std::cin, msg);write(clientSocket_, msg.c_str(), msg.size());ssize_t bytesRead = read(clientSocket_, buffer, sizeof(buffer) - 1);if (bytesRead > 0){buffer[bytesRead] = '\0';std::cout << "Received from server: " << buffer << std::endl;}else{std::cerr << "Connection closed or error in reading data!" << std::endl;close(clientSocket_);break;}}
}

3. 多进程版服务器实现

在之前的字符串回响程序中,如果只有一个客户端与服务器通信,程序是可以正常工作的。然而,如果有多个客户端发起连接请求,服务器就无法应对,因为服务器是单进程的,它只能处理一个客户端的请求,必须等待当前请求完成后才能处理下一个。这是由于服务器的处理是串行的。

为了处理多个客户端的连接请求,服务器需要能够同时处理多个连接。我们可以通过使用 多进程 或 多线程 来实现这一目标。在这里,我们采用 多进程 方案。具体来说,每当服务器成功处理一个连接请求后,它就会使用 fork() 创建一个子进程,负责与客户端的通信,而父进程继续监听其他客户端的连接请求。

3.1 多进程版服务器的核心功能
1. 创建子进程处理连接
  • 使用 fork() 创建子进程。

  • 父进程负责接受连接请求。

  • 子进程负责处理每个连接的业务逻辑。

2. 服务器的工作流程
  • 监听端口并接受连接请求。

  • 每当有新的客户端连接,父进程会通过 fork() 创建一个新的子进程处理该连接。

  • 子进程完成通信后退出,而父进程继续接收新的连接请求。

3.2 创建子进程

我们使用 fork() 函数来创建子进程。fork() 的返回值可以帮助我们区分父进程和子进程:

  • fork() 返回值为 0:表示当前是子进程,子进程将执行处理客户端请求的逻辑。

  • fork() 返回值大于 0:表示当前是父进程,父进程将继续处理其他客户端的连接请求。

  • fork() 返回值小于 0:表示子进程创建失败。

示例代码:创建子进程处理请求

// 进程创建、等待所需要的头文件
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>// 启动服务器
void StartServer()
{while (!quit_){// 1.处理连接请求struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listensock_, (struct sockaddr *)&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;}else if(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!";}}
}
3.3 设置非阻塞

在多进程模式下,父进程需要等待每个子进程的退出,这样会导致父进程阻塞在 waitpid() 函数上。为了避免这种情况,我们可以通过不同的方式设置父进程为非阻塞模式。

方式一:通过 WNOHANG 设置非阻塞等待

通过 waitpid() 的第三个参数 WNOHANG 来设置父进程非阻塞。

pid_t ret = waitpid(id, nullptr, WNOHANG); // 设置为非阻塞等待

但是这种方式虽然能避免阻塞,但仍然存在资源泄漏的问题,因为父进程可能一直处于阻塞状态。

方式二:忽略 SIGCHLD 信号(推荐)

SIGCHLD 是子进程结束时向父进程发送的信号。我们可以通过在父进程中忽略 SIGCHLD 信号,让操作系统自动回收子进程,这样就不需要父进程等待子进程退出了。

#include <signal.h> // 信号处理相关头文件// 启动服务器
void StartServer()
{// 忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN);while (!quit_){// 1.处理连接请求struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listensock_, (struct sockaddr *)&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;}else if(id == 0){// 子进程内close(listensock_); // 子进程不需要监听(建议关闭)// 执行业务处理函数Service(sock, clientip, clientport);exit(0); // 子进程退出}close(sock); // 父进程不再需要资源(必须关闭)}
}

此方法是推荐的,因为它简单且不会导致僵尸进程。

总结与优化

  1. 父进程与子进程的职责分离

    • 父进程负责监听并接受连接请求。

    • 每当父进程接收到一个客户端的连接请求,它将创建一个子进程来处理与该客户端的通信。

    • 父进程不需要等待子进程退出,而是继续接收新的连接请求。

  2. 避免资源泄漏

    • 子进程处理完客户端的请求后,应该尽快退出,避免资源泄漏。

    • 父进程应及时关闭不再使用的资源,避免文件描述符泄漏。

  3. 非阻塞等待机制

    • 使用 SIGCHLD 信号忽略子进程的退出,可以避免父进程被阻塞,同时确保操作系统能够回收子进程的资源。

4. 多线程版服务器

4.1 核心功能

通过多线程,服务器能够同时处理多个客户端的请求。每当服务器与客户端成功建立连接时,服务器会创建一个线程,专门处理该客户端的业务逻辑。多线程方式相比多进程方式更高效,因为线程间共享内存资源,开销较小。

4.2 使用原生线程库

原生线程库提供了直接使用线程的方式,通pthread,我们可以创建、管理线程,并对其进行同步。

创建线程数据结构

为了在线程中执行业务处理函数,我们需要将连接的套接字、客户端IP和端口号等信息传递给线程。由于线程的回调函数只能接受一个 void* 类型的参数,我们可以创建一个 ThreadData 类来保存这些信息。

class ThreadData {
public:ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr): sock_(sock), clientip_(ip), clientport_(port), current_(ptr) {}public:int sock_;std::string clientip_;uint16_t clientport_;TcpServer* current_; // 指向 TcpServer 对象的指针
};

线程回调函数

线程的回调函数需要是静态函数,因为它不可以访问非静态成员。我们可以使用 static void* Routine(void* args) 作为线程回调函数

// 线程回调函数
static void* Routine(void* args) {pthread_detach(pthread_self());  // 分离线程,避免阻塞ThreadData* td = static_cast<ThreadData*>(args);// 调用业务处理函数td->current_->Service(td->sock_, td->clientip_, td->clientport_);delete td;  // 释放资源
}

服务器类的修改

StartServer() 中,我们通过 pthread_create 创建线程,每个线程处理一个连接请求。

void StartServer() {while (!quit_) {// 1. 处理连接请求struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listensock_, (struct sockaddr *)&client, &len);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;// 创建线程数据并启动线程ThreadData* td = new ThreadData(sock, clientip, clientport, this);pthread_t p;pthread_create(&p, nullptr, Routine, td);  // 创建线程}
}

资源管理

通过 pthread_detach(),线程结束后会自动清理资源,避免造成内存泄漏。我们不需要显式地等待线程结束。

Makefile 修改

由于我们使用了 pthread 库,编译时需要链接该库,添加 -lpthread 参数:

.PHONY: all
all: server clientserver: server.ccg++ -o $@ $^ -std=c++11 -lpthreadclient: client.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY: clean
clean:rm -rf server client
完整代码示例:
// server.hpp 服务器头文件
#pragma once#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 {const uint16_t default_port = 8888;  // 默认端口号const int backlog = 32;              // 全连接队列的最大长度using func_t = std::function<std::string(std::string)>; // 回调函数类型class TcpServer;  // 前置声明class ThreadData {public:ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr): sock_(sock), clientip_(ip), clientport_(port), current_(ptr) {}public:int sock_;std::string clientip_;uint16_t clientport_;TcpServer* current_;  // 指向 TcpServer 对象的指针};class TcpServer {public:TcpServer(const func_t &func, const uint16_t port = default_port): func_(func), port_(port), quit_(false) {}~TcpServer() {}// 初始化服务器void InitServer() {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;struct sockaddr_in local;memset(&local, 0, sizeof(local));  // 清零local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;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);}if (listen(listensock_, backlog) == -1) {std::cerr << "Listen Fail!" << strerror(errno) << std::endl;exit(LISTEN_ERR);}std::cout << "Listen Success!" << std::endl;}// 启动服务器void StartServer() {while (!quit_) {struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listensock_, (struct sockaddr *)&client, &len);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;// 创建线程数据并启动线程ThreadData* td = new ThreadData(sock, clientip, clientport, this);pthread_t p;pthread_create(&p, nullptr, Routine, td);}}// 线程回调函数static void* Routine(void* args) {pthread_detach(pthread_self());  // 分离线程,避免阻塞ThreadData* td = static_cast<ThreadData*>(args);td->current_->Service(td->sock_, td->clientip_, td->clientport_);delete td;}// 业务处理void Service(int sock, const std::string& clientip, const uint16_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));} else if (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_;     // 回调函数};
}

多线程服务器的总结

  • 多线程处理:每个客户端连接由一个线程处理,线程间共享资源,可以提高服务器的并发能力。

  • 线程回调函数:通过传递 ThreadData 对象给线程,在线程中执行实际的业务处理。

  • 线程分离:使用 pthread_detach() 来确保线程结束时资源能够自动清理,避免造成资源泄漏。

通过以上多线程实现,我们的服务器能够高效地处理多个客户端的并发连接,并且能够充分利用 CPU 资源。


5.守护进程

5.1.会话、进程组、进程

接下来进入本文中的最后一个小节: 守护进程

守护进程 的意思就是让进程不间断的在后台运行,即便是 bash 关闭了,也能照旧运行。守护进程 就是现实生活中的服务器,因为服务器是需要 24H 不间断运行的。

当前我们的程序在启动后属于 前台进程,前台进程 是由 bash 进程替换而来的,因此会导致 bash 暂时无法使用

如果在启动程序时,带上 & 符号,程序就会变成 后台进程后台进程 并不会与 bash 进程冲突,bash 仍然可以使用

后台进程 也可以实现服务器不间断运行,但问题在于 如果当前 bash 关闭了,那么运行中的后台进程也会被关闭,最好的解决方案是使用 守护进程

在正式学习 守护进程 之前,需要先了解一组概念:会话、进程组、进程

分别运行一批 前台、后台进程,并通过指令查看进程运行情况

sleep 1000 | sleep 2000 | sleep 3000 &sleep 100 | sleep 200 | sleep 300ps -ajx | head -1 && ps -ajx | grep sleep | grep -v grep

其中 会话 <-> 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

在同一个会话中,只允许一个前台进程在运行,默认是 bash,如果其他进程运行了,bash 就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)

如何将一个 后台进程 变成 前台进程?

首先通过指令查看当前 会话 中正在运行的 后台进程,获取 任务号

jobs

接下来通过 任务号 将 后台进程 变成 前台进程,此时 bash 就无法使用了

fg 1

那如何将 前台进程 变成 后台进程 ?

首先是通过 ctrl + z 发送 19 号 SIGSTOP 信号,暂停正在运行中的 前台进程

键盘输入 ctrl + z

然后通过 任务号,可以把暂停中的进程变成 后台进程

bg 1

5.2.守护进程化

一般网络服务器为了不受到用户登录重启的影响,会以 守护进程 的形式运行,有了上面那一批前置知识后,就可以很好的理解 守护进程 的本质了

守护进程:进程单独成一个会话,并且以后台进程的形式运行

说白了就是让服务器不间断运行,可以直接使用 daemon() 函数完成 守护进程化

#include <unistd.h>int daemon(int nochdir, int noclose);

参数解读:

  1. nochdir 改变进程的工作路径
  2. noclose 重定向标准输入、标准输出、标准错误

返回值:成功返回 0,失败返回 -1

一般情况下,daemon() 函数的两个参数都只需要传递 0默认工作在 / 路径下,默认重定向至 /dev/null

/dev/null 就像是一个 黑洞,可以把所有数据都丢入其中,相当于丢弃数据

使用 damon() 函数使之前的server.cc 守护进程化

server.cc 服务器源文件

#include <memory> // 智能指针头文件
#include <string>
#include <unistd.h>
#include "server.hpp"using namespace std;
using namespace nt_server;// 业务处理回调函数(字符串回响)
string echo(string request)
{return request;
}int main()
{// 直接守护进程化daemon(0, 0);unique_ptr<TcpServer> usvr (new TcpServer(echo)); // 将回调函数进行传递usvr->InitServer();usvr->StartServer();return 0;
}

现在服务器启动后,会自动变成 后台进程,并且自成一个 新会话,归操作系统管(守护进程 本质上是一种比较坚强的 孤儿进程

注意: 现在标准输出、标准错误都被重定向至 /dev/null 中了,之前向屏幕输出的数据,现在都会直接被丢弃,如果想保存数据,可以选择使用日志

如果想终止 守护进程,需要通过 kill pid 杀死目标进程

使用系统提供的接口一键 守护进程化 固然方便,不过大多数程序员都会选择手动 守护进程化(可以根据自己的需求定制操作)

原理是 使用 setsid() 函数新设一个会话,谁调用,会话 SID 就是谁的,成为一个新的会话后,不会被之前的会话影响

#include <unistd.h>pid_t setsid(void);

返回值:成功返回该进程的 pid,失败返回 -1

注意: 调用该函数的进程,不能是组长进程,需要创建子进程后调用

手动实现守护进程时需要注意以下几点:

  1. 忽略异常信号
  2. 0、1、2 要做特殊处理(文件描述符)
  3. 进程的工作路径可能要改变(从用户目录中脱离至根目录)

具体实现步骤如下:

1、忽略常见的异常信号:SIGPIPE、SIGCHLD

2、如何保证自己不是组长? 创建子进程 ,成功后父进程退出,子进程变成守护进程

3、新建会话,自己成为会话的 话首进程

4、(可选)更改守护进程的工作路径:chdir

5、处理后续对于 0、1、2 的问题

对于 标准输入、标准输出、标准错误 的处理方式有两种

暴力处理:直接关闭 fd

优雅处理:将 fd 重定向至 /dev/null,也就是 daemon() 函数的做法

这里我们选择后者,守护进程 的函数实现如下

Daemon.hpp 守护进程头文件

#pragma once#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"static const char *path = "/home/Yohifo";void Daemon()
{// 1、忽略常见信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2、创建子进程,自己退休pid_t id = fork();if (id > 0)exit(0);else if (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);
}

现在服务器在启动后,会自动新建会话,以 守护进程 的形式运行

关于 inet_ntoa 函数的返回值(该函数的作用是将四字节的 IP 地址转化为点分十进制的 IP 地址)
inet_ntoa 返回值为 char*,转化后的 IP 地址存储在静态区,二次调用会覆盖上一次的结果,多线程场景中不是线程安全的

不过在 CentOS 7 及更高版本中,接口进行了更新,新增了互斥锁,多线程场景中测试没问题

http://www.xdnf.cn/news/1396531.html

相关文章:

  • 250830-Docker从Rootless到Rootful的Gitlab镜像迁移
  • 【Linux】网络安全管理:Netfilter、nftables 与 Firewalld | Redhat
  • Pmp项目管理方法介绍|权威详解与实战指南
  • 【超全汇总】MySQL服务启动命令手册(Linux+Windows+macOS)(上)
  • MYSQL速通(3/5)
  • Linux 830 shell:expect,ss -ant ,while IFS=read -r line,
  • 构建AI智能体:十八、解密LangChain中的RAG架构:让AI模型突破局限学会“翻书”答题
  • Python自定义函数形式参中的*args、**kwargs、*和/
  • STM32G474 IAP 双bank升级的坑
  • WebStorm无法识别@下的文件,但是可以正常使用
  • 【后端数据库】MySQL 索引生效/失效规则 + 核心原理
  • 腾讯云OpenCloudOS 9系统部署OpenTenBase数据库详细教程
  • 【云原生】Docker 搭建Kafka服务两种方式实战操作详解
  • php连接rabbitmq例子
  • 【序列晋升】21 Spring Cloud Gateway 云原生网关演进之路
  • 卷积神经网络项目:基于CNN实现心律失常(ECG)的小颗粒度分类系统
  • HAProxy 负载均衡全解析:从基础部署、负载策略到会话保持及性能优化指南
  • docker命令(二)
  • 现状摸底:如何快速诊断企业的“数字化健康度”?
  • PCIe 6.0 TLP深度解析:从结构设计到错误处理的全链路机制
  • 算法题(194):字典树
  • 从0到1玩转 Google SEO
  • Suno-API - OpenI
  • “FAQ + AI”智能助手全栈实现方案
  • Python从入门到高手9.4节-基于字典树的敏感词识别算法
  • 8月29日星期五今日早报简报微语报早读
  • 轮廓周长,面积,外接圆,外接矩形近似轮廓和模板匹配和argparse模块实现代码参数的动态配置
  • 【C++】掌握类模板:多参数实战技巧
  • 基于Net海洋生态环境保护系统的设计与实现(代码+数据库+LW)
  • MYSQL速通(2/5)