天天开心!!!
文章目录
- 一、如何实现一对一聊天?
- 1. 服务器设计
- 2. 客户端设计
- 3. 服务端代码实现
- 4. 客户端代码实现
- 5. 实现说明
- 6.实验结果
- 二、改进
- 常见的服务器高并发方案
- 1. 多线程/多进程模型
- 2. I/O多路复用
- 3. 异步I/O(Asynchronous I/O)
- 4. 事件驱动框架
- 5. Reactor模式
- 6. Proactor模式
- 7. 协程(Coroutine)
- 三、使用epoll改进Server服务端代码
- 1. epoll基本工作流程
- 2. 服务端的实现思路
- 3. 改良后的具体实现代码
- 实验结果:
- 4. 实现说明
- 5. 优势
一、如何实现一对一聊天?
在C++的Socket编程中,实现一对一聊天的基本思路是构建一个客户端(Client)和一个服服务端(Server),并让每个客户端之间通过服务器进行消息的转发,具体步骤如下:
1. 服务器设计
服务器愮接受多个客户端的连接,并为每对用户建立专属的通信通道,实现流程如下:
- 服务端启动并监听某个端口
- 每当有客户端连接时,服务端接受连接并创建一个独立的线程或使用I/O多路复用(如select、epoll)来处理客户端请求。
- 服务端维护一个客户端的连接表(这里我们使用map来存储),当两个客户端匹配时,将彼此的消息进行转发
- 实现聊天消息的收发和转发逻辑
2. 客户端设计
客户端需要与服务器保持连接,并能够持续地发送和接收消息。实现流程如下:
- 客户端启动后,连接到服务端指定的IP和端口
- 客户端可以发送消息给服务端,服务端将消息转发给目标用户
- 客户端持续接收从服务器发送来的消息,显示在用户界面或控制台
3. 服务端代码实现
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <thread>
#include <map>
#define PORT 8080
#define BUFFER_SIZE 1024
std::map<int,int> client_map; //存储客户端的配对关系
//处理客户端通信
void handle_client(int client_socket){
char buffer[BUFFER_SIZE];
int target_socket=client_map[client_socket]; //获取配对的客户端socket
while(true){
memset(buffer,0,sizeof(buffer));//清空缓冲区
ssize_t bytes_received=recv(client_socket,buffer,BUFFER_SIZE,0);//接收数据的长度
if(bytes_received<=0){//如果接收失败,则关闭连接
std::cerr<<"Error receiving data from client"<<std::endl;//输出错误信息
close(client_socket);
return;
}
std::cout<<"收到消息:"<<buffer<<std::endl;
//将消息转发给目标客户端
if(client_map.find(target_socket)!=client_map.end()){
send(target_socket,buffer, strlen(buffer),0);
}else{
std::cerr<<"Error sending data to target client"<<std::endl;
}
}
}
int main()
{
int server_socket;
struct sockaddr_in server_addr;
//创建服务器套接字
server_socket=socket(AF_INET,SOCK_STREAM,0);
if(server_socket==0){
std::cerr<<"Error creating server socket"<<std::endl;
return -1;
}
//初始化地址结构
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=INADDR_ANY;
server_addr.sin_port=htons(PORT);
//绑定地址和端口
if(bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))<0){
std::cerr<<"Error binding server socket"<<std::endl;
return -1;
}
//监听客户端连接
if(listen(server_socket,3)<0){
std::cerr<<"Error listening for connections"<<std::endl;
return -1;
}
std::cout<<"等待客户端连接..."<<std::endl;
while(true){
struct sockaddr_in client_addr;
socklen_t addr_len=sizeof(client_addr);
int new_socket=accept(server_socket,(struct sockaddr*)&client_addr,&addr_len);
if(new_socket<0){
std::cerr<<"Error accepting connection"<<std::endl;
continue;
}
std::cout<<"新客户端连接"<<inet_ntoa(client_addr.sin_addr)<<std::endl;
//这里为了简化,我们直接假设是两客户端配对,client_map存储配对关系
if(client_map.empty()){
client_map[new_socket]=-1;//第一个客户端,暂时没有配对
}else{
for(auto &pair:client_map){
if(pair.second==-1){
client_map[new_socket]=pair.first;//第二个客户端与第一个客户端配对
client_map[pair.first]=new_socket;//第一个客户端与第二个客户端配对
std::cout<<"客户端配对成功"<<std::endl;
break;
}
}
}
//创建线程处理新客户端
std::thread client_thread(handle_client,new_socket);
client_thread.detach();//线程分离,主线程不阻塞
}
close(server_socket);
return 0;
}
4. 客户端代码实现
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <thread>
#define PORT 8080
#define BUFFER_SIZE 1024
//处理服务器消息
void receive_message(int socket){
char buffer[BUFFER_SIZE];
while(true){
memset(buffer, 0, sizeof(buffer));//清空缓冲区
ssize_t bytes_received = recv(socket, buffer, BUFFER_SIZE, 0);
if(bytes_received <= 0){
std::cout << "服务器断开连接..." << std::endl;
close(socket);
return;
}
std::cout<<"收到消息:"<<buffer<<std::endl;
}
}
int main()
{
int client_socket;//客户端套接字
struct sockaddr_in server_addr;
//创建客户端套接字
client_socket=socket(AF_INET, SOCK_STREAM, 0);
if(client_socket<0){
std::cout<<"创建套接字失败"<<std::endl;
return -1;
}
//初始化服务器地址
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(PORT);
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器IP
//连接到服务器
if(connect(client_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))<0){
std::cerr<<"连接服务器失败"<<std::endl;
return -1;
}
std::cout<<"连接到服务器成功"<<std::endl;
//创建线程接受服务器消息
std::thread receive_thread(receive_message,client_socket);//创建线程(函数,函数的形参)
receive_thread.detach();//线程分离,主线程结束后,子线程也结束
//发送消息给服务器
char message[BUFFER_SIZE];
while(true){
std::cout<<"请输入消息:";
std::cin.getline(message,BUFFER_SIZE);
send(client_socket,message,strlen(message),0);
}
close(client_socket);
return 0;
}
5. 实现说明
- 服务端:服务器监听客户端连接并维护一个客户端配对表client_map;当两个客户端配对后,消息就可以在它们之间转发。这里使用了多线程来处理每个客户端的通信
- 客户端:客户端连接到服务器并启动一个线程用于接收来自服务器的消息,用户可以输入消息并发送给服务器,服务器负责转发消息给配对的客户端
6.实验结果
二、改进
我们可以使用epoll或者select替代多线程处理,提高服务器的并发性能。也可以增加心跳机制来检测客户端是否断开连接。如果要完善,还可以增加实现身份验证和聊天室功能,使得用户可以自由选择与谁聊天。
常见的服务器高并发方案
1. 多线程/多进程模型
每个连接由一个独立的线程或进程处理,能够比较简单的实现并发处理
- 优点:代码易于理解,编写较为简单
- 缺点:线程或进程的开销较大,在高并发场景下,大量的线程/进程会带来系统资源消耗和性能瓶颈,特别是在数千甚至数万个连接的时候
2. I/O多路复用
I/O多路复用可通过少量的线程处理大量并发连接,常用的方法包括:
-
select:通过一个文件描述符集合监视多个文件描述符是否有I/O事件
(1)优点:简单、易用、跨平台支持好
(2) 缺点:性能不佳,处理大量连接时,每次调用select都要遍历整个描述符集合,效率很低 -
poll:与select类似,但没有文件描述符限制
(1)优点:避免了select的文件描述符限制
(2) 缺点:与select雷系,性能仍然不高,遍历整个描述符集合 -
epoll(Linux专用):epoll是Linux特有的I/O多路复用机制,性能更好,适合处理大量并发连接
(1)优点:不会遍历所有文件描述符,性能优异,适用于高并发场景
(2) 缺点:仅限于Linux系统
3. 异步I/O(Asynchronous I/O)
异步I/O通过事件驱动机制,程序不需要等待I/O操作的完成,而是注册事件,事件触发时进行处理。常见的异步I/O实现包括:
- Windows:使用IOCP(I/O Completion Port)实现异步I/O处理
- Linux:可以使用libaio或者基于epoll实现的异步I/O
- 优点:真正的异步,无需阻塞等待I/O操作,性能高,适合高并发
- 缺点:编写异步代码比较复杂,调试很困难
4. 事件驱动框架
利用现成的事件驱动框架来处理高并发来凝结,常见的库包括
- libevent:基于事件的异步I/O库,支持epoll、kqueue等多种I/O多路复用机制,适合处理大量并发连接
- libuv:跨平台异步I/O库,Node.js就是基于libuv实现的
- Moduo:C++高性能网络库,基于epoll和线程池,适用于Linux下的高并发场景
- 优点:封装好、使用方便,能够提高并发效率
- 缺点:引入了额外的依赖,性能调优相对不够灵活
5. Reactor模式
Reactor模式是I/O多路复用的一种常见的实现模式,它通过注册I/O事件,将事件分发给事件处理器
- 典型实现:使用epoll或select监听事件,再结合事件处理回调函数进行处理
- 优点:能够较好地处理大量并发连接,灵活性高
- 缺点:编写和理解较为复杂,需要维护事件循环和回调函数
6. Proactor模式
Proactor模式是异步I/O的常见实现,区别于Reactor,Proactor是I/O操作完成后再进行回调
- 典型实现:Windows上的ICP就是Proactor模式实现的
- 优点:异步操作更加彻底。I/O操作由操作系统处理,减少了用户态的干预
- 缺点:实现较为复杂,调试难
7. 协程(Coroutine)
协程是一种轻量级的线程,能够在用户态进行切换,使用协程可以避免线程切换的开销,同时实现高并发。可以结合I/O多路复用技术,如epoll,来实现高效的协程并发
- 典型框架:如Boost.Asio支持协程、libgo协程库等
- 优点:切换开销小、性能高,代码易于理解
- 缺点:调试比较复杂,尤其实在上下文切换时容易出现问题
三、使用epoll改进Server服务端代码
使用epoll实现一对一聊天的服务端,可以大大提高服务器的并发处理能力,相比于传统的多线程或select,epoll更适合处理大量客户端的连接,尤其是在高并发场景下。
1. epoll基本工作流程
- 创建epoll文件描述符:使用epoll_create创建epoll实例
- 注册事件:通过epoll_ctl将套接字添加到epoll实例中,并设置要监听的事件(如EPOLLIN,表示有数据可读)
- 等待事件:使用epoll_wait等待事件发生
- 处理事件:一旦事件发生,处理相应的客户端读写操作
2. 服务端的实现思路
- 启动epoll实例:监听客户端的连接请求
- 当有新的客户端连接时,将其注册到epoll实例中
- 维护客户端配对关系:服务端为每个客户端建立配对表
- 消息的收发和转发:当接收到某个客户端的消息时,通过配对表找到对应的目标客户端,并将消息转发过去
3. 改良后的具体实现代码
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <map>
#include <sys/epoll.h> // epoll头文件
#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_EVENTS 10
std::map<int,int> client_map;//客户端配对表
int main(){
int server_socket,epoll_fd;// 服务器套接字,epoll文件描述符
struct sockaddr_in server_addr; // 服务器地址结构体
//创建服务器套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if(server_socket <0){
std::cerr << "创建服务器套接字失败" << std::endl;
return -1;
}
//设置地址复用
int opt=1;
setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//设置地址复用
//初始化服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
//绑定地址到服务器套接字
if(bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))<0){
std::cerr << "绑定地址到服务器套接字失败" << std::endl;
return -1;
}
//监听端口
if(listen(server_socket,10)<0){
std::cerr << "监听端口失败" << std::endl;
return -1;
}
//创建epoll实例
epoll_fd = epoll_create1(0);
if(epoll_fd ==-1){
std::cerr<<"创建epoll实例失败"<<std::endl;
return -1;
}
//将服务器套接字加入到epoll实例中
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = server_socket;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,server_socket,&event);
std::cout<<"服务器启动成功,等待客户端连接...."<<std::endl;
struct epoll_event events[MAX_EVENTS]; // epoll事件数组, 用于存储就绪事件
while(true){
//无限等待事件
int event_count=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);
for(int i=0;i<event_count;i++){
if(events[i].data.fd==server_socket){
//处理新的客户端连接
struct sockaddr_in client_addr;
socklen_t clinet_len=sizeof(client_addr);
int client_socket=accept(server_socket,(struct sockaddr*)&client_addr,&clinet_len);//接受客户端连接
if(client_socket<0){
std::cerr<<"接受客户端连接失败"<<std::endl;
continue;
}
std::cout<<"新的客户端连接:"<<inet_ntoa(client_addr.sin_addr)<<std::endl;
//将新客户端连接加入到epoll监听中
event.events=EPOLLIN;
event.data.fd=client_socket;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,client_socket,&event);
//客户端配对处理
if(client_map.empty()){
client_map[client_socket]=-1;//第一个客户端,暂时没有配对
}else{
for(auto &pair:client_map){
if(pair.second==-1){
client_map[client_socket]=pair.first;//找到第一个没有配对的客户端,进行配对
client_map[pair.first]=client_socket;//更新第一个没有配对的客户端的配对信息
break;
}
}
}
}else{
//处理客户端的消息转发
int client_socket=events[i].data.fd;
char buffer[BUFFER_SIZE];
memset(buffer,0,sizeof(buffer));//清空缓冲区
ssize_t bytes_received=recv(client_socket,buffer,sizeof(buffer),0);//接收客户端消息
if(bytes_received<=0){
std::cerr<<"客户端断开连接"<<std::endl;
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,client_socket,NULL);//从epoll中删除该客户端
close(client_socket);
continue;
}
std::cout<<"收到的消息:"<<buffer<<std::endl;
//转发消息给配对的客户端
int target_socket=client_map[client_socket];
if(target_socket!=-1){
send(target_socket,buffer, strlen(buffer), 0);
}else{
std::cerr<<"目标客户端未连接"<<std::endl;
}
}
}
}
close(server_socket);
return 0;
}
实验结果:
4. 实现说明
- epoll创建:通过epoll_create创建一个epoll实例,epoll_ctl用于将服务器套接字添加到epoll监听中
- 事件处理:使用epoll_wait等待客户端连接事件和消息事件,一旦有事件发生,处理新连接或消息收发
- 客户端配对:与之前的多线程版本类似,使用client_map维护客户端之间的配对关系
5. 优势
- 高并发支持:epoll适合大量客户端并发连接,比select或多线程处理效率更高
- 事件驱动:基于事件通知的机制,而不是轮询,降低了CPU使用率
- 资源高效:无需为每个连接创建独立线程,减少了上下文切换的开销