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

这篇具有很好参考价值的文章主要介绍了[Linux] 网络编程 - 初见TCP套接字编程: 实现简单的单进程、多进程、多线程、线程池tcp服务器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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


网络的上一篇文章, 我们介绍了网络变成的一些重要的概念, 以及 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() 初始化函数, 初始化服务器信息

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    至此, 初始化完毕.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    即,

    1. 服务器原本的套接字

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

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

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

    2. accept()创建的套接字

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

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

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

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

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

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

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

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

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

当我们使用浏览器访问服务器(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()中调用此函数:

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

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

客户端实现

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

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

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

#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()

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

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

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

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

具体的实现是:

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

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

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


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

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

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

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

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

这是为什么?

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

当客户端连接到服务器之后, 会调用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()放到子进程里执行就可以了.

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

不过需要注意的问题是:

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

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

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

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

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

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

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

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

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

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

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

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

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

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;
    }
}

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

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

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

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

多线程服务器

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

#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类内函数, 所以要定义一个结构体, 存储需要用到的数据

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

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

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

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

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

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

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

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

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

  3. loop()内的线程创建

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

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

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

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

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

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

线程池服务器

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

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

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

[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. 类中添加线程池 成员变量:

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

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

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

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

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

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

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

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

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

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

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


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

感谢阅读~文章来源地址https://www.toymoban.com/news/detail-568904.html

到了这里,关于[Linux] 网络编程 - 初见TCP套接字编程: 实现简单的单进程、多进程、多线程、线程池tcp服务器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)

    在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。 本文将延续上文内容,重点讨论简单的TCP网络程序模拟实现 。通过本文的学习,读者将能够深入了解TCP协议的实际应用,并掌握如何编写简单的TCP网络程序。让我们一起深入探讨TCP网络程序的

    2024年04月14日
    浏览(86)
  • 【Linux】TCP网络套接字编程+协议定制+序列化和反序列化

    悟已往之不谏,知来者之可追。抓不住的就放手,属于你的都在路上…… 1. 为了让我们的代码更规范化,所以搞出了日志等级分类,常见的日志输出等级有DEBUG NORMAL WARNING ERROR FATAL等,再配合上程序运行的时间,输出的内容等,公司中就是使用日志分类的方式来记录程序的输

    2024年02月08日
    浏览(69)
  • 网络编程套接字应用分享【Linux &C/C++ 】【UDP应用 | TCP应用 | TCP&线程池小项目】

    目录 前提知识 1. 理解源ip,目的ip和Macip 2. 端口号 3. 初识TCP,UDP协议 4. 网络字节序 5. socket 编程 sockaddr类型  一,基于udp协议编程  1. socket——创建套接字 2. bind——将套接字强绑定  3. recvfrom——接受数据 4. sendto——发出信息  遇到的问题 (1. 云服务器中以及无法分配I

    2024年04月08日
    浏览(90)
  • 「网络编程」第二讲:socket套接字(四 - 完结)_ Linux任务管理与守护进程 | TCP协议通讯流程

    「前言」文章是关于网络编程的socket套接字方面的,上一篇是网络编程socket套接字(三),这篇续上篇文章的内容,下面开始讲解!  「归属专栏」网络编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「句子分享」 Time goes on and on, never to an 

    2024年02月10日
    浏览(65)
  • 网络编程【TCP流套接字编程】

    目录 TCP流套接字编程 1.ServerSocket API 2.Socket API 3.TCP中的长短连接 4.回显程序(短连接) 5.服务器和客户端它们的交互过程 6.运行结果及修改代码   ❗❗两个核心: ServerSocket     Socket 1.ServerSocket API ✨ ServerSocket 是创建 TCP服务端Socket的API ServerSocket 构造方法: ServerSocket 方法 :

    2023年04月12日
    浏览(148)
  • 【网络编程】网络编程套接字(三)TCP网络程序

    与前边的UDP网络程序相同,创建套接字的接口都是socket,下边对socket接口进行介绍: 协议家族选择AF_INET,因为我们要进行网络通信。 而第二个参数,为服务类型,传入SOCK_STREAM,我们编写TCP程序,所以要选择流式的服务。 第三个参数默认传入0,由前两个参数就可以推出这是

    2024年02月16日
    浏览(61)
  • 网络编程套接字( TCP )

    目录 1、实现一个TCP网络程序(单进程版)         1.1、服务端serverTcp.cc文件                  服务端创建套接字                  服务端绑定                  服务端监听                  服务端获取连接                  服务

    2024年01月17日
    浏览(270)
  • 【探索Linux】—— 强大的命令行工具 P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)

    在上一篇文章中,我们深入探讨了Linux网络编程的基石——套接字(Socket)的概念以及相关的编程接口。我们了解到,套接字是网络通信过程中端与端之间数据交换的关键抽象概念,它提供了一套丰富的编程接口,使得开发者能够在应用层直接进行网络通信的开发。不仅如此

    2024年03月16日
    浏览(74)
  • TCP/IP网络编程(一) 理解网络编程和套接字

    网络编程和套接字概要 网络编程就是编写程序使两台联网的计算机相互交换数据 为了与远程计算机进行数据传输,需要连接因特网,而编程种的套接字就是用来连接该网络的工具。 构建套接字 1.调用soecket函数创建套接字 2.调用bind函数给套接字分配地址 3.调用listen函数将套

    2024年02月11日
    浏览(174)
  • 网络编程套接字之三【TCP】

    目录 1. ServerSocket API(给服务器端使用的类) 2. Socket API(既给服务器使用,也给客户端使用) 3. 写TCP回显—服务器 4. 使用线程池后的TCP服务器代码(最终) 5. 写回显-客户端 6. TCP回显—客户端代码 7. 运行回显服务器和客户端 TCP流套接字编程  ServerSocket 是创建TCP服务端Socket的

    2024年01月19日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包