[Linux] 网络编程 - 初见TCP套接字编程: 实现简单的单进程、多进程、多线程、线程池tcp服务器

news2024/9/23 21:37:55

|cover


网络的上一篇文章, 我们介绍了网络变成的一些重要的概念, 以及 UDP套接字的编程演示. 还实现了一个简单更简陋的UDP公共聊天室.

[Linux] 网络编程 - 初见UDP套接字编程: 网络编程部分相关概念、TCP、UDP协议基本特点、网络字节序、socket接口使用、简单的UDP网络及聊天室实现…

本篇文章, 我们来介绍一下 TCP套接字编程的接口 及 演示.


TCP套接字

在介绍UDP套接字编程的文章中, 列出了一些接口:

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

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

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

// 发送报文 (UDP)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

// 接收报文 (UDP)
ssize_t recvfrom(int socket, void* restrict buffer, size_t length, int flags, struct sockaddr* restrict address, socklen_t* restrict address_len);

其中已经演示使用了: socket()bind()sendto()recvfrom()

并且, 也介绍了UDP和TCP的部分特点. 最主要的就是:

  1. UDP非连接, 面向数据包
  2. TCP连接, 面向字节流

所以, TCP多了三个用于连接的接口: connect()listen()accept() 这三个接口具体怎么使用, 下面实现简单的TCP网络通信时 介绍一下.

简单的TCP网络通信

在介绍过 UDP网络编程之后. TCP网络编程没有很多需要新介绍的内容.

先定义一个头文件, 用来包含需要使用到的一些头文件、定义一些宏:

util.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "logMessage.hpp"

#define SOCKET_ERR  1
#define BIND_ERR    2
#define LISTEN_ERR  3
#define USE_ERR     4
#define CONNECT_ERR	5
#define FORK_ERR	6

#define BUFFER_SIZE	1024

还有日志打印相关头文件:

logMessage.hpp:

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <cstdlib>

// 宏定义 四个日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char* log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};

// 实现一个 可以输出: 日志等级、日志时间、用户、以及相关日志内容的 日志消息打印接口
void logMessage(int level, const char* format, ...) {
    // 通过可变参数实现, 传入日志等级, 日志内容格式, 日志内容相关参数

    // 确保日志等级正确
    assert(level >= DEBUG);
    assert(level <= FATAL);

    // 获取当前用户名
    char* name = getenv("USER");

    // 简单的定义log缓冲区
    char logInfo[1024];

    // 定义一个指向可变参数列表的指针
    va_list ap;
    // 将 ap 指向可变参数列表中的第一个参数, 即 format 之后的第一个参数
    va_start(ap, format);

    // 此函数 会通过 ap 遍历可变参数列表, 然后根据 format 字符串指定的格式, 将ap当前指向的参数以字符串的形式 写入到logInfo缓冲区中
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    // ap 使用完之后, 再将 ap置空
    va_end(ap); // ap = NULL

    // 通过判断日志等级, 来选择是标准输出流还是标准错误流
    FILE* out = (level == FATAL) ? stderr : stdout;

    // 获取本地时间
    time_t tm = time(nullptr);
    struct tm* localTm = localtime(&tm);
    char* localTmStr = asctime(localTm);
    char* nC = strstr(localTmStr, "\n");
    if(nC) {
        *nC = '\0';
    }
    fprintf( out, "%s | %s | %s | %s\n", 
            log_level[level],
            localTmStr,
            name == nullptr ? "unknow" : name, 
            logInfo );
}

先实现一个没有实际功能的、只能连接TCP服务器:

tcpServer.cc:

#include "util.hpp"

class tcpServer {
public:
    tcpServer(uint16_t port, const std::string& ip = "")
        : _port(port)
        , _ip(ip) {}

    void init() {
        // 先创建套接字文件描述符
        // 不过, 与UDP不同的是 TCP是面向字节流的, 所以套接字数据类型 要使用 流式套接字
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);

        if (_listenSock < 0) {
            // 套接字文件描述符创建失败
            logMessage(FATAL, "socket() faild:: %s : %d", strerror(errno), _listenSock);
            exit(SOCKET_ERR); // 创建套接字失败 以 SOCKET_ERR 退出
        }
        logMessage(DEBUG, "socket create success: %d", _listenSock);

        // 套接字创建成功, 就需要将向 sockaddr_in 里填充网络信息
        // 并将进程网络信息绑定到主机上
        struct sockaddr_in local;
        std::memset(&local, 0, sizeof(local));

        // 填充网络信息
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        _ip.empty() ? (local.sin_addr.s_addr = htonl(INADDR_ANY)) : (inet_aton(_ip.c_str(), &local.sin_addr));

        // 绑定网络信息到主机
        if (bind(_listenSock, (const struct sockaddr*)&local, sizeof(local)) == -1) {
            // 绑定失败
            logMessage(FATAL, "bind() faild:: %s : %d", strerror(errno), _listenSock);
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "socket bind success : %d", _listenSock);
        // 绑定了网络信息之后, 不同于 UDP, TCP是面向连接的.
        // 所以 在TCP服务器绑定了进程网络信息到内核中之后, 其他主机就有可能向服务器发送连接请求了
        // 然后, 所以 在绑定了网络信息之后, 要做的事就是 监听套接字
        // 监听是否有其他主机发来连接请求, 需要用到接口 listen()
        if (listen(_listenSock, 5) == -1) {
            logMessage(FATAL, "listen() faild:: %s : %d", strerror(errno), _listenSock);
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen success : %d", _listenSock);
        // 开始监听之后, 别的主机就可以发送连接请求了.
    }

    // 服务器初始化完成之后, 就可以启动了
    void loop() {
        while (true) {
            struct sockaddr_in peer;          // 输出型参数 接受所连接主机客户端网络信息
            socklen_t peerLen = sizeof(peer); // 输入输出型参数

            // 使用 accept() 接口, 接受来自其他网络客户端的连接
            // 成功会返回一个文件描述符, 失败则返回-1
            // 此函数是阻塞式的, 也就是说 在没有连接发送过来之前 进程会处于阻塞状态
            int serviceSock = accept(_listenSock, (struct sockaddr*)&peer, &peerLen);
            if (serviceSock == -1) {
                logMessage(WARINING, "accept() faild:: %s : %d", strerror(errno), serviceSock);
                continue;
            }
            // 走到这里, 就表示连接成功了
            // 连接成功之后, 就可以获取到连接客户端的网络信息了:
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIP = inet_ntoa(peer.sin_addr);
            logMessage(DEBUG, "accept success: [%s: %d] | %d ", peerIP.c_str(), peerPort, serviceSock);
        }
    }

private:
    uint16_t _port;  // 端口号
    int _listenSock; // 服务器套接字文件描述符
    std::string _ip;
};

void Usage(std::string proc) {
    std::cerr << "Usage:: \n\t" << proc << " port ip" << std::endl;
    std::cerr << "example:: \n\t" << proc << " 8080 127.0.0.1" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3 && argc != 2) {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3) {
        ip = argv[2];
    }

    tcpServer svr(port, ip);

    svr.init();
    svr.loop();

    return 0;
}

此代码中, 类的成员函数还是那几个:

  1. uint16_t _port, 用于存储服务器进程端口号

  2. int _listenSock, 用于存储服务器进程创建的 监听套接字

    关于什么是监听套接字, 下面分析代码时介绍.

  3. std::string _ip, 用于存储服务器进程IP

我们来分析一下 封装的类中的每个重要的成员函数:

  1. init() 初始化函数, 初始化服务器信息

    |wide

    初始化函数, 要执行的就是一个服务器再启动之前 需要做的工作.

    首先是 调用socket()创建套接字, 类接受套接字的变量中起了一个名字叫_listenSock.

    由于, TCP面向的是字节流通信, 所以socket()第二个参数传入 SOCK_STREAM, 表示面向字节流:

    |wide

    然后就是一系列无论是UDP还是TCP都要实现的:

    1. 将服务器网络信息 填充到 在用户栈创建的sockaddr_in结构体中, 需要传输到网络中的内容要以网络字节序存储

      |wide

    2. 将网络信息绑定到系统内核

      |wide

    不过, 下面的一个步骤 UDP就没有了.

    在之前实现的UDP网络通信中, bind()就是UDP服务器初始化的最后一步.

    而 TCP不同, 因为TCP通信是 需要建立连接 的. 所以, 这里的TCP服务器初始化要比UDP的多一个步骤: 监听

    即, 调用listen()接口, 让服务器开启监听状态.

    |wide

    执行listen(), 服务器会自动进入监听状态. 之后会一直监听 来自客户端 向服务器 发送的连接请求. 实际上监听的就是服务器的套接字. 监听的过程是非阻塞的.

    关于listen()接口的第二个参数int backlog在之后再介绍.

    当服务器开始监听之后, 可以说 就有能力看到来自其他客户端的连接请求了

    至此, 初始化完毕.

  2. loop() 服务器启动函数, 我们默认它是一个死循环

    |wide

    如果是UDP服务器, 启动之后 就可以接受客户端发送来的信息然后做处理了.

    但是TCP服务器不行, 因为TCP是 面向连接 的. 其他客户端想要将信息发送到服务器, 就必须先于服务器建立连接.

    而能否与服务器建立连接 是服务器说了算的. 只有 服务器接受了来自客户端的连接请求 才是连接成功 了.

    所以, 这里 TCP服务器开启的第一件事, 就是接受连接请求:

    |wide

    接受连接请求的接口是: accept()

    |wide

    accept()接口会按顺序接受来自客户端的连接请求. 并返回一个新的套接字文件描述符.

    这也就是说, 这个TCP服务器创建很多个套接字.

    而我们知道, UDP服务器中只会创建一个属于服务器的套接字.

    accept()新创建的套接字是什么呢? 与服务器原本的套接字有什么关系?

    我们知道, 服务器原本的套接字处于被监听的状态, 是为了能够接收到连接信息.

    accept()接受了客户端的连接请求时, 会创建一个存储有 服务器本地网络信息 以及 客户端网络信息的套接字, 服务器可以通过此套接字与客户端进行通信.

    即,

    1. 服务器原本的套接字

      服务器可以通过监听此套接字, 收到客户端的连接请求, 然后服务器就可以接受连接请求了.

      只用来与客户端建立连接.

      此套接字, 被称为 监听套接字. 所以 服务器类中的用于存储套接字的成员变量, 才起名_listenSock

    2. accept()创建的套接字

      此套接字包括 服务器本地网络信息 和 客户端网络信息. 并且 此套接字也会被绑定到系统内核中, 并与服务器原套接字相独立.

      服务器可以通过此套接字与客户端进行通信.

      此套接字, 主要是用来给客户端提供服务的套接字.

      所以, 接受此套接字的变量可以起名为serviceSock

    accept()接口的后两个参数, 还是用来接收客户端网络信息的输出型参数.

    建立了连接之后, 就可以与客户端进行通信了.

实现了之后, 其实就已经可连接到服务器了.

我们可以使用浏览器测试:

tcpServer

当我们使用浏览器访问服务器(IP:Port)时, 可以看到服务器accept()了连接请求. 但是由于服务器没有实现任何功能, 所以浏览器没有变化.

实现客户端通信

现在服务器并不具备任何的功能, 也不能接收来自客户端的信息.

服务器功能实现

下面我们来实现一个将客户端发来的信息小写转大写的功能.

void low2upService(int sock, const std::string& clientIP, const uint16_t& clientPort) {
    assert(sock > 0);
    assert(!clientIP.empty());

    // 一个用于存储来自客户端信息的数组
    char inbuffer[BUFFER_SIZE];
    while (true) {
        // TCP获取来自客户端的信息的操作就是 read
        // 从 服务器与客户端连接 的文件描述符中 读取来自客户端的信息
        // 可看作 通过文件描述符 从文件读取内容
        ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
        if (s > 0) {
            // 大于零 就是读取到数据了
            inbuffer[s] = '\0';
            // 我们实现一个操作, 如果 客户端传输过来的信息是 quit 这个单词, 就表示客户端请求退出
            // 就可以退出 服务循环了
            if (strcasecmp(inbuffer, "quit") == 0) { // strcasecmp 忽略大小写比较
                logMessage(DEBUG, "Client requests to quit: [%s: %d]", clientIP.c_str(), clientPort);
                break;
            }
            // 走到这里 就可以进行小写转大写了
            logMessage(DEBUG, "low2up before: [%s: %d] >> %s", clientIP.c_str(), clientPort, inbuffer);
            for (int i = 0; i < s; i++) {
                if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                    inbuffer[i] = toupper(inbuffer[i]);
            }
            logMessage(DEBUG, "low2up after: [%s: %d] >> %s", clientIP.c_str(), clientPort, inbuffer);

            // 上面做的都是对获取到的信息 进行转换
            // 最后需要做的就是 将转换后的信息 再重新回应给客户端
            // 而 回应给客户端 则是用 write, 可看做 通过文件描述符像文件写入内容
            write(sock, inbuffer, strlen(inbuffer));
        }
        else if (s == 0) {
            // s == 0, 表示什么?
            // 在管道通信中 read() 是阻塞式读取的. 此时 返回值为0, 表示管道的写入端关闭
            // 而 TCP类似, TCP中 read() 通常也是阻塞时读取的, 此时返回0, 表示客户端关闭
            // 所以此时, 该退出了
            logMessage(DEBUG, "Client has quited: [%s: %d]", clientIP.c_str(), clientPort);
            break;
        }
        else {
            // 到这里 本次 read() 出错
            logMessage(DEBUG, "Client [%s: %d] read:: %s", clientIP.c_str(), clientPort, strerror(errno));
            break;
        }
    }
    // 走到这里 循环已经退出了, 表示 client 也已经退出了
    // 所以 此时需要关闭文件描述符, 因为一个主机上的文件描述符数量是一定的, 达到上限之后 就无法再创建
    // 已经无用但没有被归还的文件描述符, 文件描述符泄漏
    close(sock);
    logMessage(DEBUG, "Service close %d sockFd", sock);
}

阅读代码 可以发现, 我们实现TCP通信 直接使用了read()write()接口.

因为, TCP是一种面向流的协议. 所以, 可以直接使用read()write()通过文件描述符 与 客户端进行通信.

此功能函数没有什么需要特别主义的点.

此成员函数实现之后, 就可以直接在loop()中调用此函数:

|inline

至此, 一个简单的可以接收客户端消息 并 将消息中小写字母转换成大写字母的服务器就实现完毕了.

客户端实现

服务器是可以接收消息, 也具有一定的功能了.

但是, 客户端还没有实现呢.

下面是客户端实现的代码:

#include "util.hpp"

volatile bool quit = false;

void Usage(std::string proc) {
    std::cerr << "Usage:: \n\t" << proc << " serverIP serverPort" << std::endl;
    std::cerr << "example:: \n\t" << proc << " 127.0.0.1 8080" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    std::string serverIP = argv[1];
    uint16_t serverPort = atoi(argv[2]);

    // 先创建套接字文件描述符
    int sockFd = socket(AF_INET, SOCK_STREAM, 0);

    if (sockFd < 0) {
        // 套接字文件描述符创建失败
        logMessage(FATAL, "socket() faild:: %s : %d", strerror(errno), sockFd);
        exit(SOCKET_ERR); // 创建套接字失败 以 SOCKET_ERR 退出
    }
    logMessage(DEBUG, "socket create success: %d", sockFd);

    // 客户端创建套接字之后, 首先需要做什么?
    // 服务器创建套接字之后, 需要填充绑定网络信息, 客户端也需要, 但是与UDP相同 不需要手动填充 与 bind
    // 需要listen吗? 不需要, 因为客户端不会被主动连接
    // 需要accept吗? 不需要
    // 此时 客户端需要做的是 获取填充服务器信息, 并向服务器请求连接!

    // 填充服务器基本网络信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_aton(serverIP.c_str(), &server.sin_addr);

    // 发送连接请求
    if (connect(sockFd, (const struct sockaddr*)&server, sizeof(server)) == -1) {
        // 连接失败
        logMessage(FATAL, "Client connect() faild: %d, %s", sockFd, strerror(errno));
        exit(CONNECT_ERR);
    }
    logMessage(DEBUG, "Client connect success.");

    // 连接成功之后, 就可以向服务器发送信息了
    std::string message;
    while (!quit) { // 根据退出状态 识别客户端是否退出
        message.clear();
        std::cout << "请输入消息 >> ";
        std::getline(std::cin, message); // 从命令行获取消息 到 message中
        if (strcasecmp(message.c_str(), "quit") == 0) {
            // 我们实现了 输入 quit 这个单词就向服务器请求退出 的功能
            // 所以, 在输入 quit 这个单词时, 表示 需要退出
            // 就要将 客户端的退出状态设置为 true, 让客户端不进入下一次循环
            quit = true;
        }

        ssize_t sW = write(sockFd, message.c_str(), message.size()); // 向客户端套接字文件描述符写入消息
        if (sW > 0) {
            // 写入成功, 就准备接收服务器的回复
            message.resize(BUFFER_SIZE); // 需要与服务器inbuffer大小一致
            ssize_t sR = read(sockFd, (char*)message.c_str(), BUFFER_SIZE);
            if (sR > 0) {
                message[sR] = '\0';
            }
            if (strcasecmp(message.c_str(), "quit")) {
                std::cout << "Server Echo>>> " << message << std::endl;
            }
        }
        else if (sW <= 0) {
            logMessage(FATAL, "Client write() faild: %d, %s", sockFd, strerror(errno));
            break;
        }
    }

    // 退出循环 客户端退出, 关闭文件描述符
    close(sockFd);

    return 0;
}

TCP客户端前面的实现 与UDP客户端前面的实现 步骤相同:

  1. 创建套接字

    UDP创建面向数据包的套接字, TCP则创建面向流的

  2. 填充服务器基本网络信息

之后, TCP客户端需要向TCP服务器发送连接请求, 只有在连接成功之后 才能进行通信.

连接请求的接口是:connect()

没错, connect()也会向sendto()那样 自动绑定客户端的网络信息.

连接成功之后, 就可以使用write()向服务器发送信息, 使用read()读取服务器的回复了. 这个过程中使用的文件描述符都是客户端套接字的文件描述符.

除了发送信息 接收回复功能的实现之外. 由于我们在服务器实现了 当客户端发送quit这个单词, 就表示客户端请求退出了. 所以客户端也要实现相应的功能.

具体的实现是:

|inline

通过一个bool变量标识退出状态, 如果输入了quit这个单词 就不进入下一个循环.

最后, 退出循环之后, 关闭文件描述符. 客户端退出.


整个服务器和客户端的执行演示:

但是, 这个版本的服务器是有缺陷的:

当多客户端尝试连接服务器时, 会发现 服务器只会对第一个连接的客户端进行响应.

这是为什么?

我们这个服务器是 单进程版本, 此时连接多个客户端会出问题

当客户端连接到服务器之后, 会调用low2upService()这个函数

但是 这个函数的主体是什么? 是一个死循环.

单进程调用函数进入一个死循环, 就出不来了!

后面的客户端再连接进来, 服务器是没办法响应的, 因为进程唯一的一个执行流在一个死循环内呢

要怎么解决这个问题呢?

那就要用到多进程或多线程了

多进程服务器

单进程的服务器, 在给多个客户端提供服务时 会出问题.

那就用多进程服务器解决.

v1

多进程服务器也很简单, 只需要改一下loop()中 执行low2upService()的部分就可以了:

void loop() {
    signal(SIGCHLD, SIG_IGN);       // 忽略子进程推出信号, 子进程退出时就会自动回收

    while (true) {
        struct sockaddr_in peer;
        socklen_t peerLen = sizeof(peer);

        int serviceSock = accept(_listenSock, (struct sockaddr*)&peer, &peerLen);
        if (serviceSock == -1) {
            logMessage(WARINING, "accept() faild:: %s : %d", strerror(errno), serviceSock);
            continue;
        }
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIP = inet_ntoa(peer.sin_addr);
        logMessage(DEBUG, "accept success: [%s: %d] | %d ", peerIP.c_str(), peerPort, serviceSock);

        // 连接到客户端之后, 就可以执行功能了
        // 执行转换功能 小写转大写

        // 多进程v1
        pid_t id = fork();
        if (id == -1) {
            logMessage(FATAL, "Server fork() faild: %s", strerror(errno));
            exit(FORK_ERR);
        }
        else if (id == 0) {
            // 进入子进程
            // 子进程会继承 父进程的文件描述符表, 但是这已经是两个不同的进程了
            // 所以建议进入子进程之后, 先关闭_listenSock, 防止子进程代码可能对此文件造成影响
            close(_listenSock);
            low2upService(serviceSock, peerIP.c_str(), peerPort);
            exit(0);
        }
        // 这里是父进程执行的内容
        close(serviceSock);
        // 父进程需要手动关闭 连接客户端创建的与客户端通信的文件描述符
        // 如果不关闭父进程会发生文件描述符泄漏
        // 父子进程在各子进程中关闭某文件描述符 是不影响对方的
    }
}

只需要使用fork()创建子进程, 然后将low2upService()放到子进程里执行就可以了.

|inline

不过需要注意的问题是:

  1. 子进程会继承父进程的文件描述符表, 也就意味着 父进程打开的'文件'子进程默认是打开的

    而且, 子进程与父进程已经是两个进程了, 无论是子进程还是父进程打开或关闭文件描述符, 都 不会影响对方的文件描述符状态

  2. _listenSock是父进程的监听套接字, 对子进程来说 无用.

    所以, 建议进入子进程先关闭此套接字, 避免子进程执行的代码可能会影响父进程的监听

  3. serviceSock是服务器与客户端通信的套接字.

    使用多进程, 变成了 子进程实现通信功能. 而 此套接字文件描述符的关闭操作 原本是在low2upService()中的, 现在子进程调用此函数, 所以子进程内不用再手动close(serviceSock)

    但是, 父进程已经不再执行low2upService(), 所以必须要close(serviceSock). 否则父进程会发生文件描述符泄漏

  4. 子进程退出时, 是需要父进程回收的.

    如果父进程使用wait()进行等待, 默认是阻塞式的 无法使用. 如果使用waitpid()非阻塞, 也需要存储所有子进程的pid, 很麻烦.

    所以, 我们直接在loop()函数刚开始 执行signal(SIGCHLD, SIG_IGN);忽略子进程的退出信号. 这样子进程退出时, 会被自动回收.

完成之后, 再打开服务器 和 多客户端:

可以发现, 已经可以多客户端连接并通信了.

v2

多进程的版本, 除了上面的. 还有一个版本

// 服务器初始化完成之后, 就可以启动了
void loop() {
    //signal(SIGCHLD, SIG_IGN); // 忽略子进程推出信号, 子进程退出时就会自动回收

    while (true) {
        struct sockaddr_in peer;          // 输出型参数 接受所连接主机客户端网络信息
        socklen_t peerLen = sizeof(peer); // 输入输出型参数

        // 使用 accept() 接口, 接受来自其他网络客户端的连接
        // 成功会返回一个文件描述符, 失败则返回-1
        // 此函数是阻塞式的, 也就是说 在没有连接发送过来之前 进程会处于阻塞状态
        int serviceSock = accept(_listenSock, (struct sockaddr*)&peer, &peerLen);
        if (serviceSock == -1) {
            logMessage(WARINING, "accept() faild:: %s : %d", strerror(errno), serviceSock);
            continue;
        }
        // 走到这里, 就表示连接成功了
        // 连接成功之后, 就可以获取到连接客户端的网络信息了:
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIP = inet_ntoa(peer.sin_addr);
        logMessage(DEBUG, "accept success: [%s: %d] | %d ", peerIP.c_str(), peerPort, serviceSock);

        // 连接到客户端之后, 就可以执行功能了
        // 执行转换功能 小写转大写

        // 多进程v2
        pid_t id = fork();
        if (id == -1) {
            logMessage(FATAL, "Server fork() faild: %s", strerror(errno));
            exit(FORK_ERR);
        }
        else if (id == 0) {
            // 子进程
            close(_listenSock);
            // 再创建子进程, 并且此进程退出
            if (fork() > 0) {
                exit(0);
            }
            // 这部分是再次创建的子进程 执行的, 也就是孙子进程, 因为父进程在创建出孙子进程时就推出了
            low2upService(serviceSock, peerIP.c_str(), peerPort);
            exit(0);
        }
        close(serviceSock);

        // 等待id子进程
        int ret = waitpid(id, nullptr, 0);
        // 就算是阻塞式的等待也无所谓, 因为 创建成功后马上就退出了
        if (ret == -1) {
            logMessage(FATAL, "Server waitpid() faild: %s", strerror(errno));
            exit(WAIT_ERR);
        }
        (void)ret;
    }
}

这个版本不在忽略子进程的退出信号. 而是利用了进程的一个特性.

如果子进程还没有退出, 但是其父进程退出了, 那么此子进程就成了孤儿进程, 会被操作系统领养. 退出时会自动被操作系统回收

即, 主父进程创建子进程之后, 子进程又创建了孙子进程. 然后子进程退出, 让孙子进程与客户端通信. 主父进程直接回收退出的子进程. 也不会发生一直阻塞等待的情况.

多线程服务器

多进程版本实现, 消耗的资源很大, 所以还有多线程的版本.

#include "util.hpp"

class tcpServer; 	// threadData结构体要用, 所以先声明

struct threadData {
    threadData(uint16_t clientPort, std::string clientIP, int sock, tcpServer* ts)
        : _clientPort(clientPort)
        , _clientIP(clientIP)
        , _sock(sock)
        , _this(ts) {}

    uint16_t _clientPort;
    std::string _clientIP;
    int _sock;
    tcpServer* _this;
};

class tcpServer {
public:
    tcpServer(uint16_t port, const std::string& ip = "")
        : _port(port)
        , _ip(ip)
        , _listenSock(-1) {}

    static void* threadRoutine(void* args) {
        // 线程分离
        pthread_detach(pthread_self());
        
        threadData* tD = static_cast<threadData*>(args);
        tD->_this->low2upService(tD->_sock, tD->_clientIP, tD->_clientPort);
        
        // 线程执行任务结束后, 需要delete掉 tD
        // 不能在主线程 delete 因为, 线程还没有使用
        delete tD;

        return nullptr;
    }

    void init() {
        // 先创建套接字文件描述符
        // 不过, 与UDP不同的是 TCP是面向字节流的, 所以套接字数据类型 要使用 流式套接字
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);

        if (_listenSock < 0) {
            // 套接字文件描述符创建失败
            logMessage(FATAL, "socket() faild:: %s : %d", strerror(errno), _listenSock);
            exit(SOCKET_ERR); // 创建套接字失败 以 SOCKET_ERR 退出
        }
        logMessage(DEBUG, "socket create success: %d", _listenSock);

        // 套接字创建成功, 就需要将向 sockaddr_in 里填充网络信息
        // 并将进程网络信息绑定到主机上
        struct sockaddr_in local;
        std::memset(&local, 0, sizeof(local));

        // 填充网络信息
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        _ip.empty() ? (local.sin_addr.s_addr = htonl(INADDR_ANY)) : (inet_aton(_ip.c_str(), &local.sin_addr));

        // 绑定网络信息到主机
        if (bind(_listenSock, (const struct sockaddr*)&local, sizeof(local)) == -1) {
            // 绑定失败
            logMessage(FATAL, "bind() faild:: %s : %d", strerror(errno), _listenSock);
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "socket bind success : %d", _listenSock);
        // 绑定了网络信息之后, 不同于 UDP, TCP是面向连接的.
        // 所以 在TCP服务器绑定了进程网络信息到内核中之后, 其他主机就有可能向服务器发送连接请求了
        // 然后, 所以 在绑定了网络信息之后, 要做的事就是 监听套接字
        // 监听是否有其他主机发来连接请求, 需要用到接口 listen()
        if (listen(_listenSock, 5) == -1) {
            logMessage(FATAL, "listen() faild:: %s : %d", strerror(errno), _listenSock);
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen success : %d", _listenSock);
        // 开始监听之后, 别的主机就可以发送连接请求了.
    }

    // 服务器初始化完成之后, 就可以启动了
    void loop() {
        //signal(SIGCHLD, SIG_IGN); // 忽略子进程推出信号, 子进程退出时就会自动回收

        while (true) {
            struct sockaddr_in peer;          // 输出型参数 接受所连接主机客户端网络信息
            socklen_t peerLen = sizeof(peer); // 输入输出型参数

            // 使用 accept() 接口, 接受来自其他网络客户端的连接
            // 成功会返回一个文件描述符, 失败则返回-1
            // 此函数是阻塞式的, 也就是说 在没有连接发送过来之前 进程会处于阻塞状态
            int serviceSock = accept(_listenSock, (struct sockaddr*)&peer, &peerLen);
            if (serviceSock == -1) {
                logMessage(WARINING, "accept() faild:: %s : %d", strerror(errno), serviceSock);
                continue;
            }
            // 走到这里, 就表示连接成功了
            // 连接成功之后, 就可以获取到连接客户端的网络信息了:
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIP = inet_ntoa(peer.sin_addr);
            logMessage(DEBUG, "accept success: [%s: %d] | %d ", peerIP.c_str(), peerPort, serviceSock);

            // 连接到客户端之后, 就可以执行功能了
            // 执行转换功能 小写转大写
            // 多线程版本
            // 让多线程执行low2upService(), 是需要传参的, 所以需要定义一个结构体, 存储可能使用到的参数
            threadData* tD = new threadData(peerPort, peerIP, serviceSock, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, threadRoutine, (void*)tD);
        }
    }

    void low2upService(int sock, const std::string& clientIP, const uint16_t& clientPort) {
        assert(sock > 0);
        assert(!clientIP.empty());

        // 一个用于存储来自客户端信息的数组
        char inbuffer[BUFFER_SIZE];
        while (true) {
            // TCP获取来自客户端的信息的操作就是 read
            // 从 服务器与客户端连接 的文件描述符中 读取来自客户端的信息
            // 可看作 通过文件描述符 从文件读取内容
            ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
            if (s > 0) {
                // 大于零 就是读取到数据了
                inbuffer[s] = '\0';
                // 我们实现一个操作, 如果 客户端传输过来的信息是 quit 这个单词, 就表示客户端请求退出
                // 就可以退出 服务循环了
                if (strcasecmp(inbuffer, "quit") == 0) { // strcasecmp 忽略大小写比较
                    logMessage(DEBUG, "Client requests to quit: [%s: %d]", clientIP.c_str(), clientPort);
                    break;
                }
                // 走到这里 就可以进行小写转大写了
                logMessage(DEBUG, "low2up before: [%s: %d] >> %s", clientIP.c_str(), clientPort, inbuffer);
                for (int i = 0; i < s; i++) {
                    if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                        inbuffer[i] = toupper(inbuffer[i]);
                }
                logMessage(DEBUG, "low2up after: [%s: %d] >> %s", clientIP.c_str(), clientPort, inbuffer);

                // 上面做的都是对获取到的信息 进行转换
                // 最后需要做的就是 将转换后的信息 再重新回应给客户端
                // 而 回应给客户端 则是用 write, 可看做 通过文件描述符像文件写入内容
                write(sock, inbuffer, strlen(inbuffer));
            }
            else if (s == 0) {
                // s == 0, 表示什么?
                // 在管道通信中 read() 是阻塞式读取的. 此时 返回值为0, 表示管道的写入端关闭
                // 而 TCP类似, TCP中 read() 通常也是阻塞时读取的, 此时返回0, 表示客户端关闭
                // 所以此时, 该退出了
                logMessage(DEBUG, "Client has quited: [%s: %d]", clientIP.c_str(), clientPort);
                break;
            }
            else {
                // 到这里 本次 read() 出错
                logMessage(DEBUG, "Client [%s: %d] read:: %s", clientIP.c_str(), clientPort, strerror(errno));
                break;
            }
        }
        // 走到这里 循环已经退出了, 表示 client 也已经退出了
        // 所以 此时需要关闭文件描述符, 因为一个主机上的文件描述符数量是一定的, 达到上限之后 就无法再创建
        // 已经无用但没有被归还的文件描述符, 文件描述符泄漏
        close(sock);
        logMessage(DEBUG, "Service close %d sockFd", sock);
    }

private:
    uint16_t _port; // 端口号
    std::string _ip;
    int _listenSock; // 服务器套接字文件描述符
};

其实, 多线程版本的实现具体可以分为三个部分:

  1. 为让线程可正常执行tcpServer类内函数, 所以要定义一个结构体, 存储需要用到的数据

    结构体对象作为回调函数的参数使用

    |wide

    结构体的成员变量除了客户端网络信息之外, 还有tcpServer对象指针.

    因为回调函数需要用static修饰, 所以在回调函数内是无法直接访问类内成员的. 所以需要使用tcpServer类对象指针.

  2. 回调函数的定义, 主要作用就是执行服务

    |wide

    回调函数首先要执行线程分离. 因为主线程不能阻塞式join线程, 所以让线程自灭.

    然后就是通过结构体对象内的tcpServer对象指针, 调用类内服务函数

    执行完服务之后, 需要deletethreadData指针, 因为它是new出来的. 并且无法在主线程内delete, 因为主线程无法判断, 其他线程是否使用完毕这块空间.

  3. loop()内的线程创建

    |wide

    这里就是常规的创建线程, 让线程执行回调函数

    不过, 需要将所需的数据定义成一个结构体对象, 传入回调参数中.

至此, 多线程的版本就结束了:

可以很明显的看出来, 多线程的版本多客户端连接 会让服务器的文件描述符增加. 因为 多线程共享文件描述符表

线程池服务器

这里的例子使用线程池不太合理.

因为, 这里的任务是死循环的. 所以, 线程池会被占满. 只是试验一下.

博主在之前的文章中实现过简单的线程池, 阅读下面内容之前, 建议阅读这篇文章:

[Linux] 最基础简单的线程池及其单例模式的实现

上面的多线程服务器功能实现的是, 每连接一个客户端创建一个线程. 这样效率太低了.

线程池可以实现, 预先创建多个线程, 然后等到有客户端连接时 将此客户端需要的服务 分配给空闲线程, 从而提升服务客户端的效率

首先是线程池的实现:

threadPool.hpp:

#pragma once

#include <cstddef>
#include <iostream>
#include <ostream>
#include <queue>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include "lock.hpp" 	// RAII思想实现的锁

#define THREADNUM 5

template <class T>
class threadPool {
public:
    static threadPool<T>* getInstance() {
        // RAII锁
        static Mutex mutex;
        if (_instance == nullptr) {
            LockGuard lockG(&mutex);
            if (_instance == nullptr) {
                _instance = new threadPool<T>();
            }
        }

        return _instance;
    }
    // 线程回调函数
    // static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*
    // 但是由于 需要访问类内成员, 所以 传参需要传入this指针
    static void* threadRoutine(void* args) {
        // 线程执行回调函数
        // 先分离, 自动回收
        pthread_detach(pthread_self());

        // 获取this指针
        threadPool<T>* tP = static_cast<threadPool<T>*>(args);
        while (true) {
            // 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁
            tP->lockQueue();
            while (!tP->haveTask()) {
                // 任务队列中没有任务, 就让线程通过条件变量等待
                tP->waitForTask();
            }
            // 走到这里 说明条件队列中有任务
            // 线程已经可以获取到任务
            T task = tP->popTask();
            // 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.
            // 尽量避免拿着锁 执行任务
            tP->unlockQueue();

            // 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();
            // 或者 重载operator() 实现仿函数task()执行任务
            task.run();
        }
    }

    // 开启线程池
    void start() {
        try {
            // _isStart 为true 则说明线程池已经开启
            if (_isStart)
                throw "Error: thread pool already exists";
        }
        catch (const char* e) {
            std::cout << e << std::endl;
            return;
        }

        for (int i = 0; i < _threadNum; i++) {
            pthread_t temp;
            pthread_create(
                &temp, nullptr, threadRoutine,
                this); // 回调函数的参数传入this指针, 用于类访问内成员
        }
        // 开启线程池之后, 要把 _isStart 属性设置为 true
        _isStart = true;
    }

    // 给任务队列添加任务 并分配任务
    void pushTask(const T& in) {
        // 上锁
        lockQueue();
        _taskQueue.push(in);
        // 任务队列中已经存在任务, 线程就不用再等待了, 就可以唤醒线程
        choiceThreadForHandler();
        // 释放锁
        unlockQueue();
    }

    int getThreadNum() {
        return _threadNum;
    }

    ~threadPool() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    threadPool(const threadPool<T>&) = delete;
    threadPool<T>& operator=(const threadPool<T>&) = delete;

private:
    threadPool(size_t threadNum = THREADNUM)
        : _threadNum(threadNum)
        , _isStart(false) {
        assert(_threadNum > 0);

        pthread_mutex_init(&_mutex, nullptr); // 初始化 锁
        pthread_cond_init(&_cond, nullptr);   // 初始化 条件变量
    }
    // 线程调度 即为从任务队列中给各线程分配任务
    // 所以 任务队列是临界资源需要上锁
    void lockQueue() {
        pthread_mutex_lock(&_mutex);
    }
    void unlockQueue() {
        pthread_mutex_unlock(&_mutex);
    }

    // 条件变量 使用条件, 判断是否任务队列是否存在任务
    bool haveTask() {
        return !_taskQueue.empty();
    }
    // 线程通过条件变量等待任务
    void waitForTask() {
        pthread_cond_wait(&_cond, &_mutex);
    }

    // 从任务队列中获取任务, 并返回
    T popTask() {
        T task = _taskQueue.front();
        _taskQueue.pop();

        return task;
    }

    // 唤醒在条件变量前等待的线程
    // 由于唤醒之后就是线程调度的过程
    // 所以函数名 是线程调度相关
    void choiceThreadForHandler() {
        pthread_cond_signal(&_cond);
    }

private:
    size_t _threadNum;        // 线程池内线程数量
    bool _isStart;            // 判断线程池是否已经开启
    std::queue<T> _taskQueue; // 任务队列
    pthread_mutex_t _mutex; // 锁 给临界资源使用 即任务队列 保证线程调度互斥
    pthread_cond_t _cond; // 条件变量 保证线程调度同步

    static threadPool<T>* _instance;
};

template <class T>
threadPool<T>* threadPool<T>::_instance = nullptr;

lock.hpp:

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex {
public:
    Mutex() {
        pthread_mutex_init(&_lock, nullptr);
    }
    void lock() {
        pthread_mutex_lock(&_lock);
    }
    void unlock() {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex() {
        pthread_mutex_destroy(&_lock);
    }

private:
    pthread_mutex_t _lock;
};

class LockGuard {
public:
    LockGuard(Mutex* mutex)
        : _mutex(mutex) {
        _mutex->lock();
        //std::cout << "加锁成功..." << std::endl;
    }

    ~LockGuard() {
        _mutex->unlock();
        //std::cout << "解锁成功...." << std::endl;
    }

private:
    Mutex* _mutex;
};

我们的线程池实现的是 通过向线程池的任务队列添加任务, 然后将任务队列中的任务分配给线程池中的线程.

下面是任务类的代码:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include "logMessage.hpp"

// 此例中的任务, 是tcp服务器与客户端通信
// 需要知道 客户端的网络信息, 还需要知道线程需要执行的功能函数
// 因为 此任务是在线程池中让线程执行的. 会在线程的回调函数中 通过此任务类调用, 所以此类还需要知道功能函数
class Task {
public:
    // 包装器 将功能函数包装起来 函数类型: void 函数名(int sock, std::string ip, uint16_t port)
    using callback_t = std::function<void(int, std::string, uint16_t)>;

    Task()
        : _sock(-1)
        , _port(-1) {}

    Task(int sock, std::string ip, uint16_t port, callback_t func)
        : _sock(sock)
        , _ip(ip)
        , _port(port)
        , _func(func) {}

    // 仿函数
    void operator()() {
        logMessage(DEBUG, "线程[%p] 处理 %s:%d 请求 ---开始", pthread_self(), _ip.c_str(), _port);

        _func(_sock, _ip, _port);

        logMessage(DEBUG, "线程[%p] 处理 %s:%d 请求 ---结束", pthread_self(), _ip.c_str(), _port);
    }

    void run() {
        (*this)();
    }

private:
    int _sock;        // 与客户端通信的套接字
    std::string _ip;  // 客户端IP
    uint16_t _port;   // 客户端端口号
    callback_t _func; // 功能回调函数
};

这里的任务类, 实际上就是存储有 服务器与客户端通信的套接字、客户端IP、客户端端口号以及服务器的服务函数的一个对象.

我们线程中实现的服务函数是low2upService(), 他的参数就是 与客户端通信用的套接字, 客户端IP和客户端端口号.

所以, task类的成员变量也就是 这三个外加一个服务函数.

此类中, 服务函数 我们用包装器包装起来, 并 将类型名设置为callback_t

最后就是服务器的实现了:

tcpServer.cc:

#include "util.hpp"
#include "threadPool.hpp"
#include "task.hpp"

class tcpServer {
public:
    tcpServer(uint16_t port, const std::string& ip = "")
        : _port(port)
        , _ip(ip)
        , _listenSock(-1) {}

    void init() {
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);

        if (_listenSock < 0) {
            // 套接字文件描述符创建失败
            logMessage(FATAL, "socket() faild:: %s : %d", strerror(errno), _listenSock);
            exit(SOCKET_ERR); // 创建套接字失败 以 SOCKET_ERR 退出
        }
        logMessage(DEBUG, "socket create success: %d", _listenSock);

        // 套接字创建成功, 就需要将向 sockaddr_in 里填充网络信息
        // 并将进程网络信息绑定到主机上
        struct sockaddr_in local;
        std::memset(&local, 0, sizeof(local));

        // 填充网络信息
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        _ip.empty() ? (local.sin_addr.s_addr = htonl(INADDR_ANY)) : (inet_aton(_ip.c_str(), &local.sin_addr));

        // 绑定网络信息到主机
        if (bind(_listenSock, (const struct sockaddr*)&local, sizeof(local)) == -1) {
            // 绑定失败
            logMessage(FATAL, "bind() faild:: %s : %d", strerror(errno), _listenSock);
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "socket bind success : %d", _listenSock);
        if (listen(_listenSock, 5) == -1) {
            logMessage(FATAL, "listen() faild:: %s : %d", strerror(errno), _listenSock);
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen success : %d", _listenSock);
        // 开始监听之后, 别的主机就可以发送连接请求了.

        // 线程池版本
        // 服务器初始化时, 要加载线程池
        _tP = threadPool<Task>::getInstance();
    }

    // 服务器初始化完成之后, 就可以启动了
    void loop() {
        // 线程池版本, 在服务器启动时, 也开启线程池
        _tP->start();
        logMessage(DEBUG, "threadPool start success, thread num: %d", _tP->getThreadNum());
        
        while (true) {
            struct sockaddr_in peer;          // 输出型参数 接受所连接主机客户端网络信息
            socklen_t peerLen = sizeof(peer); // 输入输出型参数

            // 使用 accept() 接口, 接受来自其他网络客户端的连接
            int serviceSock = accept(_listenSock, (struct sockaddr*)&peer, &peerLen);
            if (serviceSock == -1) {
                logMessage(WARINING, "accept() faild:: %s : %d", strerror(errno), serviceSock);
                continue;
            }
            // 走到这里, 就表示连接成功了
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIP = inet_ntoa(peer.sin_addr);
            logMessage(DEBUG, "accept success: [%s: %d] | %d ", peerIP.c_str(), peerPort, serviceSock);

            // 连接到客户端之后, 就可以执行功能了
            // 线程池版本
            // v1
            Task t(serviceSock, peerIP, peerPort, std::bind(&tcpServer::low2upService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
            _tP->pushTask(t);
        }
    }

    void low2upService(int sock, const std::string& clientIP, const uint16_t& clientPort) {
        assert(sock > 0);
        assert(!clientIP.empty());

        char inbuffer[BUFFER_SIZE];
        while (true) {
            ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
            if (s > 0) {
                // 大于零 就是读取到数据了
                inbuffer[s] = '\0';
                if (strcasecmp(inbuffer, "quit") == 0) { // strcasecmp 忽略大小写比较
                    logMessage(DEBUG, "Client requests to quit: [%s: %d]", clientIP.c_str(), clientPort);
                    break;
                }
                // 走到这里 就可以进行小写转大写了
                logMessage(DEBUG, "low2up before: [%s: %d] >> %s", clientIP.c_str(), clientPort, inbuffer);
                for (int i = 0; i < s; i++) {
                    if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                        inbuffer[i] = toupper(inbuffer[i]);
                }
                logMessage(DEBUG, "low2up after: [%s: %d] >> %s", clientIP.c_str(), clientPort, inbuffer);

                write(sock, inbuffer, strlen(inbuffer));
            }
            else if (s == 0) {
                logMessage(DEBUG, "Client has quited: [%s: %d]", clientIP.c_str(), clientPort);
                break;
            }
            else {
                // 到这里 本次 read() 出错
                logMessage(DEBUG, "Client [%s: %d] read:: %s", clientIP.c_str(), clientPort, strerror(errno));
                break;
            }
        }
        // 走到这里 循环已经退出了, 表示 client 也已经退出了
        close(sock);
        logMessage(DEBUG, "Service close %d sockFd", sock);
    }

private:
    uint16_t _port; // 端口号
    std::string _ip;
    int _listenSock; // 服务器套接字文件描述符
    threadPool<Task>* _tP;
};

void Usage(std::string proc) {
    std::cerr << "Usage:: \n\t" << proc << " port ip" << std::endl;
    std::cerr << "example:: \n\t" << proc << " 8080 127.0.0.1" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3 && argc != 2) {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3) {
        ip = argv[2];
    }

    tcpServer svr(port, ip);

    svr.init();
    svr.loop();

    return 0;
}

实现过线程池之后, 服务器的实现就很简单.

而且, 由于使用了线程池 tcpServer类中, 不再涉及线程的相关内容.

改动几个地方:

  1. 类中添加线程池 成员变量:

    |wide

  2. 初始化时 加载线程池:

    |wide

  3. 开启服务器时, 开启线程池.

    并且, 在原本该创建线程服务客户端时, 创建任务 并添加到线程池中:

    |wide

    这里构造任务对象时, 第四个参数使用了std::bind()C++标准库函数.

    此函数在博主 另一篇文章 有过介绍, 本篇文章就不赘述了

    使用std::bind()原因是, 服务函数low2upService()是类内函数, 不通过类对象是无法正常调用的.

这些内容实现之后, 就可以使用线程池服务客户端了:


这就是本篇文章的全部内容啦~

感谢阅读~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/758372.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows 10 - Flask 框架 学习总结 1

目录 一、环境配置安装安装 Virtualenv 虚拟环境Virtualenv 虚拟环境内安装 Flask 框架Tips: 二、Flask 框架的初级学习Flask 的 app 补充说明&#xff1a;运行 Flask 的最小应用(app)(后期) 解决 Warning 报错&#xff0c;开启 WSGI 服务 理解调试模式 app.run理解 Flask 框架…

直流有刷电机

直流有刷电机 直流有刷电机(Brushed DC motor) 具有结构简单、易于控制、成本低等特点&#xff0c;在一些功能简单的应用场合&#xff0c;或者说在能够满足必要的性能、低成本和足够的可靠性的前提下&#xff0c;直流有刷电机往往是一个很好的选择。例如便宜的电子玩具、各种风…

辅助驾驶功能开发-功能规范篇(22)-5-L2级辅助驾驶方案功能规范

1.3.5 LKA 系统功能定义 1.3.5.1 状态机 1.3.5.2 状态迁移表 初始状态转移状态转移条件INITOFF系统自检过程中&#xff0c;为 OFF 状态&#xff0c;自检无故障且车辆上次掉电前&#xff0c;为 OFF 状态INITSTANDBY自检无故障&#xff0c;车辆为首次上电&#xff0c;或者上次…

力扣 1005. K 次取反后最大化的数组和

题目来源&#xff1a;https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/description/ C题解1&#xff1a;最直接的想法就是负的变正的&#xff0c;如果负的元素数量小于k&#xff0c;就挑选绝对值大的负数变正&#xff1b;如果负的元素数量大于k&#xf…

深入浅出对话系统——基于预训练语言模型的对话管理

引言 主要讲解三篇论文&#xff0c;主要思想是把自然语言理解、对话管理和自然语言生成三部分整合到一起。 先导知识 数据集 CamRest676MultiWOZ 都是用的自回归语言模型 causalGPT-2、Transformer Decoder 一个概念&#xff1a;delexicalization 通过相应的占位符替换…

TTS | 文本转语音中的声码器(Vocoder)

在这篇文章中&#xff0c;我想详细说明 语音合成(TTS) 中的 Vocoder 部分。 目录 1.声码器(Vocoder)的作用 2.经典的声码器 2.1.WaveNet 2.2.WaveGlow 2.3.MelGAN 2.4.VocGAN 2.5.HiFi-GAN 参考文献 Reference 1.声码器(Vocoder)的作用 神经语音合成主要分为&…

通讯录(纯C语言实现)

相信大家都有过通讯录&#xff0c;今天我来带大家实现以下最简单的通讯录&#xff0c;通过本篇文章&#xff0c;相信可以让大家对C语言有进一步的认识。 话不多说&#xff0c;我们先放函数的实现 #define _CRT_SECURE_NO_WARNINGS 1 #include "Contact.h"int Chea…

Python 算法基础篇:什么是算法及其重要性

Python 算法基础篇&#xff1a;什么是算法及其重要性 引言 1. 什么是算法&#xff1f;2. 算法的重要性 a ) 提高程序性能 b ) 解决复杂问题 c ) 优化资源利用 3. 算法实践与 Python a ) 线性搜索算法 b ) 快速排序算法 结论 引言 算法是计算机科学中的基础概念之一&#xff0…

程序执行过程发生了什么

程序执行过程发生了什么 预处理&#xff08;Preprocessing&#xff09;&#xff1a; 预处理包括宏替换、条件编译、文件包含、去除注释等工作。 此时产生的是 .i文件&#xff0c;这是一个文本文件。 linux生成预处理文件命令&#xff1a; gcc -E test.c -o test.i上述命令中…

天眼使用指南-威胁文件鉴定器

包含了静态检测&#xff0c; 主要负责对传感器&#xff0c;手东提交url等多种数据来源的一些通道&#xff0c;过来的一些样本进行检测。 检测过程&#xff1a;威胁情报的匹配&#xff0c;沙箱检测。及时发现恶意行为和文件进行告警&#xff0c;传给天眼分析平台统一的分析。提…

技能学习机器人代码解析

技能学习机器人代码解析 promt部分生成取文本摘要再次提炼上述文本输出需要学习的内容&#xff08;学习路线&#xff09;输出学习视频URL封装好每一个promt 主体部分输出 promt部分 生成取文本摘要 再次提炼上述文本 通过上面的promt生成文本摘要后&#xff0c;在我们生成的技…

Redis对象结构 — RedisObject

目录 Redis 键值对数据库的全过程​编辑 RedisObject结构体 Redis的encoding编码方式 type对应的数据对象类型 Redis 键值对数据库的全过程 redisDb 结构&#xff0c;表示 Redis 数据库的结构&#xff0c;结构体里存放了指向了 dict 结构的指针&#xff1b;dict 结构&#…

哈希结构(详解)

目录 哈希表 哈希表原理 散列函数 哈希冲突和处理的办法 哈希集合 哈希集合的实现 哈希映射 哈希映射的基本操作 哈希映射的实现 哈希表 散列表&#xff08;Hash table&#xff0c;也叫哈希表&#xff09;&#xff0c;是根据关键码值(Key)而直接进行访问的数据结构 …

dede去掉列表推荐文档的粗体字效果的修改方法

这样看起来多么的不美观了&#xff0c;现在我们本帖教程就是去掉列表这个粗体字效果。 DedeCMSv5.6具体操纵方法如下&#xff1a; 找到 /include/arc.listview.class.php 打开找到743 - 746 行下列代码&#xff1a; if(ereg(c,$row[flag])) {$row[title] "<b>"…

2023.7.15排序算法合集

排序算法合集 一、概述二、排序算法1.冒泡排序2.插入排序3.选择排序4.快速排序5.归并排序6.计数排序 三、完整源码 一、概述 排序算法是计算机科学中的一类常见算法&#xff0c;用于将一组数据按照特定的顺序进行排列&#xff1b;排序算法的应用非常广泛&#xff0c;涉及到数据…

代码随想录第三十九天||● 62.不同路径 ● 63. 不同路径 II

● 62.不同路径 这道题注意&#xff1a;初始化不仅是可以初始化一个值&#xff0c;也可以初始化整个一行或者一列值 这道题递推公式好想&#xff0c;就是初始化不太好想 机器人从(0 , 0) 位置出发&#xff0c;到(m - 1, n - 1)终点。 按照动规五部曲来分析&#xff1a; 确…

用 pesq 给 torchaudio 读取的音频数据打分

用torchaudio读取的音频文件&#xff0c;在输入pesq之前需要进行格式处理与转换。 import torchaudio from pesq import pesq# 读取音频文件 audio_clean, src torchaudio.load(./audio/NOIZEUS/clean/sp01.wav) audio_0dB, sr0 torchaudio.load(./audio/NOIZEUS/bable/0dB/…

算法:动态规划

目录 &#x1f349;什么是动态规划&#xff1f; 1&#xff09;题目解析&#xff0c;&#xff08;我们以第N个泰波那契数来进行举例&#xff09; 2&#xff09;解读算法原理 &#x1f349;第N个泰波那契数的代码编写 &#x1f349;空间优化 &#x1f349;什么是动态规划&am…

全国青少年信息素养大赛参赛练习题2

海龟画图: 漂亮的螺旋线 import turtle as tt.bgcolor("black") n=6 colors = [red, green, blue, yellow,orange,purple]for x in range(160):t.pencolor(colors[x%6])t.forward(x*5/n+x)t.left(360/n+1)t.width(x*n/200) t.up() t.left(-90) t.forward(50) t.d…

什么是番茄工作法?

基本概念 番茄工作法是由弗朗西斯科西里洛于1992年创立的一种相对于GTD更微观的时间管理方法&#xff0c;旨在帮助使用者尽可能保持专注以及创造力&#xff0c;从而更快地完成工作并减少心理疲劳。 使用番茄工作法&#xff0c;首先要选择一个待完成的任务&#xff0c;然后将番茄…