Bootstrap

epoll - Linux网络编程:多人聊天室

零、效果展示

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

在这里插入图片描述


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


一、服务器代码

chatServer.cpp

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


1.陈子青版本

#include <cstdio>
#include <iostream>
#include <string>
#include <sys/epoll.h>  //epoll的头文件
#include <sys/socket.h> //socket的头文件
#include <unistd.h>     //close()的头文件
#include <netinet/in.h> //包含结构体 sockaddr_in
#include <map>          //保存客户端信息
#include <arpa/inet.h>  //提供inet_ntoa函数
using namespace std;

const int MAX_CONNECT = 5; //全局静态变量,允许的最大连接数

struct Client{
    int sockfd; //socket file descriptor 套接字文件描述符 
    string username;
};

int main(){
    //创建监听的socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0){ //若socket创建失败,则返回-1
        perror("socket error");
        return -1;
    }

    //填充网络地址
    struct sockaddr_in addr;  //结构体声明,头文件是<netinet/in.h>
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port  = htons(9999);
	
	//绑定本地ip和端口
    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 == -1){
        perror("listen error");
        return -1;
    }

    //创建一个epoll实例
    int epfd = epoll_create1(0); //或老版本 epoll_create(1);
    if(epfd == -1){
        perror("epoll create");
        return -1;
    }
    
    //将监听的socket加入epoll
    struct epoll_event ev;
    ev.events = EPOLLIN; //套接字可读
    ev.data.fd = sockfd;

    ret = epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev); //epoll_ctl():监控
    if(ret == -1){
        perror("epoll_ctl"); //防御性编程,方便出bug时快速定位问题
        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){
            perror("epoll_wait");
            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); //>0:读取 ==0:读取结束 <0:错误  
                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;
}

2.Edward版本:重构for循环,分离出两个函数

陈子青原版最后的for循环过于冗长,有多个if、else。
我将其分离为两个函数,提高了代码的可读性(清晰度)和复用性。

#include <cstdio>
#include <iostream>
#include <string>
#include <sys/epoll.h>  //epoll的头文件
#include <sys/socket.h> //socket的头文件
#include <unistd.h>     //close()的头文件
#include <netinet/in.h> //包含结构体 sockaddr_in
#include <map>          //保存客户端信息
#include <arpa/inet.h>  //提供inet_ntoa函数
using namespace std;

const int MAX_CONNECT = 5; //全局静态变量,允许的最大连接数

struct Client{
    int sockfd; //socket file descriptor 套接字文件描述符
    string username;
};

//1.处理新的客户端连接请求
void handle_new_connection(int epoll_fd, int server_fd, map<int,Client> &clients, int &clientCount){
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_sockfd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if(client_sockfd < 0) {
        perror("accept error");
        return;
    }
    
    //将客户端的socket加入epoll
    struct epoll_event ev_client;
    ev_client.events = EPOLLIN;  //检测客户端有没有消息过来
    ev_client.data.fd = client_sockfd;
    if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_sockfd, &ev_client) < 0) {
        perror("epoll_ctl error");
        close(client_sockfd);
        return;
    }
    
    clientCount++;  //有新的客户端加入时,增加计数器
    
    //iner_ntoa() 将客户端的IP地址从网络字节顺序转换为点分十进制字符串
    printf("客户端%d已连接: IP地址为 %s\n", clientCount, inet_ntoa(client_addr.sin_addr));
    
    //保存该客户端信息
    Client client;
    client.sockfd = client_sockfd;
    clients[client_sockfd] = client;
}

//2.处理客户端消息
void handle_client_message(int epoll_fd, int client_fd, map<int,Client> &clients){
    char buffer[1024];
    int n = read(client_fd, buffer, sizeof(buffer));
    //1.read()<0:错误
    if(n < 0){ 
        perror("read error");
        return;
    }
    //2.read()==0:读取结束
	else if(n == 0){ 
        printf("客户端断开连接\n");
        close(client_fd);
        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, nullptr);
        clients.erase(client_fd);
        return;
    }
    //3.read()>0:处理消息
    string msg(buffer, n);
    if(clients[client_fd].username == "") {
        clients[client_fd].username = msg; //如果该客户端username为空,说明该消息是这个客户端的用户名
    }else{
        for(auto & c: clients) {
            string name = clients[client_fd].username;
            if(c.first != client_fd){  //不为自己
                write(c.first,('[' + name + ']' + ":" + msg).c_str(),msg.size()+name.size()+3);
            }
        }
    }
}


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;  //结构体声明,头文件是<netinet/in.h>
    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); //epoll_ctl():监控
    if(ret < 0){        //防御性编程,方便出bug时快速定位问题
        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++) {
            if(evs[i].data.fd == sockfd){ //1.处理:有新的客户端连接请求
                handle_new_connection(epfd, sockfd, clients, clientCount);
            }else{                        //2.处理:已连接的客户端发送消息
                handle_client_message(epfd, evs[i].data.fd, clients);
            }
        }
    }
    //关闭epoll实例
    close(epfd);
    close(sockfd);
    return 0;
}

二、客户端代码

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

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

#include <cstdio>
#include <iostream> 
#include <cstring>       //memset()的头文件
#include <sys/socket.h>  //socket(),connect()等函数的头文件
#include <netinet/in.h>  //sockaddr_in的头文件
#include <arpa/inet.h>   //inet_pton()函数的头文件
#include <unistd.h>      //close()函数的头文件
#include <pthread.h>     //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.客户端代码,写死了IP。复制代码下来的时候,记得改成自己Xshell的IP
在这里插入图片描述


2.客户端是多线程,编译时要加 -pthread
在这里插入图片描述


四、知识点

跳转链接:https://blog.csdn.net/Edward1027/article/details/136688966


五、改进方向

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


六、跟练视频

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

;