目录
2. TCP 中的 listen、accept 和 connect
3. UDP 中的区别:没有 listen、accept 和 connect
前言:
当我们使用 TCP 和 UDP 协议进行网络编程时,尽管都使用套接字(socket)进行通信,但它们之间存在一些重要的区别。特别是关于如何建立连接、如何处理客户端请求以及如何进行数据传输,TCP和UDP有着根本性的不同。以下是详细的对比,重点讨论 listen
、accept
和 connect
等函数在 TCP 和 UDP 中的差异。
1. TCP 和 UDP 的基本区别
TCP(传输控制协议) 是面向连接的协议。在通信之前,客户端和服务器需要建立连接,确保可靠传输。
UDP(用户数据报协议) 是无连接的协议,不需要建立连接,也不保证数据传输的可靠性。每个数据包是独立的,不需要在发送前确认目标是否可达。
2. TCP 中的 listen
、accept
和 connect
listen
:
在 TCP 中,listen
函数是用来将服务器端的套接字设置为“监听状态”,它等待客户端的连接请求。这个函数需要在创建套接字并绑定端口后调用。listen
通常传入一个参数(backlog
),它表示服务器端能够排队的连接请求的数量。如果队列已满,新的连接请求会被拒绝。int listen(int sockfd, int backlog);
sockfd
是服务器端用于监听的套接字。backlog
是连接请求的队列长度。
accept
:
accept
是 TCP 中用于接受客户端连接请求的函数。当客户端请求连接时,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 中的区别:没有 listen
、accept
和 connect
listen
和accept
在 UDP 中没有意义:
UDP 是无连接的,不需要等待连接或接受连接请求。每个数据包(数据报)都是独立的,发送方和接收方不需要在发送数据之前进行握手或连接确认。因此,UDP 协议中没有listen
和accept
函数。客户端可以直接使用sendto
或recvfrom
来发送和接收数据。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. 总结对比:
函数 | TCP | UDP |
---|---|---|
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 client
server: server.cpp
g++ -o $@ $^ -std=c++11
client: client.cpp
g++ -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); // 父进程不再需要资源(必须关闭)
}
}
此方法是推荐的,因为它简单且不会导致僵尸进程。
总结与优化
父进程与子进程的职责分离:
父进程负责监听并接受连接请求。
每当父进程接收到一个客户端的连接请求,它将创建一个子进程来处理与该客户端的通信。
父进程不需要等待子进程退出,而是继续接收新的连接请求。
避免资源泄漏:
子进程处理完客户端的请求后,应该尽快退出,避免资源泄漏。
父进程应及时关闭不再使用的资源,避免文件描述符泄漏。
非阻塞等待机制:
使用
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 client
server: server.cc
g++ -o $@ $^ -std=c++11 -lpthread
client: client.cc
g++ -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 300
ps -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);
参数解读:
nochdir
改变进程的工作路径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
注意: 调用该函数的进程,不能是组长进程,需要创建子进程后调用
手动实现守护进程时需要注意以下几点:
- 忽略异常信号
0、1、2
要做特殊处理(文件描述符)- 进程的工作路径可能要改变(从用户目录中脱离至根目录)
具体实现步骤如下:
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 及更高版本中,接口进行了更新,新增了互斥锁,多线程场景中测试没问题