简单的TCP程序

文章目录

  • [3. TCP程序](#3. TCP程序)
    • [3.1 接口](#3.1 接口)
      • [3.1.1 inet_aton()](#3.1.1 inet_aton())
      • [3.1.2 listen()](#3.1.2 listen())
      • [3.1.3 现在的服务器代码](#3.1.3 现在的服务器代码)
      • [3.1.4 accept()](#3.1.4 accept())
      • [3.1.5 inet_ntop()](#3.1.5 inet_ntop())
      • [3.1.6 tcpClient.cc](#3.1.6 tcpClient.cc)
    • [3.2 并发的 tcpServer](#3.2 并发的 tcpServer)
      • [3.2.1 多进程版本](#3.2.1 多进程版本)
      • [3.2.2 多线程版本](#3.2.2 多线程版本)
      • [3.2.3 线程池版本](#3.2.3 线程池版本)
    • [3.3 继续完善](#3.3 继续完善)
      • [3.3.1 增加客户端重连功能](#3.3.1 增加客户端重连功能)
      • [3.3.2 守护进程](#3.3.2 守护进程)

3. TCP程序

3.1 接口

3.1.1 inet_aton()

inet_aton() 是一个用于网络编程的函数,它用于将 IPv4 地址的点分十进制字符串表示转换为网络字节序的二进制形式。这个函数是 POSIX 标准的一部分。功能与inet_addr()一样

函数原型如下:

c 复制代码
int inet_aton(const char *cp, struct in_addr *inp);
  • cp 是一个指向以点分十进制表示的 IPv4 地址字符串的指针(例如 "192.168.1.1")。
  • inp 是一个指向 struct in_addr 结构体的指针,该结构体用于存储转换后的网络地址。

如果转换成功,inet_aton 函数返回非零值(通常是 1),表示输入字符串是一个有效的 IPv4 地址。如果转换失败(即输入字符串不是一个有效的 IPv4 地址),函数返回 0。

3.1.2 listen()

TCP是面向连接的,服务器一般是比较"被动的",一直处于一种一直等待连接到来的状态

cpp 复制代码
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

用于将一个套接字从初始状态(CLOSED)转变为监听状态(LISTEN),以便服务器可以接受来自客户端的连接请求。

参数说明:

  1. int sockfd:这是之前使用socket()系统调用创建的套接字的文件描述符。
  2. int backlog:这个参数指定了内核应该为相应套接字排队的最大连接个数。如果同时有超过backlog个的连接请求,那么超出的连接请求将被拒绝。backlog的值至少为1,具体的最大值依赖于具体的实现和系统配置。

返回值:

  • 如果listen()调用成功,返回0。
  • 如果调用失败,返回-1,并设置全局变量errno以指示错误原因。

3.1.3 现在的服务器代码

cpp 复制代码
// tcpServer.hpp
const string defaultip = "0.0.0.0";
const int backlog = 10;
Log log;

enum {
    SOCKET_ERR,
    IP_ERR,
    BIND_ERR,
    LISTEN_ERR,
};

class TcpServer
{
private:
    int _listenFd = -1;
    uint16_t _port;
    string _ip;
public:
    TcpServer();
    TcpServer(const uint16_t& port, const string& ip);
    ~TcpServer();
    void initServer();
    void run();
};

TcpServer::TcpServer()
{}

inline TcpServer::TcpServer(const uint16_t& port, const string& ip=defaultip) : _port(port), _ip(ip)
{}

TcpServer::~TcpServer()
{}

void TcpServer::initServer()
{
    // 创建套接字
    _listenFd = socket(AF_INET, SOCK_STREAM, 0);
    if(_listenFd == -1) {
        log(FATAL, "创建套接字失败, %s\n", strerror(errno));
        exit(SOCKET_ERR);
    }
    log(INFO, "创建套接字成功, fd: %d\n", _listenFd);
    sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    if (inet_aton(_ip.c_str(), &(local.sin_addr)) == 0) {
        log(FATAL, "IP不合法, %s\n", strerror(errno));
        exit(IP_ERR);
    };
    // 绑定
    if(bind(_listenFd, (sockaddr*)&local, sizeof(local)) == -1) {
        log(FATAL, "绑定失败, %s\n", strerror(errno));
        exit(BIND_ERR);
    }
    log(INFO, "绑定成功, fd: %d\n", _listenFd);
    // 监听
    if(listen(_listenFd, backlog) == -1) {
        log(FATAL, "监听失败, %s\n", strerror(errno));
        exit(LISTEN_ERR);
    }
    log(INFO, "监听成功, fd: %d\n", _listenFd);
}

void TcpServer::run()
{
    for(;;) {
        printf("tcp server is running...\n");
        sleep(1);
    }
}
cpp 复制代码
// main.cc
#include "tcpServer.hpp"
#include <memory>

void Usage(const char* string)
{
    cout << "\n\rUsage: " << string << " port(1024+)\n\n";  
}

int main(int argc, char* argv[])
{
    if(argc != 2) {
        Usage(argv[0]);
        return -1;
    } else {
        uint16_t port = stoi(argv[1]);
        unique_ptr<TcpServer> tp(new TcpServer(port));
        tp->initServer();
        tp->run();
    }
    return 0;
}

3.1.4 accept()

cpp 复制代码
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

用于从已在listen()状态的套接字(服务器端)接受一个连接请求,并创建一个新的套接字来处理这个连接。

参数说明:

  1. int socket:这是之前使用listen()函数设置为监听状态的套接字的文件描述符。
  2. struct sockaddr* address:输出型参数,这是一个指向sockaddr结构的指针,用于存储连接客户端的地址信息。这个结构能够容纳任何类型的协议地址,比如IPv4或IPv6地址。
  3. socklen_t* address_len:输出型参数,这是一个指向socklen_t类型变量的指针,该变量在调用accept()之前应该被初始化为address指向的缓冲区的大小。在accept()调用成功返回后,这个变量将被设置为实际存储在address中的地址结构的长度。

返回值:

  • 如果accept()调用成功,返回一个新的文件描述符,这个文件描述符与原始的监听套接字是独立的,用于与客户端通信。
  • 如果调用失败,返回-1,并设置全局变量errno以指示错误原因

为什么使用两个套接字?

  1. 并发连接

    • 监听套接字:服务器使用一个监听套接字来监听客户端的连接请求。这个套接字绑定到服务器的一个端口上,并设置为监听模式,以便接受来自客户端的连接请求。
    • 已接受连接套接字 :每当一个客户端连接请求被接受时,accept()函数会创建一个新的套接字,称为已接受连接套接字。这个新的套接字用于与特定的客户端进行通信。这样,服务器可以同时处理多个客户端连接,每个连接都有自己的套接字。
  2. 资源管理

    • 每个已接受连接套接字都是独立的,可以有自己的缓冲区、状态和选项。这使得服务器可以为每个连接提供定制化的服务。
    • 监听套接字和已接受连接套接字的分离有助于更好地管理资源和控制连接的生命周期。
  3. 非阻塞和多路复用

    • 监听套接字通常设置为非阻塞模式,这样服务器可以在等待新的连接请求时继续执行其他任务,如处理已建立的连接。

    • 服务器可以使用I/O多路复用技术(如select()poll()epoll())来同时监控多个套接字,包括监听套接字和多个已接受连接套接字。这样,服务器可以在一个线程或进程中高效地处理多个连接


完善一下run()方法

cpp 复制代码
void TcpServer::run()
{
    for(;;) {
        sockaddr_in client;
        socklen_t len = sizeof(client);
        // 接受请求
        int socketFd = accept(_listenFd, (sockaddr*)&client, &len);
        if(socketFd == -1) {
            log(WARNING, "接受请求失败, %s\n", strerror(errno));
            continue;
        }
        log(INFO, "获得了一个新链接, 新的socket fd: %d\n", socketFd);
        printf("tcp server is running...\n");
        sleep(1);
    }
}

3.1.5 inet_ntop()

inet_ntop() 函数的主要用途是将网络地址(如IPv4或IPv6地址)从它们的二进制表示形式转换为人类可读的点分十进制形式(对于IPv4)或冒号分隔形式(对于IPv6)。这对于调试和日志记录非常有用,因为二进制形式的地址难以阅读和理解。

c 复制代码
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明

  • int af:指定地址族,常见的有AF_INET(IPv4)和AF_INET6(IPv6)。
  • const void *src:指向包含原始网络地址的缓冲区的指针。
  • char *dst:指向目标字符串的缓冲区的指针,该字符串将包含转换后的地址。
  • socklen_t size:目标缓冲区的大小。

返回值

  • 如果转换成功,返回指向目标缓冲区的指针。
  • 如果转换失败,返回NULL,并设置errno以指示错误。

inet_ntoa()的区别

  1. IPv6支持

    • inet_ntop():支持IPv4和IPv6地址。

    • inet_ntoa():仅支持IPv4地址。

  2. 返回值

    • inet_ntop():返回一个指向目标缓冲区的指针,如果转换成功;如果失败,则返回NULL
    • inet_ntoa():返回一个指向静态分配的字符串的指针,该字符串包含转换后的地址。这意味着多次调用inet_ntoa()可能会覆盖前一次的返回值。
  3. 缓冲区管理

    • inet_ntop():需要用户提供一个目标缓冲区,并指定缓冲区的大小,这有助于避免缓冲区溢出。
    • inet_ntoa():使用内部静态缓冲区,这意味着 它不是线程安全的,因为多个线程可能会同时修改这个缓冲区。

继续完善一下run()方法

cpp 复制代码
void TcpServer::run()
{
    for(;;) {
        printf("tcp server is running...\n");
        sockaddr_in client;
        socklen_t len = sizeof(client);
        // 接受请求
        int socketFd = accept(_listenFd, (sockaddr*)&client, &len);

        if(socketFd == -1) {
            log(WARNING, "接受请求失败, %s\n", strerror(errno));
            continue;
        }

        // 获得客户端的ip和端口号
        uint16_t port = ntohs(client.sin_port);
        char ip[INET_ADDRSTRLEN];       // INET_ADDRSTRLEN 是16
        memset(ip, 0, INET_ADDRSTRLEN);

        if(inet_ntop(AF_INET, &(client.sin_addr), ip, INET_ADDRSTRLEN) == nullptr) {
            log(FATAL, "inet_ntop, IP不合法, %s\n", strerror(errno));
            exit(IP_ERR); 
        }

        log(INFO, "获得了一个新链接, 新的socket fd: %d, 客户端ip: %s, 客户端端口号: %d\n", socketFd, ip, port);
        service(socketFd, ip, port);
        close(socketFd);
    }
}

void TcpServer::service(const int &socketFd, const string &ip, const uint16_t &port)
{
    char buf[1024];
    while(true) {
        // 从socketFd中读取数据
        ssize_t n = read(socketFd, buf, sizeof (buf) - 1);
        if(n > 0) {
            buf[n] = 0;          // 确保字符串以空字符结尾
            // 写回客户端
            string echoString = "server get a message$ ";
            echoString += buf;
            printf("client say$ %s\n", buf);
            write(socketFd, echoString.c_str(), echoString.size());
        } else if(n == 0) {
            log(INFO, "客户端 %s:%d 退出, 关闭fd:%d", ip.c_str(), port, socketFd);
            break;
        } else {
            log(WARNING, "读取客户端 %s:%d 信息失败, 关闭fd:%d", ip.c_str(), port, socketFd);
            break;
        }
    }
}

3.1.6 tcpClient.cc

这里使用inet_pton()

inet_pton() 函数(Internet Protocol Family to Numeric)是一个用于将网络地址的文本表示形式转换为网络字节顺序的二进制形式的函数。这个函数支持IPv4和IPv6地址。

函数原型

c 复制代码
#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);

参数说明

  • int af:指定地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
  • const char *src:指向包含网络地址文本表示的字符串的指针。
  • void *dst:指向用于存储转换后的网络地址二进制表示的缓冲区的指针。

返回值

  • 如果转换成功,返回1
  • 如果src不是有效的网络地址字符串,返回0
  • 如果发生错误(如af不支持),返回-1,并设置全局变量errno以指示错误原因。

行为

  • inet_pton() 函数会尝试将src指向的字符串转换为指定地址族af的网络地址二进制形式,并存储在dst指向的缓冲区中。
  • 对于IPv4地址,dst需要是一个struct in_addr类型的缓冲区。
  • 对于IPv6地址,dst需要是一个struct in6_addr类型的缓冲区。

客户端需要connect()

这个函数允许一个套接字(socket)主动连接到另一个服务器套接字。

函数原型

c 复制代码
#include <sys/socket.h>

int connect(int socket, const struct sockaddr *address, socklen_t address_len);

参数说明

  • int socket:要连接的套接字的文件描述符。
  • const struct sockaddr *address:指向一个 sockaddr 结构体的指针,该结构体包含了服务器套接字的地址信息。
  • socklen_t address_lenaddress 参数指向的 sockaddr 结构体的大小。

返回值

  • 如果连接成功建立,返回 0
  • 如果连接失败,返回 -1,并设置全局变量 errno 以指示错误原因。

行为

  • connect() 函数会尝试将本地套接字(由 socket 参数指定)连接到远程服务器套接字(由 address 参数指定)。
  • 这个函数通常用于主动套接字(client socket),即客户端程序中。
  • 如果连接成功,套接字 socket 将与远程地址关联,并准备好进行数据交换。
  • 如果 address 参数中的地址族与套接字 socket 的地址族不匹配,connect() 将失败并设置 errnoEINVAL
  • connect() 是一个阻塞调用,如果连接不能立即建立,调用将阻塞直到连接建立或发生错误。

cpp 复制代码
// ...
#include "log.hpp"
using namespace std;

Log log;
enum {
    SOCKET_ERR,
    CONNECT_ERR,
};

void Usage(const char* string)
{
    cout << "\n\rUsage: " << string << " serverIp serverPort\n\n";  
}

int 
main(int argc, char* argv[])
{
    if(argc != 3) {
        Usage(argv[0]);
        return -1;
    }

    // 创建套接字
    int socketFd = socket(AF_INET, SOCK_STREAM, 0);

    if(socketFd == -1) {
        // 创建失败
        log(FATAL, "客户端创建套接字失败, fd: %d, 原因: %s\n", socketFd, strerror(errno));
        close(socketFd);
        exit(SOCKET_ERR);
    }
    log(INFO, "客户端创建套接字成功\n");
    // 获得服务端的IP和端口号
    string serverIp = argv[1];
    uint16_t serverPort = stoi(argv[2]);
    // 构建服务器信息
    sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_pton(AF_INET, serverIp.c_str(), &(server.sin_addr));
    
    if(connect(socketFd, (sockaddr*)&server, sizeof server) < 0) {
        // 连接失败
        log(FATAL, "客户端连接失败, 原因: %s\n", strerror(errno));
        close(socketFd);
        exit(CONNECT_ERR);
    }
    log(INFO, "客户端连接成功, 服务器ip: %s, 服务器端口号: %d\n", serverIp.c_str(), serverPort);

    string msg;
    // 收发消息
    while (true) {
        printf("Please enter$ ");
        getline(cin, msg);
        write(socketFd, msg.c_str(), msg.size());

        char buf[1024];
        ssize_t n = read(socketFd, buf, sizeof(buf) - 1);
        if(n > 0) {
            buf[n] = 0;
            printf("%s\n", buf); 
        }
    }

    close(socketFd);
}

3.2 并发的 tcpServer

当前的tcpServer.hpp仅支持单进程版本,如果再有一个client想要访问该服务器,消息会发不过去,因为server一直在service()里,直到上一个client退出

3.2.1 多进程版本

父进程和孙子进程并发执行,孙子进程会被系统进程领养。(当然也可以设置waitpid()option参数为WNOHANG,或者设置signal(SIGCHLD, SIG_IGN)

cpp 复制代码
void TcpServer::run()
{
    for(;;) {
        // ...
        pid_t pid = fork();
        if(pid < 0) {
            log(WARNING, "fork()失败, 原因:%s\n", strerror(errno));
            sleep(1);
            continue;
        } else if(pid == 0) {
            close(_listenFd);           // 关闭_listenFd的目的是为了防止子进程修改改文件描述符下的内容
            if(fork() > 0)  exit(0);    // 关闭子进程,让孙子进程执行,防止父进程在waitpid的时候阻塞
            service(socketFd, ip, port);
            close(socketFd);            // 不要忘了关闭socketFd
            exit(0);
        }
        // 父进程
        close(socketFd);            // 父进程关闭socketFd的意义是为了防止文件描述符越用越少,关闭了之后能保证每一个新的进程用的都是fd都是4
        waitpid(pid, nullptr, 0); 
    }
}

3.2.2 多线程版本

cpp 复制代码
void TcpServer::run()
{
    for(;;) {
        // ...
        thread([=]      // 这里为什么必须用=, 用&就会失败?
        {
            service(socketFd, ip, port);
            close(socketFd);
            return 0;
        }).detach();
    }
}

3.2.3 线程池版本

每个线程得到结果后就让它退出

cpp 复制代码
void TcpServer::run()
{
    ThreadPool<Task>::GetInstance()->Start();
    for(;;) {
        // ...
        Task t(socketFd, ip, port);
        ThreadPool<Task>::GetInstance()->Push(t);
    }
}
cpp 复制代码
// Task.hpp
Log log;

class Task
{
public:
    Task(const int &socketFd, const string &ip, const uint16_t &port) 
    : _socketFd(socketFd), _ip(ip), _port(port)
    {}

    void Run()
    {
        // 每个线程得到结果后就让它退出
        char buf[1024];
        // 从socketFd中读取数据
        ssize_t n = read(_socketFd, buf, sizeof (buf) - 1);
        if(n > 0) {
            buf[n] = 0;          // 确保字符串以空字符结尾
            // 写回客户端
            string echoString = "server get a message$ ";
            echoString += buf;
            printf("client say$ %s\n", buf);
            write(_socketFd, echoString.c_str(), echoString.size());
        } else if(n == 0) {
            log(INFO, "客户端 %s:%d 退出, 关闭fd: %d\n", _ip.c_str(), _port, _socketFd);
        } else {
            log(WARNING, "读取客户端 %s:%d 信息失败, 关闭fd: %d\n", _ip.c_str(), _port, _socketFd);
        }
        close(_socketFd);       // 注意这里不要忘记close()
    }
private:
    int _socketFd;
    string _ip;
    uint16_t _port;
};

补充一下:上面写操作也要做差错处理,对write的返回值做判断。实际开发中,如果出现服务端正准备向客户端写,而客户端把文件描述符关闭的情况,则可能出现问题。这里类似管道,一端关闭了读,另一端写时会有SIGPIPE13号信号,所以经常会有signal(SIGPIPE, SIG_IGN);这样可以避免服务器因为客户端退出而异常终止。

3.3 继续完善

3.3.1 增加客户端重连功能

将之前的Task.hpp改为可以翻译字符的功能

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include "Init.hpp"
using namespace std;

Log log;
Init init;

class Task
{
public:
    Task(const int &socketFd, const string &ip, const uint16_t &port) 
    : _socketFd(socketFd), _ip(ip), _port(port)
    {}

    void Run()
    {
        // 每个线程得到结果后就让它退出
        char buf[1024];
        // 从socketFd中读取数据
        ssize_t n = read(_socketFd, buf, sizeof (buf) - 1);
        if(n > 0) {
            buf[n] = 0;          // 确保字符串以空字符结尾
            // 写回客户端
            string echoString = "server get a message$ ";
            echoString += init.translation(buf);
            printf("client say$ %s\n", buf);
            n = write(_socketFd, echoString.c_str(), echoString.size());
            if(n == -1) {
                log(WARNING, "向客户端 %s:%d 发送信息失败, 关闭fd: %d\n", _ip.c_str(), _port, _socketFd);
            }
        } else if(n == 0) {
            log(INFO, "客户端 %s:%d 退出, 关闭fd: %d\n", _ip.c_str(), _port, _socketFd);
        } else {
            log(WARNING, "读取客户端 %s:%d 信息失败, 关闭fd: %d\n", _ip.c_str(), _port, _socketFd);
        }
        close(_socketFd);       // 注意这里不要忘记close()
    }
private:
    int _socketFd;
    string _ip;
    uint16_t _port;
};

// Init.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";
extern Log log;
//yellow:黄色...
static bool Split(std::string &s, std::string *part1, std::string *part2)
{
    auto pos = s.find(sep);
    if(pos == std::string::npos) return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos+1);
    return true;
}

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            log(FATAL, "ifstream open %s error", dictname.c_str());
            exit(1);
        }
        std::string line;
        while(std::getline(in, line))
        {
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
        }
        in.close();
    }
    std::string translation(const std::string &key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end()) return "Unknow";
        else return iter->second;
    }
private:
    std::unordered_map<std::string, std::string> dict;
};

这样,Task.hpp中的run()方法通过调用Init.hpp中的translation()方法,就可以对来自客户端的内容进行翻译

让我们的TcpClient.cc每次发送消息都建立新的连接,并且增加重连方法。

cpp 复制代码
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <sys/types.h>        
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "log.hpp"
using namespace std;
#define MAXSLEEP 8

Log log;
void Usage(const char* string)
{
    cout << "\n\rUsage: " << string << " serverIp serverPort\n\n";  
}

int connRetry(int domain, int type, int protocol, const struct sockaddr* addr, socklen_t alen, bool &flag)
{
    int socketFd = -1;
    flag = false;
    for(int numSec = 1; numSec <= MAXSLEEP; numSec <<= 1) {
        // 重新创建套接字
        if((socketFd = socket(domain, type, protocol)) == -1) return -1;
        
        if(connect(socketFd, addr, alen) == 0) {
            // 连接成功
            flag = true;
            return socketFd;
        } 

        close(socketFd);        // 如果connect失败,关闭套接字用于下一次重连

        if(numSec <= MAXSLEEP / 2) {
            // 睡一会,等待下次重连
            printf("客户端重连服务端中, numSec: %d...\n", numSec);
            sleep(numSec);
        }
    }
    flag = false;
}

int 
main(int argc, char* argv[])
{
    if(argc != 3) {
        Usage(argv[0]);
        return -1;
    }

    // 获得服务端的IP和端口号
    string serverIp = argv[1];
    uint16_t serverPort = stoi(argv[2]);
    // 构建服务器信息
    sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_pton(AF_INET, serverIp.c_str(), &(server.sin_addr));

    while(true) {
        // 创建套接字 + connect
        bool flag = false;
        int socketFd = connRetry(AF_INET, SOCK_STREAM, 0, (sockaddr*)&server, sizeof(server), flag);
        if(flag == false) {
            cerr << "连接服务端出错!" << endl;
            break;
        }
        printf("连接成功!\n");
        
        // 收发消息
        string msg;
        printf("Please enter$ ");
        getline(cin, msg);
        ssize_t n = write(socketFd, msg.c_str(), msg.size());
        if(n == -1) {
            log(WARNING, "客户端写失败, 原因: %s\n", strerror(errno));
            continue;
        }
        
        char buf[1024];
        n = read(socketFd, buf, sizeof(buf) - 1);
        if(n > 0) {
            buf[n] = 0;
            printf("%s\n", buf); 
        } else if(n == 0) {
            log(WARNING, "客户端什么也没有读到!\n");
            continue;
        } else {
            log(WARNING, "客户端读取出错, 原因%s\n", strerror(errno));
            continue;
        }

        close(socketFd);
    } 
}

3.3.2 守护进程

守护进程(daemon)是生存期长的一种进程。通常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们实在后台运行的,linux系统有很多守护进程,它们执行日常事务活动。

有下面的程序

cpp 复制代码
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    while(true) {
        cout << "A message" << endl;
        sleep(1);
    }
    return 0;
}

运行该程序,然后用户退出登录,该进程的状态会变成这样(下图运行了4个程序)

可以看到,ppid(父进程)变为1,tty(终端名称)是?,说明之前的4个process程序变成了孤儿进程,被1号进程领养了。

若不想让该进程受到用户登录和注销的影响,就需要守护进程化。具体操作是让该进程自成一个会话(session)

进程通过调用setsid()建立一个新会话

c 复制代码
#include <unistd.h>

pid_t setsid(void);
																返回值:若成功,返回进程组ID(PGID);若出错,返回-1,错误码被设置

如果调用该函数的进程是一个进程组的组长,则该函数返回出错。为了保证不处于这种情况,通常先调用fork(),然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID是新分配的,两者不可能相等,这样就保证了子进程不是一个进程组的组长。

下面的函数可以由一个想要初始化守护进程的程序调用

cpp 复制代码
// daemon.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;
const string nullPath = "/dev/null";

void deamonize(const string& cwd = "")
{
    pid_t pid;
    // 1. 忽略其它异常信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 让其变成一个独立的会话
    if((pid = fork()) < 0) {
        printf("创建子进程失败!\n");
        return;
    } else if(pid == 0)    exit(0);
    setsid();

    // 3. 更改工作目录(可选)
    if(cwd != "") {
        if(chdir(cwd.c_str()) < 0) {
            printf("更改目录失败!\n");
            return;
        }
    }

    /*
    4. 将标准输入,标准输出,标准错误重定向到nullPath
    这样,任何一个试图读标准输入,写标准输出或标准错误的库例程都不会产生任何效果。
    因为守护进程不与终端设备相关联,所以其输出无从显示,也无处从交互式用户那里产生任何效果
    */ 
    int fd = open(nullPath.c_str(), O_RDWR);
    if(fd > 0) {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    } else {
        printf("打开nullPath失败\n");
        return;
    }
}

将服务器守护进程话

cpp 复制代码
// tcpServer.hpp
void TcpServer::run()
{
    deamonize();
    ThreadPool<Task>::GetInstance()->Start();
    for(;;) {
        sockaddr_in client;
        socklen_t len = sizeof(client);
        // 接受请求
        int socketFd = accept(_listenFd, (sockaddr*)&client, &len);

        if(socketFd == -1) {
            log(WARNING, "接受请求失败, %s\n", strerror(errno));
            sleep(1);
            continue;
        }

        // 获得客户端的ip和端口号
        uint16_t port = ntohs(client.sin_port);
        char ip[32];       // INET_ADDRSTRLEN 是16
        memset(ip, 0, sizeof ip);

        if(inet_ntop(AF_INET, &(client.sin_addr), ip, 32) == nullptr) {
            log(FATAL, "inet_ntop, IP不合法, %s\n", strerror(errno));
            exit(IP_ERR); 
        }

        log(INFO, "获得了一个新链接, 新的socket fd: %d, 客户端ip: %s, 客户端端口号: %d\n", socketFd, ip, port);

        Task t(socketFd, ip, port);
        ThreadPool<Task>::GetInstance()->Push(t);
    }
}

可以看到,在18056下只有myServerd这一个进程,意味着守护进程在一个孤儿进程组中。它自成一个会话,自成一个组。

就算我们再次重启xshell,该守护进程也会存在。所以客户端可以无时无刻的访问该服务器,非常酷。

相关推荐
獨枭2 小时前
CMake 构建项目并整理头文件和库文件
c++·github·cmake
小王爱吃月亮糖3 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
007php0074 小时前
linux服务器上CentOS的yum和Ubuntu包管理工具apt区别与使用实战
linux·运维·服务器·ubuntu·centos·php·ai编程
djykkkkkk4 小时前
ubuntu编译遇到的问题
linux·运维·ubuntu
qq_429856575 小时前
linux 查看服务是否开机自启动
linux·运维·服务器
我要学编程(ಥ_ಥ)5 小时前
初始JavaEE篇 —— 网络原理---传输层协议:深入理解UDP/TCP
java·网络·tcp/ip·udp·java-ee
小哈龙5 小时前
c++ 类似与c# 线程 AutoResetEvent 和 ManualResetEvent的实现
c++·c#·多线程
百事可乐☆5 小时前
全局webSocket 单个页面进行监听并移除单页面监听
网络·websocket·网络协议
yuanbenshidiaos6 小时前
C++--------------树
java·数据库·c++
海螺姑娘的小魏6 小时前
Effective C++ 条款 15:在资源管理类中提供对原始资源的访问
开发语言·c++