多进程网络服务端详细说明文档

发布于:2025-02-28 ⋅ 阅读:(10) ⋅ 点赞:(0)

多进程网络服务端详细说明文档

一、概述

本项目实现了一个基于多进程的 TCP 网络服务端,主要用于处理多个客户端的连接请求。为了提高代码的可维护性和可复用性,分成了头文件(.h)和多个源文件(.cpp)。具体包含 ctcpserver.hctcpserver.cppmulti_process_network_server.cpp 三个文件。

二、文件结构及功能
1. ctcpserver.h
  • 功能:该头文件主要用于声明 ctcpserver 类,定义了类的成员变量和成员函数接口,起到了对类的功能进行抽象和声明的作用,方便其他源文件引用该类。
  • 关键内容
    • 成员变量
      • m_listenfd:监听的 socket 描述符,-1 表示未初始化。
      • m_clientfd:客户端连接的 socket 描述符,-1 表示客户端未连接。
      • m_clientip:客户端的 IP 地址,以字符串形式存储。
      • m_port:服务端用于通信的端口号。
    • 成员函数声明:包含了构造函数、析构函数以及一系列用于初始化服务端、接受客户端连接、发送和接收数据、关闭 socket 等操作的函数声明。
class ctcpserver {
private:
    int m_listenfd;
    int m_clientfd;
    std::string m_clientip;
    unsigned short m_port;
public:
    ctcpserver();
    ~ctcpserver();
    bool initserver(const unsigned short in_port);
    bool accept();
    const std::string & clientip() const;
    bool send(const std::string &buffer);
    bool recv(std::string &buffer, const size_t maxlen);
    bool closelisten();
    bool closeclient();
};
2. ctcpserver.cpp
  • 功能:该源文件实现了 ctcpserver.h 中声明的 ctcpserver 类的所有成员函数,是类的具体实现部分。
  • 关键函数及实现细节
    • 构造函数 ctcpserver::ctcpserver():初始化 m_listenfdm_clientfd-1,表示初始状态下监听 socket 和客户端 socket 未初始化。
    • 析构函数 ctcpserver::~ctcpserver():调用 closelisten()closeclient() 函数,确保在对象销毁时关闭监听 socket 和客户端 socket,释放资源。
    • initserver 函数
      • 使用 socket() 函数创建一个 TCP 套接字,使用 IPv4 协议(AF_INET)和面向连接的 TCP 协议(SOCK_STREAM)。
      • 填充 sockaddr_in 结构体,设置协议族、端口号(使用 htons() 进行字节序转换)和 IP 地址(INADDR_ANY 表示监听所有可用 IP)。
      • 使用 bind() 函数将套接字绑定到指定的 IP 和端口,若绑定失败则关闭套接字并返回 false
      • 使用 listen() 函数将套接字设置为监听状态,允许最多 5 个客户端连接请求进入队列,若设置失败则关闭套接字并返回 false
    • accept 函数
      • 使用 accept() 函数从监听队列中取出一个客户端连接请求,返回一个新的套接字描述符用于与该客户端通信。
      • 使用 inet_ntoa() 函数将客户端的 IP 地址从大端序转换为字符串格式并存储在 m_clientip 中。
    • send 函数:检查客户端 socket 是否有效,若有效则使用 send() 函数向客户端发送数据。
    • recv 函数
      • 清空接收缓冲区并调整其大小为指定的最大长度。
      • 使用 recv() 函数接收客户端发送的数据,若接收失败则清空缓冲区并返回 false,否则根据实际接收的字节数调整缓冲区大小并返回 true
    • closelisten 函数:关闭监听 socket 并将 m_listenfd 设置为 -1
    • closeclient 函数:关闭客户端 socket 并将 m_clientfd 设置为 -1
3. multi_process_network_server.cpp
  • 功能:该文件是整个服务端程序的入口,包含 main 函数和信号处理函数,负责初始化服务端、处理客户端连接和信号处理。
  • 关键流程及函数说明
    • main 函数
      • 参数检查:检查命令行参数是否正确,若不正确则输出使用说明并退出程序。
      • 信号处理设置
        • 使用 for 循环忽略所有信号,避免程序被不必要的信号干扰,同时解决僵尸进程问题。
        • 设置 SIGTERMSIGINT 信号的处理函数为 FathEXIT,允许通过 kill 命令或 Ctrl + C 正常终止程序。
      • 服务端初始化:调用 tcpserver.initserver() 函数初始化服务端监听 socket,若初始化失败则输出错误信息并退出。
      • 客户端连接处理
        • 使用 while 循环不断接受客户端连接请求,若接受失败则输出错误信息并退出。
        • 调用 fork() 函数创建子进程,父进程关闭客户端 socket 并继续等待下一个客户端连接;子进程关闭监听 socket,重新设置信号处理函数,并与客户端进行通信。
      • 子进程通信处理
        • 循环接收客户端发送的数据,若接收失败则退出循环。
        • 接收到数据后,向客户端发送 “ok” 作为响应,若发送失败则退出循环。
    • 信号处理函数
      • FathEXIT 函数:处理父进程接收到的 SIGTERMSIGINT 信号。首先屏蔽这两个信号,防止信号处理函数被中断;输出父进程退出信息,使用 kill(0, SIGTERM) 向所有子进程发送终止信号;关闭监听 socket 并退出父进程。
      • ChldEXIT 函数:处理子进程接收到的 SIGTERM 信号。屏蔽 SIGINTSIGTERM 信号,输出子进程退出信息,关闭客户端 socket 并退出子进程。
四、注意事项
  • 资源管理:每个子进程在处理完与客户端的通信后必须退出,否则会继续进入 accept 函数,导致错误。同时,要确保在程序结束时正确关闭所有 socket 资源,避免资源泄漏。
  • 信号处理:信号处理函数中屏蔽了 SIGINTSIGTERM 信号,防止信号处理函数被中断,确保程序的稳定性。
  • 高并发场景:该程序使用多进程处理客户端连接,每个客户端连接会创建一个新的子进程,可能会消耗较多的系统资源。在高并发场景下,可以考虑使用多线程或异步 I/O 技术进行优化。

1. ctcpserver.h 头文件

#ifndef CTCP_SERVER_H
#define CTCP_SERVER_H

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

class ctcpserver {
private:
    int m_listenfd;        // 监听的socket,-1表示未初始化
    int m_clientfd;        // 客户端连上来的socket,-1表示客户端未连接
    std::string m_clientip;     // 客户端字符串格式的IP
    unsigned short m_port; // 服务端用于通讯的端口
public:
    ctcpserver();
    ~ctcpserver();

    // 初始化服务端用于监听的socket
    bool initserver(const unsigned short in_port);

    // 受理客户端的连接
    bool accept();

    // 获取客户端的IP(字符串格式)
    const std::string & clientip() const;

    // 向对端发送报文
    bool send(const std::string &buffer);

    // 接收对端的报文
    bool recv(std::string &buffer, const size_t maxlen);

    // 关闭监听的socket
    bool closelisten();

    // 关闭客户端连上来的socket
    bool closeclient();
};

#endif

2. ctcpserver.cpp 源文件

#include "ctcpserver.h"

// 构造函数,初始化监听socket和客户端socket为未初始化状态
ctcpserver::ctcpserver() : m_listenfd(-1), m_clientfd(-1) {}

// 析构函数,关闭监听socket和客户端socket
ctcpserver::~ctcpserver() {
    closelisten();
    closeclient();
}

// 初始化服务端用于监听的socket
bool ctcpserver::initserver(const unsigned short in_port) {
    // 第1步:创建服务端的socket
    // AF_INET表示使用IPv4协议,SOCK_STREAM表示使用面向连接的TCP协议
    if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;

    m_port = in_port;

    // 第2步:把服务端用于通信的IP和端口绑定到socket上
    struct sockaddr_in servaddr;  // 用于存放协议、端口和IP地址的结构体
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;  // 协议族,固定填AF_INET
    servaddr.sin_port = htons(m_port);  // 指定服务端的通信端口,htons用于将主机字节序转换为网络字节序
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 如果操作系统有多个IP,全部的IP都可以用于通讯

    // 绑定服务端的IP和端口(为socket分配IP和端口)
    if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        close(m_listenfd);
        m_listenfd = -1;
        return false;
    }

    // 第3步:把socket设置为可连接(监听)的状态
    // 5表示监听队列的最大长度
    if (listen(m_listenfd, 5) == -1) {
        close(m_listenfd);
        m_listenfd = -1;
        return false;
    }

    return true;
}

// 受理客户端的连接(从已连接的客户端中取出一个客户端)
// 如果没有已连接的客户端,accept()函数将阻塞等待
bool ctcpserver::accept() {
    struct sockaddr_in caddr;  // 客户端的地址信息
    socklen_t addrlen = sizeof(caddr);  // struct sockaddr_in的大小
    // 从监听队列中取出一个客户端连接
    if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1) return false;

    m_clientip = inet_ntoa(caddr.sin_addr);  // 把客户端的地址从大端序转换成字符串

    return true;
}

// 获取客户端的IP(字符串格式)
const std::string & ctcpserver::clientip() const {
    return m_clientip;
}

// 向对端发送报文,成功返回true,失败返回false
bool ctcpserver::send(const std::string &buffer) {
    if (m_clientfd == -1) return false;

    // 发送报文
    if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0) return false;

    return true;
}

// 接收对端的报文,成功返回true,失败返回false
// buffer - 存放接收到的报文的内容,maxlen - 本次接收报文的最大长度
bool ctcpserver::recv(std::string &buffer, const size_t maxlen) {
    buffer.clear();  // 清空容器
    buffer.resize(maxlen);  // 设置容器的大小为maxlen
    // 直接操作buffer的内存接收数据
    int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0);
    if (readn <= 0) {
        buffer.clear();
        return false;
    }
    buffer.resize(readn);  // 重置buffer的实际大小

    return true;
}

// 关闭监听的socket
bool ctcpserver::closelisten() {
    if (m_listenfd == -1) return false;

    ::close(m_listenfd);
    m_listenfd = -1;
    return true;
}

// 关闭客户端连上来的socket
bool ctcpserver::closeclient() {
    if (m_clientfd == -1) return false;

    ::close(m_clientfd);
    m_clientfd = -1;
    return true;
}

3. multi_process_network_server.cpp 主源文件

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "ctcpserver.h"

ctcpserver tcpserver;

// 父进程的信号处理函数
void FathEXIT(int sig);
// 子进程的信号处理函数
void ChldEXIT(int sig);

int main(int argc, char *argv[]) {
    // 检查命令行参数
    if (argc != 2) {
        std::cout << "Using:./multi_process_network_server 通讯端口\nExample:./multi_process_network_server 5005\n\n";
        std::cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
        std::cout << "      如果是云服务器,还要开通云平台的访问策略。\n\n";
        return -1;
    }

    // 忽略全部的信号,不希望被打扰。顺便解决了僵尸进程的问题
    for (int ii = 1; ii <= 64; ii++) signal(ii, SIG_IGN);

    // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
    // 但请不要用 "kill -9 +进程号" 强行终止
    signal(SIGTERM, FathEXIT);
    signal(SIGINT, FathEXIT);  // SIGTERM 15 SIGINT 2

    // 初始化服务端用于监听的socket
    if (tcpserver.initserver(atoi(argv[1])) == false) {
        perror("initserver()");
        return -1;
    }

    while (true) {
        // 受理客户端的连接(从已连接的客户端中取出一个客户端)
        // 如果没有已连接的客户端,accept()函数将阻塞等待
        if (tcpserver.accept() == false) {
            perror("accept()");
            return -1;
        }

        // 创建子进程处理客户端连接
        int pid = fork();
        if (pid == -1) {
            perror("fork()");
            return -1;
        }  // 系统资源不足
        if (pid > 0) {
            // 父进程
            tcpserver.closeclient();  // 父进程关闭客户端连接的socket
            continue;  // 父进程返回到循环开始的位置,继续受理客户端的连接
        }

        // 子进程
        tcpserver.closelisten();  // 子进程关闭监听的socket

        // 子进程需要重新设置信号
        signal(SIGTERM, ChldEXIT);  // 子进程的退出函数与父进程不一样
        signal(SIGINT, SIG_IGN);  // 子进程不需要捕获SIGINT信号

        // 子进程负责与客户端进行通讯
        std::cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";

        std::string buffer;
        while (true) {
            // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待
            if (tcpserver.recv(buffer, 1024) == false) {
                perror("recv()");
                break;
            }
            std::cout << "接收:" << buffer << std::endl;

            buffer = "ok";
            if (tcpserver.send(buffer) == false) {  // 向对端发送报文
                perror("send");
                break;
            }
            std::cout << "发送:" << buffer << std::endl;
        }

        return 0;  // 子进程一定要退出,否则又会回到accept()函数的位置
    }
}

// 父进程的信号处理函数
void FathEXIT(int sig) {
    // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断
    signal(SIGINT, SIG_IGN);
    signal(SIGTERM, SIG_IGN);

    std::cout << "父进程退出,sig=" << sig << std::endl;

    kill(0, SIGTERM);  // 向全部的子进程发送15的信号,通知它们退出

    // 在这里增加释放资源的代码(全局的资源)
    tcpserver.closelisten();  // 父进程关闭监听的socket

    exit(0);
}

// 子进程的信号处理函数
void ChldEXIT(int sig) {
    // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断
    signal(SIGINT, SIG_IGN);
    signal(SIGTERM, SIG_IGN);

    std::cout << "子进程" << getpid() << "退出,sig=" << sig << std::endl;

    // 在这里增加释放资源的代码(只释放子进程的资源)
    tcpserver.closeclient();  // 子进程关闭客户端连上来的socket

    exit(0);
}

代码说明

  • ctcpserver.h:定义了 ctcpserver 类的接口,包括类的成员变量和成员函数的声明。使用了预处理器指令 #ifndef#define#endif 来防止头文件被重复包含。
  • ctcpserver.cpp:实现了 ctcpserver 类的成员函数,包括构造函数、析构函数以及各种操作函数。
  • multi_process_network_server.cpp:主源文件,包含了 main 函数和信号处理函数,使用 ctcpserver 类来实现多进程的网络服务端。

编译和运行

将上述三个文件放在同一目录下,使用以下命令进行编译:

g++ -o multi_process_network_server ctcpserver.cpp multi_process_network_server.cpp

编译成功后,使用以下命令运行服务端程序:

./multi_process_network_server 5005

其中 5005 是服务端监听的端口号,你可以根据需要修改。