在我们预想中,服务器端应该能够同时与多个客户端建立连接并进行网络通信。然而,在之前的代码中,服务器实现只支持单一连接,因为在处理连接时,主线程会被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. 运行方式
-
编译代码: 使用
g++
编译器将代码编译为可执行文件:g++ server.cpp -o server -std=c++11 -pthread
-
运行服务器: 在终端中运行服务器程序:
./server
-
运行客户端: 需要在不同的终端中运行多个客户端程序:
./client
可以打开多个终端来模拟多个客户端。
-
观察输出: 在服务器终端,您将看到每个客户端的连接消息以及客户端发送的消息,服务器将响应这些消息。
-
结束运行: 要结束服务器和客户端,可以在各自的终端使用
Ctrl+C
来终止程序。