欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 旅游 > 2.基于多线程的TCP服务器实现

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

2025/4/22 12:53:54 来源:https://blog.csdn.net/m0_74200772/article/details/146515984  浏览:    关键词:2.基于多线程的TCP服务器实现

在我们预想中,服务器端应该能够同时与多个客户端建立连接并进行网络通信。然而,在之前的代码中,服务器实现只支持单一连接,因为在处理连接时,主线程会被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; // IPv4saddr.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,而使用printfprintf("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; // IPv4saddr.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 来终止程序。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词