• 多人聊天室 (epoll - Linux网络编程)


    零、效果展示

    一个服务器作为中转站,多个客户端之间可以相互通信。至少需要启动两个客户端。

    在这里插入图片描述


    三个客户端互相通信
    在这里插入图片描述


    一、服务器代码

    chatServer.cpp

    函数:socket()、bind()、listen()、accept()、read()、write()

    #include 
    #include 
    #include 
    #include   //epoll的头文件
    #include  //socket的头文件
    #include      //close()的头文件
    #include  //包含结构体 sockaddr_in
    #include           //保存客户端信息
    #include   //提供inet_ntoa函数
    using namespace std;
    
    const int MAX_CONNECT = 5; //全局静态变量,允许的最大连接数
    
    struct Client{
        int sockfd; //socket file descriptor 套接字文件描述符 
        string username;
    };
    
    int main(){
        //创建一个epoll实例
        int epfd = epoll_create1(0); //或老版本 epoll_create(1);
        if(epfd < 0){
            perror("epoll create error");
            return -1;
        }
    
        //创建监听的socket
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd < 0){ //若socket创建失败,则返回-1
            perror("socket error");
            return -1;
        }
    
        //绑定本地ip和端口
        struct sockaddr_in addr;  //结构体声明,头文件是
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
        addr.sin_port  = htons(9999);
    
        int ret = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
        if(ret < 0){
            printf("bind error\n");
            cout << "该端口号已被占用,请检查服务器是否已经启动。" << endl;
            return -1;
        }
        
        cout << "服务器中转站已启动,请加入客户端。" << endl;
    
        //监听客户端
        ret = listen(sockfd,1024);
        if(ret < 0){
            printf("listen error\n");
            return -1;
        }
    
        //将监听的socket加入epoll
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = sockfd;
    
        ret = epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev); //防御性编程,方便出bug时快速定位问题
        if(ret < 0){
            printf("epoll_ctl error\n");
            return -1;
        }
        
        //保存客户端信息
        map<int,Client> clients;
        int clientCount = 0; //添加一个客户端计数器
    
        //循环监听
        while(true){
            struct epoll_event evs[MAX_CONNECT];
            int n = epoll_wait(epfd,evs,MAX_CONNECT,-1);
            if(n < 0){
                printf("epoll_wait error\n");
                break;
            }
    
            for(int i = 0; i < n; i ++){
                int fd = evs[i].data.fd;
                //如果是监听的fd收到消息,则表示有客户端进行连接了
                if(fd == sockfd){
                    struct sockaddr_in client_addr;
                    socklen_t client_addr_len = sizeof(client_addr);
                    int client_sockfd = accept(sockfd, (struct sockaddr*) & client_addr, &client_addr_len);
                    if(client_sockfd < 0){
                        printf("accept error,连接出错\n");
                        continue;
                    }
                    //将客户端的socket加入epoll
                    struct epoll_event ev_client;
                    ev_client.events = EPOLLIN; //检测客户端有没有消息过来
                    ev_client.data.fd = client_sockfd;
                    ret = epoll_ctl(epfd, EPOLL_CTL_ADD,client_sockfd,&ev_client);
                    if(ret < 0){
                        printf("epoll_ctl error\n");
                        break;
                    } //iner_ntoa() 将客户端的IP地址从网络字节顺序转换为点分十进制字符串
                    clientCount++; //有新的客户端加入时,增加计数器
                    printf("客户端%d已连接: IP地址为 %s\n", clientCount, inet_ntoa(client_addr.sin_addr));
                    
                    //保存该客户端信息
                    Client client;
                    client.sockfd = client_sockfd;
                    client.username = "";
                    clients[client_sockfd] = client;
                }else{
                    char buffer[1024];
                    int n = read(fd, buffer, 1024);
                    if(n < 0){
                        break; //处理错误
                    }else if(n == 0){
                        //客户端断开连接
                        close(fd);
                        epoll_ctl(epfd,EPOLL_CTL_DEL, fd ,0);
                        clients.erase(fd);
                    }else{ // n > 0
                        string msg(buffer,n);
    
                        //如果该客户端username为空,说明该消息是这个客户端的用户名
                        if(clients[fd].username == ""){
                            clients[fd].username = msg;
                        }else{
                            string name = clients[fd].username;
    
                            //把消息发给其他所有客户端
                            for(auto &c:clients){
                                if(c.first != fd){
                                    string full_message = '[' + name + ']' + ':' + msg;
                                    write(c.first, full_message.c_str(), full_message.length());
                                    //write(c.first,('[' + name + ']' + ":" + msg).c_str(),msg.size() + name.size() + 4);
                                }
                            }
                        }
                    }
                }
            }
        }
        //关闭epoll实例
        close(epfd);
        close(sockfd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145

    二、客户端代码

    client.cpp (注意g++编译时要加 -pthread)

    函数:socket()、connect()、send()、recv()

    #include 
    #include  
    #include        //memset()的头文件
    #include   //socket(),connect()等函数的头文件
    #include   //sockaddr_in的头文件
    #include    //inet_pton()函数的头文件
    #include       //close()函数的头文件
    #include      //pthread创建线程和管理线程的头文件
    using namespace std;
    
    #define BUF_SIZE 1024
    char szMsg[BUF_SIZE];
    
    //发送消息
    void* SendMsg(void *arg){
        int sock = *((int*)arg);
        while(1){
            //scanf("%s",szMsg);
            fgets(szMsg,BUF_SIZE,stdin); //使用fgets代替scanf
            if(szMsg[strlen(szMsg) - 1] == '\n'){
                szMsg[strlen(szMsg)- 1] = '\0'; //去除换行符
            }
            
            if(!strcmp(szMsg,"QUIT\n") || !strcmp(szMsg,"quit\n")){
                close(sock);
                exit(0);
            }
            send(sock, szMsg, strlen(szMsg), 0);
        }
        return nullptr;
    }
    
    //接收消息
    void* RecvMsg(void * arg){
        int sock = *((int*)arg);
        char msg[BUF_SIZE];
        while(1){
            int len = recv(sock, msg, sizeof(msg)-1, 0);
            if(len == -1){
                cout << "系统挂了" << endl;
                return (void*)-1;
            }
            msg[len] = '\0';
            printf("%s\n",msg);
        }
        return nullptr;
    }
    
    int main()
    {
        //创建socket
        int hSock;
        hSock = socket(AF_INET, SOCK_STREAM, 0);
        if(hSock < 0){
            perror("socket creation failed");
            return -1;
        }
    
        //绑定端口
        sockaddr_in servAdr;
        memset(&servAdr, 0, sizeof(servAdr));
        servAdr.sin_family = AF_INET;
        servAdr.sin_port = htons(9999);
        if(inet_pton(AF_INET, "172.16.51.88", &servAdr.sin_addr) <= 0){
            perror("Invalid address");
            return -1;
        }
        
        //连接到服务器
        if(connect(hSock, (struct sockaddr*)&servAdr, sizeof(servAdr)) < 0){
            perror("连接服务器失败");
            cout << "请检查是否已启动服务器。" << endl;
            return -1;
        }else{
            printf("已连接到服务器,IP地址:%s,端口:%d\n", inet_ntoa(servAdr.sin_addr), ntohs(servAdr.sin_port));
            printf("欢迎来到私人聊天室,请输入你的聊天用户名:");
        }
        
        //创建线程
        pthread_t sendThread,recvThread;
        if(pthread_create(&sendThread, NULL, SendMsg, (void*)&hSock)){
            perror("创建发送消息线程失败");
            return -1;
        }
        if(pthread_create(&recvThread, NULL, RecvMsg, (void*)&hSock)){
            perror("创建接收消息线程失败");
            return -1;
        }
    
        //等待线程结束
        pthread_join(sendThread, NULL);
        pthread_join(recvThread, NULL);
    
        //关闭socket
        close(hSock);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    三、知识点

    1.connect()

    在这里插入图片描述

    在这里插入图片描述


    #include 
    #include 
    
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4

    connect()成功返回0,失败返回-1


    以下是一个简单的 TCP 客户端示例,展示了如何使用 connect() 连接到服务器:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) {
            perror("Socket creation failed");
            return -1;
        }
    
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(12345);  // 服务器监听的端口
        inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);  // 服务器的IP地址
    
        if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
            perror("Connection failed");
            return -1;
        }
    
        printf("Connected to the server\n");
    
        // 之后可以使用sockfd进行数据传输
    
        close(sockfd);  // 关闭套接字
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这个示例中,客户端程序创建了一个套接字,设置服务器的 IP 地址和端口,然后尝试与服务器建立连接。如果 connect() 调用成功,客户端就与服务器建立了连接,并可以通过该套接字进行数据通信。


    2.socket()

    1.参数

    int socket(int address_family, int type, int protocol);
    
    • 1

    (1)address family:
    AF_INET:IPv4 网络协议。用于TCP/IP和UDP/IP网络通信。
    ②AF_INET6:IPv6 网络协议。用于TCP/IP和UDP/IP网络通信,但支持IPv6地址。
    ③AF_UNIX(或AF_LOCAL):本地通道通信。用于在同一台机器上的进程间通信。

    2)type:
    SOCK_STREAM:TCP协议,提供面向连接的稳定数据传输,保证数据能够按顺序、完整地到达。
    ②SOCK_DGRAM:UDP协议,提供无连接的数据传输服务。发送的是独立的消息,不保证顺序或数据完整性。
    ③SOCK_RAW:提供原始网络协议访问。在网络模型中,这种类型的套接字允许直接访问IP层,通常用于网络协议的开发和测试。

    (3)protocol:默认协议填0


    2.返回值:
    ①成功时,socket() 返回一个非负整数,即新创建的套接字文件描述符。
    ②出错时,返回 -1,并设置全局变量 errno 以表示具体的错误类型。


    3.创建一个使用IPv4地址和TCP协议的套接字:

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
    }
    
    • 1
    • 2
    • 3
    • 4

    这里,AF_INET 指定使用IPv4地址,SOCK_STREAM 指定使用面向连接的数据传输方式(TCP),0 表示自动选择使用TCP协议。


    在这里插入图片描述


    3.bind()

    在这里插入图片描述

    在这里插入图片描述


    4.send()

    发送数据:将数据放到发送缓冲区。由内核决定什么时候将数据发送出去。

    在这里插入图片描述

    在这里插入图片描述


    5.recv()

    接收数据:当数据送到Linux内核后,数据不是立即给到应用程序,而是放在接收缓冲区,等应用程序什么时候调用recv()函数,什么时候才由内核给到应用程序。
    在这里插入图片描述


    四、改进方向

    1.做的Linux端,只能在相同的IP上启动几个客户端自己玩。
    后续可以做成Windows的exe,买个云服务器,然后发给朋友,进行通信。


    五、跟练视频

    陈子青多人聊天室-C/C++ 多人聊天室开发-epoll模型的IO多路复用

  • 相关阅读:
    建模杂谈系列179 博弈推演模型探索
    HTML学习笔记Day1-快速入门
    springboot vue uniapp电影购票选座系统源码
    C语言小常识大杂烩
    安全 创新 实践|海泰方圆受邀参加“数字时代的网信创新与价值共创”技术交流研讨会
    java 企业工程管理系统软件源码 自主研发 工程行业适用
    Linux下C/C++链接mysql流程
    【漏洞通告】CVE-2022-39944 Apache Linkis反序列化漏洞
    济南某类国企单位面试复盘
    【Android笔记20】Android中的字符串、颜色、尺寸等资源的介绍及使用
  • 原文地址:https://blog.csdn.net/Edward1027/article/details/136732670