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:

cpp 复制代码
#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基本一致,都需要套接字

cpp 复制代码
// 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时说明

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

cpp 复制代码
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信息
cpp 复制代码
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是从文件中读写数据)

cpp 复制代码
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时一样

cpp 复制代码
#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时完全一样

cpp 复制代码
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自动绑定

cpp 复制代码
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
cpp 复制代码
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一样

cpp 复制代码
#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结构体的字段如下:

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

cpp 复制代码
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;
}
cpp 复制代码
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的格式化打印,就需要用到可变参数列表

cpp 复制代码
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最终实现:

cpp 复制代码
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回收资源,就不会出现僵尸进程了

cpp 复制代码
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改为处理的客户端通信任务

cpp 复制代码
#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一个任务

cpp 复制代码
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()的接口的进程不能是该作业(进程组)的组长

cpp 复制代码
#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终端,也不会因此关闭服务端进程

cpp 复制代码
int main(int argc, char *argv[])
{
    //......

    server.init();
    DaemonSelf(); //使该进程变为守护进程
    server.start();
    return 0;
}
相关推荐
烂白菜1 小时前
码道启辰:定时任务自由编排
运维·服务器·网络
梁辰兴1 小时前
计算机网络基础:对称加密密码体制
网络·计算机网络·计算机·对称加密·计算机网络基础·梁辰兴
Zhan8611241 小时前
WebSocket心跳与断线重连实战:芬兰赫尔辛基指数行情数据接口接入记录
网络·websocket·网络协议
衣乌安、1 小时前
JSON-RPC协议
网络协议·rpc·json
KaMeidebaby1 小时前
卡梅德生物技术快报 | Fab 合成文库构建与抗体筛选实验流程及数据解析
人工智能·python·tcp/ip·算法·机器学习
AI科技星1 小时前
氢原子基态能级跃迁紫外频段光子频率计算
开发语言·网络·量子计算·agi·拓扑学
CoderYanger2 小时前
Java EE:6.网络编程套接字(第二弹)
java·网络·程序人生·面试·职场和发展·java-ee·学习方法
网络研究院2 小时前
美国网络安全法律与实践
网络·安全·美国·法律·实践
yyuuuzz2 小时前
云服务器软件部署的常见问题与经验
linux·运维·服务器·网络·数据库·人工智能·github