网络io与io多路复用(2) 多线程服务器实现多客户端连接
前言
上一篇文章里,实现了单客户端对单服务端的连接。但是如果增加Client的数量呢?会发现客户端发送信息后,终端被卡死在“RECE”,并且无法及时接收每个客户端发送的信息。本篇将继续解决这个问题。
多客户端(3个)对单服务端的消息接发
解决方法
为每个客户端连接创建一个线程,处理客户端请求。
代码实现
#include
#include
#include
#include
#include
#include
#include
//客户端线程,处理客户端请求
void *client__thread(void *arg) {
int clientfd = *(int *)arg;
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
if (count < 0) {
perror("recv failed");
} else {
printf("RECV:%s
", buffer);
count = send(clientfd, buffer, count, 0);
if (count < 0) {
perror("send failed");
} else {
printf("SEND:%d
", count);
}
}
close(clientfd);
return NULL;
}
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket failed");
return -1;
}
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
printf("bind failed:%s
", strerror(errno));
close(sockfd);
return -1;
}
if (listen(sockfd, 10) < 0) {
perror("listen failed");
close(sockfd);
return -1;
}
printf("listen finished
");
//接受客户端连接,并创建线程处理请求
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
while (1) {
printf("accept
");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd < 0) {
perror("accept failed");
continue;
}
printf("accept finished
");
pthread_t thid;
if (pthread_create(&thid, NULL, client__thread, &clientfd) != 0) {
perror("pthread_create failed");
close(clientfd);
} else {
pthread_detach(thid);
}
}
close(sockfd);
printf("exit
");
return 0;
}
代码实现TCP连接过程的分析
TCP连接的定义
TCP连接是指通信双方(如客户端和服务器)通过TCP协议建立的一个逻辑上的通信通道。
TCP连接建立过程
1. 客户端通过 connect() 向服务器发起连接请求。
2. 服务器通过 listen() 将套接字设置为监听状态。
3. 服务器通过 accept() 接受客户端连接,并创建一个新的文件描述符 clientfd。
4. 通过 clientfd,服务器和客户端可以进行双向数据通信。
在程序中的体现
1. 服务器通过 sockfd 监听客户端连接。
2. 当客户端连接时,accept() 返回 clientfd,表示一个新的TCP连接。
3. 通过 clientfd,服务器可以接收客户端发送的数据,并将数据返回给客户端。
FD、accept()
和 TCP连接的关系
FD 是 TCP连接的标识
每个TCP连接在服务器端都对应一个唯一的文件描述符(clientfd)。
sockfd 用于监听连接,clientfd 用于处理连接。
accept() 是 TCP连接的桥梁
accept() 从监听套接字 sockfd 中接受客户端连接,并为该连接创建一个新的文件描述符 clientfd。
通过 clientfd,服务器可以与客户端进行数据通信。
TCP连接是通信的基础
TCP连接是服务器和客户端之间数据传输的通道。
通过 accept() 创建的 clientfd,服务器可以管理多个TCP连接,并为每个连接分配独立的线程或进程。
代码运行结果
1. 编译运行程序
2. 创建3个客户端,并连接与服务端连接
3. 客户端依次发送消息
由此可见,代码成功实现了通过创建多线程服务器成功实现处理多客户端连接,并将接收到的数据原样返回。
代码缺点
使用一请求一线程的方式实现多客户端连接是一种常见的多线程编程模型,但它也存在一些明显的缺点。
1. 线程创建和销毁的开销
问题:每次接收到一个客户端请求时,都需要创建一个新的线程来处理该请求。线程的创建和销毁会消耗大量的系统资源(如CPU和内存)。
影响:在高并发场景下,频繁创建和销毁线程会导致系统性能下降,甚至可能耗尽系统资源。
2. 线程数量限制
问题:每个线程都需要占用一定的内存空间(如线程栈),而系统的线程数量是有限的。
影响:当客户端连接数过多时,线程数量可能达到系统上限,导致无法处理新的连接请求。
3. 上下文切换开销
问题:多个线程之间会频繁发生上下文切换(Context Switching),尤其是在线程数量较多时。
影响:上下文切换会消耗CPU资源,降低系统的整体性能。
4. 线程安全问题
问题:多个线程可能同时访问共享资源(如全局变量、文件、数据库等),如果没有正确的同步机制,会导致数据竞争和不一致。
影响:需要引入锁(如互斥锁、读写锁)来保证线程安全,但锁的使用会增加代码复杂度,并可能引发死锁问题。
5. 资源管理复杂
问题:每个线程都需要独立管理资源(如文件描述符、内存等),如果线程没有正确释放资源,会导致资源泄漏。
影响:资源泄漏会逐渐耗尽系统资源,最终导致系统崩溃。
6. 扩展性差
问题:一请求一线程的模型难以扩展到大规模并发场景,因为线程数量和系统资源是有限的。
影响:在面对高并发需求时,这种模型无法有效利用系统资源,性能瓶颈明显。
7. 调试和维护困难
问题:多线程程序的调试和维护比单线程程序复杂得多,因为线程之间的交互和竞争条件难以重现和分析。
影响:增加了开发和维护的成本。
替代方案
为了克服一请求一线程的缺点,可以采用以下替代方案:
线程池:预先创建一组线程,将任务分配给空闲线程,避免频繁创建和销毁线程。
事件驱动模型:使用I/O多路复用(如select、poll、epoll)或异步I/O(如libuv、Boost.Asio)来处理多个客户端连接,减少线程数量。
协程:使用轻量级的协程(如Go语言的goroutine、Python的asyncio)来实现高并发,减少线程切换的开销。
总结
本文通过实现一个多线程服务器,解决了单客户端连接时终端卡死的问题,成功支持多客户端连接和消息收发。通过为每个客户端连接创建独立线程,服务器能够同时处理多个客户端请求,并将接收到的数据原样返回。然而,这种“一请求一线程”的模型存在线程创建和销毁开销大、线程数量限制、上下文切换频繁、线程安全问题、资源管理复杂、扩展性差以及调试维护困难等缺点。为应对高并发场景,建议采用线程池、事件驱动模型或协程等更高效的替代方案。
学习资料参考
https://github.com/0voice