簡易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.pswp.cn/pingmian/95351.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/95351.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/95351.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

廣電手機卡到底好不好?

中國廣電于2020年與中國移動簽署了戰略合作協議&#xff0c;雙方在5G基站建設方面實現了共建共享。直到2022年下半年&#xff0c;中國廣電才正式進入號卡服務領域&#xff0c;成為新晉運營商。雖然在三年的時間內其發展速度較快&#xff0c;但對于消費者而言&#xff0c;廣電的…

Git中批量恢復文件到之前提交狀態

<摘要> Git中批量恢復文件到之前提交狀態的核心命令是git checkout、git reset和git restore。根據文件是否已暫存&#xff08;git add&#xff09;&#xff0c;需采用不同方案&#xff1a;未暫存變更用git checkout -- <file>或git restore <file>丟棄修改&…

UniApp 基礎開發第一步:HBuilderX 安裝與環境配置

UniApp 是一個基于 Vue.js 的跨平臺開發框架&#xff0c;支持快速構建小程序、H5、App 等應用。作為開發的第一步&#xff0c;正確安裝和配置 HBuilderX&#xff08;官方推薦的 IDE&#xff09;是至關重要的。下面我將以清晰步驟引導您完成整個過程&#xff0c;確保環境可用。整…

華為云Stack Deploy安裝(VMware workstation物理部署)

1.1 華為云Stack Deploy安裝(VMware workstation物理部署) 步驟 1 安裝軟件及環境準備 HUAWEI_CLOUD_Stack_Deploy_8.1.1-X86_64.iso HCSD安裝鏡像 VMware workstation軟件 VirtualBox安裝包 步驟2 修改VMware workstation網絡模式 打開VMware workstation軟件,點“編輯”…

安全等保復習筆記

信息安全概述1.2 信息安全的脆弱性及常見安全攻擊 ? 網絡環境的開放性物理層--物理攻擊 ? 物理設備破壞 ? 指攻擊者直接破壞網絡的各種物理設施&#xff0c;比如服務器設施&#xff0c;或者網絡的傳輸通信設施等 ? 設備破壞攻擊的目的主要是為了中斷網絡服務 ? 物理設備竊…

【Audio】切換至靜音或振動模式時媒體音自動置 0

一、問題描述 基于 Android 14平臺&#xff0c;AudioService 中當用戶切換到靜音模式&#xff08;RINGER_MODE_SILENT&#xff09;或振動模式&#xff08;RINGER_MODE_VIBRATE&#xff09;時會自動將響鈴和通知音量置0&#xff0c;當切換成響鈴模式&#xff08;RINGER_MODE_NO…

VPS云服務器安全加固指南:從入門到精通的全面防護策略

在數字化時代&#xff0c; VPS云服務器已成為企業及個人用戶的重要基礎設施。隨著網絡攻擊手段的不斷升級&#xff0c;如何有效進行VPS安全加固成為每個管理員必須掌握的技能。本文將系統性地介紹從基礎配置到高級防護的完整安全方案&#xff0c;幫助您構建銅墻鐵壁般的云服務器…

Mysql雜志(八)

游標游標是MySQL中一種重要的數據庫操作機制&#xff0c;它解決了SQL集合操作與逐行處理之間的矛盾。這個相信大家基本上都怎么使用過&#xff0c;這個都是建立在使用存儲過程的基礎上的。我們都知道SQL都是批量處理的也就是面向集合操作&#xff08;一次操作多行&#xff09;&…

Dify 從入門到精通(第 71/100 篇):Dify 的實時流式處理(高級篇)

Dify 從入門到精通&#xff08;第 71/100 篇&#xff09;&#xff1a;Dify 的實時流式處理 Dify 入門到精通系列文章目錄 第一篇《Dify 究竟是什么&#xff1f;真能開啟低代碼 AI 應用開發的未來&#xff1f;》介紹了 Dify 的定位與優勢第二篇《Dify 的核心組件&#xff1a;從…

日志分析與安全數據上傳腳本

最近在學習計算機網絡&#xff0c;想著跟python結合做一些事情。這段代碼是一個自動化腳本&#xff0c;它主要有三個功能&#xff1a;分析日志&#xff1a; 它從你指定的日志文件中讀取內容&#xff0c;并篩選出所有包含特定關鍵字的行。網絡交互&#xff1a; 它將篩選出的數據…

【論文閱讀】Sparse4D v3:Advancing End-to-End 3D Detection and Tracking

標題&#xff1a;Sparse4D v3&#xff1a;Advancing End-to-End 3D Detection and Tracking 作者&#xff1a;Xuewu Lin, Zixiang Pei, Tianwei Lin, Lichao Huang, Zhizhong Su motivation 作者覺得做自動駕駛&#xff0c;還需要跟蹤。于是更深入的把3D-檢測&跟蹤用sparse…

基于 DNA 的原核生物與微小真核生物分類學:分子革命下的范式重構?

李升偉 李昱均 茅 矛&#xff08;特趣生物科技公司&#xff0c;email: 1298261062qq.com&#xff09;傳統微生物分類學長期依賴形態特征和生理生化特性&#xff0c;這在原核生物和微小真核生物研究中面臨巨大挑戰。原核生物形態簡單且表型可塑性強&#xff0c;微小真核生物…

【FastDDS】Layer DDS之Domain (01-overview)

Fast DDS 域&#xff08;Domain&#xff09;模塊詳解 一、域&#xff08;Domain&#xff09;概述 域代表一個獨立的通信平面&#xff0c;能在共享通用通信基礎設施的實體&#xff08;Entities&#xff09;之間建立邏輯隔離。從概念層面來看&#xff0c;域可視為一個虛擬網絡&am…

http和https區別是什么

區別主要有以下四點&#xff1a;HTTP 是超文本傳輸協議&#xff0c;信息是明文傳輸&#xff0c;存在安全風險的問題。HTTPS 則解決 HTTP 不安全的缺陷&#xff0c;在 TCP 和 HTTP 網絡層之間加入了 SSL/TLS 安全協議&#xff0c;使得報文能夠加密傳輸。HTTP 連接建立相對簡單&a…

推薦算法發展歷史

推薦算法的發展歷史是一部從簡單規則到復雜智能&#xff0c;從宏觀群體推薦到微觀個性化精準推薦的 演進史。它大致可以分為以下幾個階段&#xff1a;推薦算法的發展歷史是一部從簡單規則到復雜智能&#xff0c;從宏觀群體推薦到微觀個性化精準推薦的演進史。它大致可以分為以下…

企業DevOps的安全與合規關鍵:三大主流DevOps平臺能力對比

在數字化轉型的浪潮中&#xff0c;DevOps平臺已成為企業加速軟件交付、提升協作效率的核心引擎。然而&#xff0c;隨著應用范圍的擴大&#xff0c;安全漏洞與合規風險也隨之凸顯。如何平衡速度與安全&#xff0c;實現高效且合規的DevOps流程&#xff0c;已成為企業亟需解決的關…

pgroll:簡化PostgreSQL零停機遷移

pgroll&#xff1a;PostgreSQL零停機遷移的新思路作為后端開發者&#xff0c;我們都遇到過數據庫變更的難題。想象一下&#xff0c;你需要在電商大促期間修改用戶表結構——傳統的ALTER TABLE可能導致鎖表&#xff0c;用戶下單流程中斷&#xff0c;每分鐘都是真金白銀的損失。p…

JVM1.8與1.9的區別是什么?

一、核心機制變化 類加載器調整 JDK 1.8&#xff1a;使用三種類加載器&#xff1a; 啟動類加載器&#xff08;Bootstrap&#xff09;&#xff1a;加載核心類庫&#xff08;如 rt.jar&#xff09;。擴展類加載器&#xff08;ExtClassLoader&#xff09;&#xff1a;加載 JAVA_HO…

CentOS交換區處理

文章目錄前言創建交換文件&#xff08;推薦&#xff09;清理舊交換區前言 很多剛開始使用 CentOS 的用戶都會遇到。1GB 的交換分區在現代應用環境下確實偏小&#xff0c;很容易在內存壓力大時導致系統性能下降甚至應用程序被強制終止。 關于交換分區的大小&#xff0c;沒有一…

JavaScript原型與原型鏈:對象的家族傳承系統

文章目錄JavaScript原型與原型鏈&#xff1a;對象的"家族傳承"系統 &#x1f468;&#x1f469;&#x1f467;&#x1f466;引言&#xff1a;為什么需要原型&#xff1f;原型系統三大核心概念概念關系圖核心概念表一、原型基礎&#xff1a;對象如何"繼承"屬…