2.基于多线程的TCP服务器实现

发布于:2025-03-26 ⋅ 阅读:(26) ⋅ 点赞:(0)

目录

1. 简单分析之前的代码

2. 多线程服务器设计

2.1 C++11线程的基本使用

2.2 服务器主体逻辑

3. 错误处理的封装

4. 完整的代码实现

客户端代码(client.cpp)

服务器代码(server.cpp)

5. 运行方式


在我们预想中,服务器端应该能够同时与多个客户端建立连接并进行网络通信。然而,在之前的代码中,服务器实现只支持单一连接,因为在处理连接时,主线程会被accept()read()write()等方法阻塞,导致无法响应新的连接请求。为了解决这一问题,本文将介绍如何实现一个多线程的TCP服务器,让我们来一步步分析并构建代码。

1. 简单分析之前的代码

在之前的单线程实现中,伪代码大致如下:

int lfd = socket();
int ret = bind();
ret = listen();

int cfd = accept();

while(1) {
    read();
    write();
}

在此程序中,一旦与客户端建立连接,程序会进入while(1)循环,进行数据的接收和发送。这种设计导致了以下几个问题:

  • accept()会阻塞当前进程,直到有新客户端连接。
  • read()会阻塞当前进程,直到有数据可以读取。
  • write()在写缓冲接满时也可能阻塞。

由于这种设计,主要阻塞在read()accept()中,导致服务器无法处理多个客户端的连接。

2. 多线程服务器设计

在多线程服务器中,我们将主要分为两个角色:监听和通信。主线程负责监听客户端的连接请求,而子线程则负责与不同的客户端进行通信。

2.1 C++11线程的基本使用

C++11提供了强大的线程支持。以下是一个简单的线程使用示例:

void func(int num, std::string str) {
    for (int i = 0; i < 10; ++i) {
        std::cout << "子线程: i = " << i << ", num: " << num << ", str: " << str << std::endl;
    }
}

std::thread t(func, 520, "I love you"); // 创建子线程
// 创建子线程对象 t,执行 func() 函数。线程启动后自动运行,参数 520 和 "I love you" 传递给 func()。  
// std::thread 的构造函数支持变参,无需担心参数个数。通常,任务函数 func() 返回 void,因为子线程不处理返回值。  

以上代码会在一个新线程中执行func(),并传递具体参数。

2.2 服务器主体逻辑

伪代码的主体逻辑如下所示:

void func(int fd) {    
    while(1) {
        read();
        write();
    }
    close(fd);
}

int main() {
    int lfd = socket(); // 创建监听套接字
    int ret = bind(); // 绑定地址和端口
    ret = listen(); // 开始监听

    while(1) {
        int cfd = accept(); // 接受客户端连接
        
        // 创建新线程来处理通信
        std::thread t(func, cfd);
        t.detach(); // 分离线程,使其独立运行
    }

    close(lfd); // 关闭监听套接字
}

在此代码中,每当接受到一个新的客户端连接,就会创建一个新的子线程来负责与该客户端的通信。

3. 错误处理的封装

为了简化错误处理,我们可以将错误判断和处理封装到一个函数中,下面是错误处理函数的实现:

void perror_if(bool condition, const char* errorMessage) {
    if (condition) {
        perror(errorMessage);
        exit(1);
    }
}

// 使用示例
int lfd = socket(AF_INET, SOCK_STREAM, 0);
perror_if(lfd == -1, "socket");

这样的封装可以使代码更加简洁且易于维护。

4. 完整的代码实现

客户端代码(client.cpp)

#include <stdlib.h>      // 提供exit函数
#include <stdio.h>       // 提供printf和perror函数
#include <unistd.h>      // 提供close函数
#include <arpa/inet.h>   // 提供socket、connect等函数
#include <string.h>      // 提供memset和strlen函数

// 错误处理函数
void perror_if(bool condition, const char* errorMessage) {
    if (condition) {
        perror(errorMessage); // 输出错误信息
        exit(1);              // 退出程序
    }
}

int main() {
    // 1. 创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    perror_if(fd == -1, "socket"); // 检查socket创建是否成功

    // 2. 绑定IP地址和端口
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr)); // 清空结构体
    saddr.sin_family = AF_INET; // IPv4
    saddr.sin_port = htons(10000); // 设置端口,使用网络字节序
    inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr); // 将IP地址转换为网络字节序

    // 连接到服务器
    int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    perror_if(ret == -1, "connect"); // 检查连接是否成功

    // 3. 与服务器进行通信
    int n = 0; // 消息计数
    while (1) {
        // 发送数据
        char buf[512] = {0}; // 初始化缓冲区
        sprintf(buf, "hi, I am client...%d\n", n++); // 格式化消息
        write(fd, buf, strlen(buf)); // 发送数据到服务器

        // 接收数据
        memset(buf, 0, sizeof(buf)); // 清空缓冲区
        int len = read(fd, buf, sizeof(buf)); // 从服务器读取数据
        if (len > 0) {
            printf("server say: %s\n", buf); // 打印服务器返回的消息
        } else if (len == 0) {
            printf("server disconnect...\n"); // 服务器断开连接
            break; // 退出循环
        } else {
            perror("read"); // 读取数据出错
            break; // 退出循环
        }
        sleep(1); // 每隔1秒发送一条数据
    }

    close(fd); // 关闭套接字
    return 0; // 程序结束
}

服务器代码(server.cpp)

#include <stdlib.h>      // 提供exit函数
#include <stdio.h>       // 提供printf和perror函数
#include <unistd.h>      // 提供close函数
#include <arpa/inet.h>   // 提供socket、bind、listen、accept等函数
#include <string.h>      // 提供memset函数
#include <thread>        // 提供std::thread类以支持多线程

// 错误处理函数
void perror_if(bool condition, const char* errorMessage) {
    if (condition) {
        perror(errorMessage); // 输出错误信息
        exit(1);              // 退出程序
    }
}

// 子线程函数,负责与客户端的通信
void working(int clientfd) {
    char buf[512]; // 用于存储接收到的数据
    while (1) {
        memset(buf, 0, sizeof(buf)); // 清空缓冲区
        int len = read(clientfd, buf, sizeof(buf)); // 从客户端读取数据
        if (len > 0) {
            printf("client says: %s\n", buf); // 打印客户端发送的消息
            write(clientfd, buf, len); // 将接收到的数据回写给客户端(回显)
        }
        else if (len == 0) {
            printf("client is disconnect..\n"); // 客户端断开连接
            break; // 退出循环
        }
        else {
            // 在多线程环境中,不再使用perror,而使用printf
            printf("read error..\n"); // 读取数据出错
            break; // 退出循环
        }
    }
    close(clientfd); // 关闭与客户端的连接
}

int main() {
    // 1. 创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    perror_if(fd == -1, "socket"); // 检查socket创建是否成功

    // 2. 绑定IP地址和端口
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr)); // 清空结构体
    saddr.sin_family = AF_INET; // IPv4
    saddr.sin_port = htons(10000); // 设置端口,使用网络字节序
    saddr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的接口

    // 绑定监听套接字
    int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    perror_if(ret == -1, "bind"); // 检查绑定是否成功

    // 3. 设置监听
    ret = listen(fd, 64); // 开始监听连接请求
    perror_if(ret == -1, "listen"); // 检查监听是否成功

    while (1) {
        // 4. 等待并建立连接
        struct sockaddr_in cliaddr; // 保存客户端IP地址信息
        socklen_t len = sizeof(cliaddr);

        // 接受连接
        int cfd = accept(fd, (struct sockaddr*)&cliaddr, &len);
        if (cfd == -1) {
            perror("accept"); // 处理错误
            continue; // 继续等待新的连接
        }

        char ip[64] = { 0 }; // 用于保存客户端IP地址
        printf("new client fd:%d ip:%s, port:%d\n", cfd,
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)), // 获取客户端IP地址
            ntohs(cliaddr.sin_port)); // 获取客户端端口

        // 创建新的线程来处理客户端的通信
        std::thread t(working, cfd);
        t.detach(); // 分离线程,使其独立运行
    }

    close(fd); // 关闭监听套接字
    return 0; // 程序结束
}

5. 运行方式

  1. 编译代码: 使用 g++ 编译器将代码编译为可执行文件:

    g++ server.cpp -o server -std=c++11 -pthread
    
  2. 运行服务器: 在终端中运行服务器程序:

    ./server
    
  3. 运行客户端: 需要在不同的终端中运行多个客户端程序:

    ./client
    

    可以打开多个终端来模拟多个客户端。

  4. 观察输出: 在服务器终端,您将看到每个客户端的连接消息以及客户端发送的消息,服务器将响应这些消息。

  5. 结束运行: 要结束服务器和客户端,可以在各自的终端使用 Ctrl+C 来终止程序。