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

Linux:TCP协议的socket套接字

目录

服务端

init()初始化方法

start()运行方法

收发数据:

main()

客户端

init()初始化方法

start()运行方法

main()

完善日志打印

多进程版

多线程版

守护进程

Deamon()实现


关于套接字的介绍,可以移步到下面这篇文章:

Linux:UDP协议的socket套接字-CSDN博客https://blog.csdn.net/suimingtao/article/details/161145821对于TCP套接字,和UDP一样需要创建socket,需要bind绑定ip和port

但,除此之外都有或多或少的区别,下面就边实现边介绍

声明:后续代码都在有此文件的基础上进行:log.hpp:

#pragma once // 颜色控制 #define BLACK "\033[0;30;1m" // 黑色 #define RED "\033[0;31;1m" // 红色 #define GREEN "\033[0;32;1m" // 绿色 #define YELLOW "\033[0;33;1m" // 黄色 #define BLUE "\033[0;34;1m" // 蓝色 #define PURPLE "\033[0;35;1m" // 紫色 #define CYAN "\033[0;36;1m" // 青色 #define WHITE "\033[0;37;1m" // 白色 #define BLACK_BL "\033[40;1m" // 背景黑色 #define RED_BL "\033[41;1m" // 背景红色 #define GREEN_BL "\033[42;1m" // 背景绿色 #define YELLOW_BL "\033[43;1m" // 背景黄色 #define BLUE_BL "\033[44;1m" // 背景蓝色 #define PURPLE_BL "\033[45;1m" // 背景紫色 #define CYAN_BL "\033[46;1m" // 背景青色 #define WHITE_BL "\033[47;1m" // 背景白色 #define ED "\033[0m" // 结束颜色控制 enum SevEx // 错误码 { USAGE_ERR = 1, // 传入命令行参数错误 SOCKET_ERR = 2, // socket()失败 BIND_ERR, // bind()失败 INETPN_ERR, // inet_pton/inet_ntop失败 OPENFILE_ERR, // ifstream打开文件失败 SENDTO_ERR, // sendto()失败 LISTEN_ERR, // listen()失败 ACCEPT_ERR, // accept()失败 CONNECT_ERR, // connect()失败 }; enum ERR_LEVEL // 错误等级/类型 { DEBUG = 0, // 调试 NORMAL, // 正常 WARNING, // 警告 ERROR, // 非致命错误 FATAL // 致命错误 }; void LogMessage(ERR_LEVEL error, std::string message) { //[错误等级/类型] [时间戳/时间] [pid] [message] std::string color; if(error == FATAL) color = RED_BL; else if(error == NORMAL) color = GREEN_BL; std::cout << color << message << ED << std::endl; }

服务端

服务端的成员变量和UDP基本一致,都需要套接字

// typedef function<void (std::string)> func_t;//回调函数类型 class TcpServer { public: TcpServer(uint16_t port) : _port(port) {} TcpServer(std::string ip, uint16_t port) : _ip(ip), _port(port) { } ~TcpServer() { } private: int _ListenSock; // listen监听套接字 std::string _ip = "0.0.0.0"; // 默认接收所有ip uint16_t _port; // 服务器端口号 // func_t _callback; };
  • 但服务端的套接字成员变量不是直接用来通信的,下面会细说
  • 由于现在先不涉及对数据的再处理,因此先把_callback注释掉

init()初始化方法

在UDP的初始化中,需要socket()创建套接字,bind()绑定套接字,而TCP也是如此,但TCP在这之后还需要进行一步:listen()开启监听

  • sockfd即为我们的监听套接字成员变量
  • backlog为全连接队列长度,这里就暂时设为5,具体含义会在介绍TCP时说明

只有设置了监听状态,才可以接收与客户端们的连接

const int gbacklog = 5; // 全连接队列长度 void init() { // 创建监听套接字 _ListenSock = socket(AF_INET, SOCK_STREAM, 0); if (_ListenSock == -1) { LogMessage(FATAL, "socket创建监听套接字失败"); exit(SOCKET_ERR); } LogMessage(NORMAL, "socket创建监听套接字成功"); // bind绑定ip+port struct sockaddr_in ServerAddr; memset(&ServerAddr, 0, sizeof(ServerAddr)); ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(_port); if (inet_pton(AF_INET, _ip.c_str(), &ServerAddr.sin_addr) != 1) { LogMessage(FATAL, "点分十进制ip转网络序列失败"); exit(INETPN_ERR); } LogMessage(NORMAL, "点分十进制ip转网络序列成功"); if (bind(_ListenSock, (struct sockaddr *)&ServerAddr, sizeof(ServerAddr)) != 0) { LogMessage(FATAL, "bind绑定失败"); exit(BIND_ERR); } LogMessage(NORMAL, "bind绑定成功"); // 开启监听状态 if (listen(_ListenSock, gbacklog) != 0) { LogMessage(FATAL, "listen监听状态开启失败"); exit(LISTEN_ERR); } LogMessage(NORMAL, "listen监听状态开启成功"); }

start()运行方法

在TCP中,接收来自客户端的数据前需要先跟目标客户端建立连接accept()就可以取出已经建立好的连接,并返回一个套接字描述符用于通信

  • sockfd需要传入监听套接字文件描述符
  • addr和addrlen类似于recvfrom中的addr和addrlen,都是输入输出型参数,用于获取客户端的addr信息
void start() { // 建立连接 struct sockaddr_in ClientAddr; memset(&ClientAddr, 0, sizeof(ClientAddr)); socklen_t socklen = sizeof(ClientAddr); int sockfd = accept(_ListenSock, (struct sockaddr *)&ClientAddr, &socklen); if (sockfd == -1) { LogMessage(FATAL, "accept建立新连接失败"); exit(ACCEPT_ERR); } LogMessage(NORMAL, "accept建立新连接成功"); linkone(sockfd, ClientAddr);//持续接收客户端的消息 }

获取新连接后就要从accpet的返回值的套接字中读取数据并处理

收发数据:

TCP收发数据的方式有很多,其中read/write就是一种(因为sockfd本质上也是文件描述符,而read/write是从文件中读写数据)

void linkone(int sockfd, sockaddr_in addr) { uint16_t port = ntohs(addr.sin_port); char buffer[65] = {0}; const char *ip = inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer)); if(ip == nullptr) { LogMessage(FATAL, "网络序列ip转点分十进制失败"); exit(INETPN_ERR); } LogMessage(NORMAL, "网络序列ip转点分十进制成功"); //收发数据 while (true) { char message[1024] = {0}; //读取数据 int n = read(sockfd, message, sizeof(message)); //数据处理并返回 if (n > 0) { message[n] = 0; std::cout << BLUE << "[" << ip << '-' << port << "]# " << ED PURPLE << message << ED << std::endl; write(sockfd, message, sizeof(message)); } else//如果read返回值为0代表对方进程结束 { std::cout << RED_BL << "客户端退出..." ED << std::endl; close(sockfd); break; } } }

main()

对于主函数,逻辑和UDP时一样

#include <iostream> #include "TcpServer.hpp" using namespace std; using namespace Server; void usage(string proc) // 使用手册 { cout << GREEN << "\nUsage: \n\t" << ED << RED << proc << " [port]\n\n" << ED; } int main(int argc, char *argv[]) { if (argc != 2) { usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]);//字符串port转整数 TcpServer server(port); server.init(); server.start(); return 0; }

客户端

对于客户端而言,成员变量和UDP时完全一样

class TcpClient { public: TcpClient(std::string ip, uint16_t port) : _ip(ip), _port(port) { } ~TcpClient() { } private: int _SocketFd; // 通信的套接字 std::string _ip; // 服务端的ip uint16_t _port; // 服务端的端口号 struct sockaddr_in _ServerAddr; // 服务端的addr };

init()初始化方法

TCP的初始化与UDP时完全一样,创建套接字后无需显式绑定ip+port,在第一次connect()时会由OS自动绑定

void init() { // 创建套接字 _SocketFd = socket(AF_INET, SOCK_STREAM, 0); if (_SocketFd == -1) { LogMessage(FATAL, "socket创建套接字失败"); exit(SOCKET_ERR); } LogMessage(NORMAL, "socket创建套接字成功"); // 无需显式bind绑定 // 初始化服务端的addr memset(&_ServerAddr, 0, sizeof(_ServerAddr)); _ServerAddr.sin_family = AF_INET; _ServerAddr.sin_port = htons(_port); if (inet_pton(AF_INET, _ip.c_str(), &_ServerAddr.sin_addr) != 1) { LogMessage(FATAL, "点分十进制ip转网络序列失败"); exit(INETPN_ERR); } LogMessage(NORMAL, "点分十进制ip转网络序列成功"); }

start()运行方法

TCP中要想服务器发送数据,需要先建立连接,建立连接就需要用到connect()

  • sockfd传入套接字描述符
  • addr和addrlen类似于sendto时的addr和addrlen,用于指定要连接的服务端的addr
void start() { // 向服务器申请建立连接 if (connect(_SocketFd, (struct sockaddr *)&_ServerAddr, sizeof(_ServerAddr)) != 0) { LogMessage(FATAL, "connect申请建立连接失败"); exit(CONNECT_ERR); } LogMessage(NORMAL, "connect申请建立连接成功"); // 收发消息 std::string message; while (true) { // 写入 std::cout << RED "请输入文本# " ED; std::getline(std::cin, message); write(_SocketFd, message.c_str(), message.size()); // 读取 char buffer[1024] = {0}; read(_SocketFd, buffer, sizeof(buffer)); message = std::string(buffer) + "[Server Echo]"; std::cout << PURPLE << message << ED << std::endl; } }

main()

对于TCP的main,与UDP一样

#include <iostream> #include "TcpClient.hpp" using namespace std; using namespace Client; void usage(string proc) // 使用手册 { cout << GREEN << "\nUsage:\n\t" ED RED << proc << " [ip] [port]\n\n" ED; } int main(int argc, char *argv[]) { if (argc != 3) { usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[2]); TcpClient client(argv[1], port); client.init(); client.start(); return 0; }

当启动客户端与服务端后,用netstat命令查看当前连接

就可以看到8080端口的TcpClient向8080端口发连接的TcpClient

完善日志打印

现有的log.hpp仅完成了打印消息这一个功能,而实际中日志还应该包含日期,错误等级等信息

关于日期时间的打印,time(nullptr)可以拿到当前时间的时间戳,再通过localtime()将对应时间戳转换为带有日期时间字段的结构体

struct tm结构体的字段如下:

之后再通过该结构体的字段将年月日时分秒拼接成一个字符串,即为时间打印

std::string DateTime(time_t timesatamp) // 获取对应时间戳的日期-时间 { struct tm *t = localtime(&timesatamp); // 2026/5/28-20:39:01 std::string year = std::to_string(t->tm_year + 1900); // 除了年份,必须要保持始终为两位数(不够用0补齐) std::string month = (std::to_string(t->tm_mon).size() == 1) ? ('0' + std::to_string(t->tm_mon)) : (std::to_string(t->tm_mon)); // 月 始终是两位数 std::string day = (std::to_string(t->tm_mday).size() == 1) ? ('0' + std::to_string(t->tm_mday)) : (std::to_string(t->tm_mday)); // 日 始终是两位数 std::string hour = (std::to_string(t->tm_hour).size() == 1) ? ('0' + std::to_string(t->tm_hour)) : (std::to_string(t->tm_hour)); // 时 始终是两位数 std::string min = (std::to_string(t->tm_min).size() == 1) ? ('0' + std::to_string(t->tm_min)) : (std::to_string(t->tm_min)); // 分 始终是两位数 std::string sec = (std::to_string(t->tm_sec).size() == 1) ? ('0' + std::to_string(t->tm_sec)) : (std::to_string(t->tm_sec)); // 秒 始终是两位数 std::string now = year + '/' + month + '/' + day + '-' + hour + ':' + min + ':' + sec; return now; }
void LogMessage(ERR_LEVEL error, char *format, ...) // 可变参数列表 { //[错误等级/类型] [时间戳/时间] [pid] [message] // 取得错误等级字符串/颜色 std::string errLevel, color; switch (error) { case DEBUG: errLevel = "DEBUG"; color = CYAN_BL; break; case WARNING: errLevel = "WARNING"; color = YELLOW_BL; break; case ERROR: errLevel = "ERROR"; color = RED_BL; break; case FATAL: errLevel = "FATAL"; color = PURPLE_BL; break; default: errLevel = "DEBUG"; color = CYAN_BL; } // 日志类型 char logprefix[1024] = {0}; snprintf(logprefix, sizeof(logprefix), "[%s] [%s] [pid: %d]", errLevel.c_str(), DateTime(time(nullptr)).c_str(), getpid()); // TODO }

现在对于日志的类型字段就打印完成了

对于日志的消息,原来只能完成固定字符串的打印,如果想要支持类似printf的格式化打印,就需要用到可变参数列表

void LogMessage(ERR_LEVEL error, char *format, ...) // 可变参数列表 { // ...... }

C 语言函数调用时,参数通常从右向左压入栈中,对于上面的LogMessage函数,栈顶固定为error参数,函数内部就可以通过error的位置来确定栈顶。但如果从左向右压栈,栈顶为可变参数的最后一个参数,因为可变参数的数量和类型在编译时是未知的,函数内部就无法确定栈顶在哪里。可以说C设计成从右向左压栈,正是为了支持可变参数

C语言提供了va_listva_start()va_end()va_arg()等宏来支持可变参数

  • va_list用于定义一个可变参数的指针(该宏本质上就是char*)
  • va_start()用于初始化一个va_list类型,通过传入可变参数的上一个参数,从而找到可变参数的第一个参数,在这里就是传入va_start(va_list类型, format),因为format是可变参数列表前的最后一个参数
  • va_end()用于清理va_list类型变量

LogMessage最终实现:

void LogMessage(ERR_LEVEL error, char *format, ...) // 可变参数列表 { //[错误等级/类型] [日期时间] [pid] [message] // 取得错误等级字符串/颜色 std::string errLevel, color; switch (error) { case DEBUG: errLevel = "DEBUG"; color = CYAN_BL; break; case WARNING: errLevel = "WARNING"; color = YELLOW_BL; break; case ERROR: errLevel = "ERROR"; color = RED_BL; break; case FATAL: errLevel = "FATAL"; color = PURPLE_BL; break; default: errLevel = "DEBUG"; color = CYAN_BL; } // 日志类型 char logprefix[1024] = {0}; snprintf(logprefix, sizeof(logprefix), "[%s] [%s] [pid: %d]", errLevel.c_str(), DateTime(time(nullptr)).c_str(), getpid()); // 日志信息 char logcontent[1024] = {0}; va_list arg; va_start(arg, format); // 将 arg 定位到 format 参数之后的位置 vsnprintf(logcontent, sizeof(logcontent), format, arg); // va_list充当可变参数列表 va_end(arg); std::cout << color << logprefix << "# " << logcontent << ED << std::endl; }

多进程版

在上面实现的服务端中,同时有且只能有一个客户端同时进行通信,而在实际应用中,往往有多个客户端同时连接着服务端

要实现多进程版,就需要fork创建子进程。当accept获取到新连接后,需要fork,让子进程去执行该客户端(套接字描述符)的收发数据工作

  • 但如果仅让子进程运行而不处理其终止状态,父进程仍需要通过wait回收子进程资源,否则会产生僵尸进程
  • 若采用阻塞式等待,则与之前情况相同——必须等待当前客户端断开连接后才能建立新连接
  • 若采用非阻塞式等待,当多个客户端连接服务端时,由于非阻塞等待会立即返回,父进程将直接阻塞在accept调用处。此时若不再有新连接,先前未成功回收的子进程将永远无法被等待,最终导致僵尸进程堆积的问题。

这里采用一种很巧妙的方法:让子进程再fork创建孙子进程,再让子进程直接退出,由孙子进程执行客户端的收发数据任务。由于孙子进程变成了孤儿进程,被OS领养,当退出时由OS回收资源,就不会出现僵尸进程了

void start() { signal(SIGCHLD, SIG_IGN);// OS自动回收子进程资源 while (true) { // 建立连接 struct sockaddr_in ClientAddr; memset(&ClientAddr, 0, sizeof(ClientAddr)); socklen_t socklen = sizeof(ClientAddr); int sockfd = accept(_ListenSock, (struct sockaddr *)&ClientAddr, &socklen); if (sockfd == -1) { LogMessage(FATAL, (char *)"accept建立新连接失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(ACCEPT_ERR); } LogMessage(DEBUG, (char *)"accept建立新连接成功,sockfd = %d", sockfd); pid_t pid = fork(); if (pid == 0) // 子进程 { close(_ListenSock); // 关掉无用文件描述符 if (fork() > 0) // 子进程本身 exit(0); // 孙子进程,被OS领养,不等待也不会变成僵尸进程 linkone(sockfd, ClientAddr); } close(sockfd); // 父进程关掉该文件描述符,防止文件描述符被用完 } }

这里有个细节,几乎每次accpet返回的套接字描述符都是4,这是因为父进程每次都会关闭新接收的套接字描述符,交给孙子进程(为什么是4?因为0/1/2被标准输入/输出/错误占用,3被监听套接字占用

多线程版

在多进程版中,每有一个新客户端,都要fork创建子进程,这开销还是太大了,因此下面用多线程实现一波(实际业务中多线程只适用于可以一瞬间完成的任务,这种需要持续存在的任务其实并不适合...)

多线程部分用本篇文章实现的线程池demo:

Linux生产者消费者模型-CSDN博客https://blog.csdn.net/suimingtao/article/details/160381695把Task.hpp改为处理的客户端通信任务

#pragma once #include <functional> #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include "log.hpp" void linkone(int sockfd, struct sockaddr_in addr) { uint16_t port = ntohs(addr.sin_port); char buffer[65] = {0}; const char *ip = inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer)); if (ip == nullptr) { LogMessage(FATAL, (char *)"网络序列ip转点分十进制失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(INETPN_ERR); } LogMessage(DEBUG, (char *)"网络序列ip转点分十进制成功"); // 收发数据 while (true) { char message[1024] = {0}; // 读取数据 int n = read(sockfd, message, sizeof(message)); // 数据处理并返回 if (n > 0) { message[n] = 0; std::cout << BLUE << "[" << ip << '-' << port << "]# " << ED PURPLE << message << ED << std::endl; write(sockfd, message, sizeof(message)); } else // 如果read返回值为0代表对方进程结束 { std::cout << RED_BL << "客户端退出..." ED << std::endl; close(sockfd); break; } } } class Task // 计算任务类型 { using func_t = std::function<void(int, struct sockaddr_in)>; public: Task(int sockfd, struct sockaddr_in addr, func_t fun) : _sockfd(sockfd), _addr(addr), _callback(fun) { } Task() { } void operator()() // 仿函数,返回结果描述 { _callback(_sockfd, _addr); } // std::string toop() // 返回要处理的任务描述 // { // char buffer[64]; // snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y); // return buffer; // } private: int _sockfd; struct sockaddr_in _addr; func_t _callback; // 回调函数 };

在TcpServer.hpp中,添加对线程池的初始化,并且在每次accept获取新连接成功后,往线程池中push一个任务

void start() { //初始化线程池 ThreadPool<Task>::getInstance().runc(); signal(SIGCHLD, SIG_IGN);// OS自动回收子进程资源 while (true) { // 建立连接 struct sockaddr_in ClientAddr; memset(&ClientAddr, 0, sizeof(ClientAddr)); socklen_t socklen = sizeof(ClientAddr); int sockfd = accept(_ListenSock, (struct sockaddr *)&ClientAddr, &socklen); if (sockfd == -1) { LogMessage(FATAL, (char *)"accept建立新连接失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(ACCEPT_ERR); } LogMessage(DEBUG, (char *)"accept建立新连接成功,sockfd = %d", sockfd); Task t(sockfd, ClientAddr, linkone); ThreadPool<Task>::getInstance().push(t); //close(sockfd); // 父进程关掉该文件描述符,防止文件描述符被用完 } }

在TcpServer启动后,用ps -aL就可以看到已经在运行的线程(线程池内设置成了默认启动10个线程):

守护进程

在实际业务中的服务器,启动后即使关闭远程的ssh连接,服务器进程也不会退出。但我们上面实现的服务器,如果启动后关闭xshell窗口,服务器进程就会一起退出。

为了让进程不会随ssh连接而退出,就要将该进程变为守护进程

每个进程都有自己的组ID,例如sleep 1000 | sleep 2000 | slepp 3000运行起来后,用ps命令查看时会发现他们的PGID一样,并且为sleep 1000 的PID(&代表让命令在后台运行

它们三个进程要共同完成这一个任务,这里的PGID就是该作业的组ID,sleep 1000是该组第一个被启动的进程,因此它就是组长,PGID 就是组长的 PIDSID为会话号(Session ID),下面会介绍会话概念)

除了可以这么查看之外,还可以用jobs命令查看当前终端会话中所有正在后台运行或已暂停的作业
每个作业都有作业号(最前面的[ ]内的数字)

  • +号分配给最近一次被挂起(按 Ctrl+Z)或放入后台(加 &)的作业
  • -号分配给前一个/次当前作业,即倒数第二近被操作的作业。

如果想将在后台的作业先放回前台,可以用fg命令

若直接输入fg,会调回默认作业(带+号的作业)

或输入fg %[作业号],调回指定作业,在fg命令中可以省略%(kill %[作业号]可以终止指定作业)


对于前台任务,按下Ctrl+Z可以暂停该任务,若想对暂停的任务,继续运行,可以用bg命令:将一个已经暂停(Stopped)的作业,放到后台去继续运行

例如下面程序,在前台运行时我用Ctrl + Z暂停该作业,再用bg命令让该作业在后台继续运行(用法和fg类似)


拿xshell来举例,每一个窗口都是一个会话每个会话都有一个bash进程用于解释命令行(对于在终端输入的命令,它的 PPID 即为bash

且每个会话有且只能有一个前台进程(默认为bash),当有进程要在前台启动时,bash会自动去后台(这也就是为什么进程启动后不能再输入命令)
xshell窗口关闭时,该会话也会自动关闭里面的任务自然也会关闭

若想不受ssh登录注销的影响,可以让该进程自成会话,自成进程组,此时这个进程就叫作守护进程

虽然Linux也提供用于创建守护进程的接口daemon(),但这个接口实在太老旧了,因此一般都选择自己手写一个daemon接口

  • nochdir:守护进程更改后的工作目录
  • noclose:若为0,则关闭0/1/2文件描述符,并重定向到/dev/null(下面会介绍),否则不关闭

Deamon()实现

创建守护进程,必不可少的接口:setsid()接口可以让调用它的进程离开当前所在的作业,独自成立会话,成为该作业进程组组长

需要注意的是,调用setsid()的接口的进程不能是该作业(进程组)的组长

#pragma once #include <string> #include <unistd.h> #include <fcntl.h> #include <cstdlib> #include <csignal> #include "log.hpp" #define DUP_PATH "/dev/null" // 黑洞文件,相当于垃圾桶 void DaemonSelf(std::string nochdir = std::string() /*要更改为的工作目录*/, bool noclose = false /*是否要重定向std in/out/err*/) { // 让调用进程忽略掉异常的信号 signal(SIGPIPE, SIG_IGN); // 让进程不是组长,从而调用setsid() if (fork() > 0) exit(0); // 子进程 int pid = setsid(); if (pid == -1) { LogMessage(FATAL, "setsid()创建会话失败"); exit(SETSID_ERR); } // 此时该进程是新会话的Session Leader(首进程),需要再次fork脱离Session Leader角色 if (fork() > 0) exit(0); // 孙子进程,此时彻底与终端绝缘 //如果设置了工作目录,就更改 if(!nochdir.empty()) if(!chdir(nochdir.c_str()))//若chdir失败,报错 LogMessage(ERROR, "chdir修改工作目录失败"); // 如果没有设定不关闭,就重定向进程的标准输出/输出/错误 if(!noclose) { int fd = open(DUP_PATH, O_RDWR); if(fd == -1) { LogMessage(ERROR, "打开重定向文件失败"); } else { //重定向std in/out/err到该文件中 dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if(fd > STDERR_FILENO) // fd不是0/1/2其中一个,才可以关闭 close(fd); } } }

/dev/null被称为黑洞文件,不管是读取还是写入,都无视掉,是Linux的安全垃圾桶

在启动服务端时让进程变为守护进程,这样即使退出ssh终端,也不会因此关闭服务端进程

int main(int argc, char *argv[]) { //...... server.init(); DaemonSelf(); //使该进程变为守护进程 server.start(); return 0; }
http://www.jsqmd.com/news/1025395/

相关文章:

  • 沈阳专利咨询机构排行盘点 客观呈现服务核心能力 - 互联网科技品牌测评
  • 2026连云港黄金回收用户口碑测评,6家门店真实客评+区域选购全攻略 - 速递信息
  • 2026成都回收爱马仕怎么选?完整版防坑白皮书盘点门店 - 禹竞
  • 征管新规下浦东新区市场主体疑难注销阻滞成因与代办机构能力评估研究 - 企服靠谱君
  • 计算机毕业设计之基于jsp“梦回汉唐”汉服商城网站的设计与实现
  • 猫抓浏览器插件:如何简单快速下载网页视频和音频的完整指南
  • 分布式图书数据集成架构:Open Library高性能API网关与微服务架构设计
  • 【2026最新亲测】7款高性价比免费降AI率工具测评 - 殷念写论文
  • 如何高效管理漫画收藏:专业转换工具完全指南
  • 上海包包回收机构哪家最靠谱?收的顶专业回收香奈儿,当面鉴定报价透明 - 奢侈品回收测评
  • 2026榆次搬家全攻略:价格明细、服务商筛选、长途与大件搬运注意事项汇总 - 资讯纵览
  • Maximum Subarray Sum After at Most K Swaps
  • CANN OAM-Tools运维工具包手把手实战入门:基于昇腾NPU的oamget/oamset/oamsetper设备诊断命令从安装部署到生产环境实战的全流程操作指南
  • 终极开源金融大模型:Cornucopia-LLaMA-Fin-Chinese 完整部署与实战指南 [特殊字符]
  • 2026北京名包回收榜单,高报价靠谱门店汇总 - 名奢变现站
  • 2026年6月最新欧米茄中国官方售后电话热线服务地址网点客服 - 速递信息
  • 百万外贸订单险失效!实地尽调规避科威特骗货风险
  • 5家靠谱铸铝门厂家挑选指南,高端别墅入户门工厂实测对比 - 门业测评
  • LiveKit完整指南:5分钟搭建你的第一个实时音视频应用
  • AI驱动测试自动化:基于Codex与DeepSeek的Selenium/Appium实战指南
  • 一条线公排模式开发解析
  • 【Linux】系统级文件I/O与文件描述符深度剖析
  • 2026济南奢侈品包包回收行业白皮书,正规门店全域实测 - 薛定谔的梨花猫
  • 如何快速掌握Python量化投资分析:QuantStats完整指南
  • 金融行业数字化——解读金融数据库存算分离架构选型白皮书【附全文阅读】
  • Linux Pulseaudio深度解析之pa_context_set_sink_input_volume用流程与实战(五十九)
  • 2026内衬不锈钢复合管厂家到底哪家强 - 速递信息
  • 2026南昌公司变更避坑TOP榜单!股权/地址/法人变更均可 - 江西企服智库
  • 2026北京卡地亚手表回收深度测评,禹竞名奢汇变现首选,六大靠谱商家综合实力盘点 - 名奢变现站
  • EVM3588-B开发板+NPU+Qwen2.5-3B-Instruct(一)