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