C-S模式之实现一对一聊天

天天开心!!!

文章目录

  • 一、如何实现一对一聊天?
    • [1. 服务器设计](#1. 服务器设计)
    • [2. 客户端设计](#2. 客户端设计)
    • [3. 服务端代码实现](#3. 服务端代码实现)
    • [4. 客户端代码实现](#4. 客户端代码实现)
    • [5. 实现说明](#5. 实现说明)
    • 6.实验结果
  • 二、改进
    • 常见的服务器高并发方案
    • [1. 多线程/多进程模型](#1. 多线程/多进程模型)
    • [2. I/O多路复用](#2. I/O多路复用)
    • [3. 异步I/O(Asynchronous I/O)](#3. 异步I/O(Asynchronous I/O))
    • [4. 事件驱动框架](#4. 事件驱动框架)
    • [5. Reactor模式](#5. Reactor模式)
    • [6. Proactor模式](#6. Proactor模式)
    • [7. 协程(Coroutine)](#7. 协程(Coroutine))
  • 三、使用epoll改进Server服务端代码
    • [1. epoll基本工作流程](#1. epoll基本工作流程)
    • [2. 服务端的实现思路](#2. 服务端的实现思路)
    • [3. 改良后的具体实现代码](#3. 改良后的具体实现代码)
    • 实验结果:
    • [4. 实现说明](#4. 实现说明)
    • [5. 优势](#5. 优势)

一、如何实现一对一聊天?

在C++的Socket编程中,实现一对一聊天的基本思路是构建一个客户端(Client)和一个服服务端(Server),并让每个客户端之间通过服务器进行消息的转发,具体步骤如下:

1. 服务器设计

服务器愮接受多个客户端的连接,并为每对用户建立专属的通信通道,实现流程如下:

  • 服务端启动并监听某个端口
  • 每当有客户端连接时,服务端接受连接并创建一个独立的线程或使用I/O多路复用(如select、epoll)来处理客户端请求。
  • 服务端维护一个客户端的连接表(这里我们使用map来存储),当两个客户端匹配时,将彼此的消息进行转发
  • 实现聊天消息的收发和转发逻辑

2. 客户端设计

客户端需要与服务器保持连接,并能够持续地发送和接收消息。实现流程如下:

  • 客户端启动后,连接到服务端指定的IP和端口
  • 客户端可以发送消息给服务端,服务端将消息转发给目标用户
  • 客户端持续接收从服务器发送来的消息,显示在用户界面或控制台

3. 服务端代码实现

cpp 复制代码
#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. 客户端代码实现

cpp 复制代码
#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. 改良后的具体实现代码

cpp 复制代码
#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使用率
  • 资源高效:无需为每个连接创建独立线程,减少了上下文切换的开销
相关推荐
jingfeng5146 分钟前
C++ STL-string类底层实现
前端·c++·算法
bobz96512 分钟前
Python 项目打包为 Windows exe 最好用的工具是哪个?
后端
郝学胜-神的一滴12 分钟前
基于C++的词法分析器:使用正则表达式的实现
开发语言·c++·程序人生·正则表达式·stl
用户214118326360224 分钟前
超算挑战赛实战!AI 一键生成中医药科普短视频,青少年轻松学药材
后端
还是鼠鼠30 分钟前
tlias智能学习辅助系统--Maven 高级-私服介绍与资源上传下载
java·spring boot·后端·spring·maven
追逐时光者35 分钟前
2025 年程序员必备 TOP 10 高效实用工具
后端
网硕互联的小客服37 分钟前
Apache 如何支持SHTML(SSI)的配置方法
运维·服务器·网络·windows·php
高阳言编程1 小时前
4. 存储体系
架构
基于python的毕设1 小时前
C语言栈的实现
linux·c语言·ubuntu
20181 小时前
Supabase migration 开发实践
后端