目录
服务端
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(×atamp); // 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_list、va_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 就是组长的 PID(SID为会话号(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; }