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

news2025/1/11 7:47:37

文章目录

    • 零、效果展示
    • 一、服务器代码
    • 二、客户端代码
    • 三、知识点
      • 1.bind()
    • 四、改进方向
    • 五、跟练视频

零、效果展示

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

在这里插入图片描述


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


一、服务器代码

chatServer.cpp

#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(){
    //创建一个epoll实例
    int epld = epoll_create(1);
    if(epld < 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(epld,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(epld,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(epld, 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(epld,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(epld);
    close(sockfd);

    return 0;
}

二、客户端代码

client.cpp

注意g++编译时要加 -pthread

#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.bind()

在这里插入图片描述

在这里插入图片描述


四、改进方向

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


五、跟练视频

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1520206.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java代码审计安全篇-反序列化漏洞

前言&#xff1a; 堕落了三个月&#xff0c;现在因为被找实习而困扰&#xff0c;着实自己能力不足&#xff0c;从今天开始 每天沉淀一点点 &#xff0c;准备秋招 加油 注意&#xff1a; 本文章参考qax的网络安全java代码审计和部分师傅审计思路以及webgoat靶场&#xff0c;记录…

续上篇 qiankun 微前端配置

上篇文章地址&#xff1a;微前端框架 qiankun 配置使用【基于 vue/react脚手架创建项目 】-CSDN博客 主应用&#xff1a; src/main.js 配置&#xff1a; import Vue from vue import App from ./App.vue import router from ./router import { registerMicroApps, start } …

paddle ocr识别文字

paddle使用 # pip install paddlepaddle2.5.2 -i https://mirror.baidu.com/pypi/simple # pip install paddleocr2.7.0.3 -i https://mirror.baidu.com/pypi/simplefrom paddleocr import PaddleOCR from PIL import Image import numpy as npimage Image.open(./2.png) ocr…

CentOS7 部署 k8s

准备两台虚拟机192.168.152.129192.168.152.130更改主机名192.168.152.129&#xff1a;hostnamectl set-hostname k8s-masterhostnamectl192.168.152.130&#xff1a;hostnamectl set-hostname k8s-node1hostnamectl master节点配置 1.配置hosts 在两台节点上执行vim /etc/h…

js【详解】ajax (含XMLHttpRequest、 同源策略、跨域、JSONP)

ajax 的核心API – XMLHttpRequest get 请求 // 新建 XMLHttpRequest 对象的实例 const xhr new XMLHttpRequest(); // 发起 get 请求&#xff0c;open 的三个参数为&#xff1a;请求类型&#xff0c;请求地址&#xff0c;是否异步请求&#xff08; true 为异步&#xff0c;f…

个人简历主页搭建系列-03:Hexo+Github Pages 介绍,框架配置

今天的更新内容主要是了解为什么选择这个网站搭建方案&#xff0c;以及一些前置软件的安装。 Why Hexo? 首先我们了解一下几种简单的网站框架搭建方案&#xff0c;看看对于搭建简历网站的需求哪个更合适。 在 BuiltWith&#xff08;网站技术分析工具&#xff09;上我们可以…

mybatis实现动态sql和关联映射以及延迟加载策略

一、动态sql的简述 什么是动态sql:在不同条件下拼接不同的sql Mybatis框架的动态sql技术是一种根据特定条件动态拼接SQl语句的功能&#xff0c;他存在的意义是为了解决拼接SQL语句字符串时的痛点问题。比如我们在用淘宝之类的软件在进行商品属性选择的时候&#xff0c;我们会发…

2024最新小狐狸AI 免授权源码

后台安装步骤&#xff1a; 1、在宝塔新建个站点&#xff0c;php版本使用7.2 、 7.3 或 7.4&#xff0c;把压缩包上传到站点根目录&#xff0c;运行目录设置为/public 2、导入数据库文件&#xff0c;数据库文件是 /db.sql 3、修改数据库连接配置&#xff0c;配置文件是/.env 4、…

Ubuntu20.04 部署 k8s

目前生产部署 Kubernetes 集群主要有两种方式 kubeadm Kubeadm是一个k8s部署工具&#xff0c;提供 kubeadm init 和 kubeadm join&#xff0c;用于快速部署 Kubernetes 集群。 kubeadm 是由 k8s 官方所提供的专门部署集群的管理工具。在每一个节点主机上都要手动安装并运行 …

OpenOFDM接收端信号处理流程

Overview — OpenOFDM 1.0 documentation 本篇文章为学习OpenOFDM之后的产出PPT&#xff0c;仅供学习参考。 ​​​​​​​

每日五道java面试题之springMVC篇(三)

目录&#xff1a; 第一题. Controller注解的作用第二题. RequestMapping注解的作用第三题. ResponseBody注解的作用第四题. PathVariable和RequestParam的区别第五题. Spring MVC与Struts2区别 第一题. Controller注解的作用 在Spring MVC 中&#xff0c;控制器Controller 负责…

Upload-labs靶场

文件漏洞上传进行复现 环境搭建--->搭建好环境如下&#xff1a; 打开第一关&#xff0c;尝试文件上传漏洞 根据界面提示&#xff0c;选择一个文件&#xff08;.php文件&#xff09;进行上传&#xff0c;发现无法上传 根据提示是指使用js对不合法文件进行了检查&#xff0c;…

蓝桥杯单片机快速开发笔记——DS18B20温度传感器

一、原理分析 考试时可能会需要自己编写底层驱动的.h文件&#xff0c;编写.h的关键是会查原理图把DQ定义好。 sbit DQ P1^4; 二、应用 温度监控&#xff1a;DS18B20广泛应用于温度监控系统中&#xff0c;如室内温度监测、工业生产过程中的温度控制等。 环境监测&#xf…

怎样提升小程序日活?签到抽奖可行吗?

一、 日活运营策略 小程序应该是即用即走的&#xff0c;每个小程序都在用户中有自己的独特定位&#xff0c;可能是生活日常必备&#xff08;美食、团购、商城&#xff09;&#xff0c;也可能是工作办公必备&#xff08;文档、打卡、工具&#xff09;。 如果你想要让自己的小程…

白话transformer(三):Q K V矩阵代码演示

在前面文章讲解了QKV矩阵的原理&#xff0c;属于比较主观的解释&#xff0c;下面用简单的代码再过一遍加深下印象。 B站视频 白话transformer&#xff08;三&#xff09; 1、生成数据 我们呢就使用一个句子来做一个测试&#xff0c; text1 "我喜欢的水果是橙子和苹果&…

WPF布局、控件与样式

视频来源&#xff1a;https://www.bilibili.com/video/BV1HC4y1b76v/ 布局 常用布局属性 HorizontalAlignment&#xff1a;用于设置元素的水平位置VerticalAlignment&#xff1a;用于设置元素的垂直位置Margin&#xff1a;指定元素与容器的边距Height&#xff1a;指定元素的…

AMRT 3D 数字孪生引擎(轻量化图形引擎、GIS/BIM/3D融合引擎):智慧城市、智慧工厂、智慧建筑、智慧校园。。。

AMRT3D 一、概述 1、提供强大完整的工具链 AMRT3D包含开发引擎、资源管理、场景编辑、UI搭建、项目预览和发布等项目开发所需的全套功能&#xff0c;并整合了动画路径、精准测量、动态天气、视角切换和动画特效等工具。 2、轻量化技术应用与个性化定制 AMRT3D适用于快速开…

《雷德斯东家》 第一话——火把花的回忆

引子 温馨提示:本系列小说为福瑞向! ------------------------------------------------------------------------------------------------------------------------- 作者 RedstoneCuberoot 地图 RedstoneCuberoot、梓元sama 审核 Brenda_fyx、RedstoneCuberoot Minecraft大…

Linux_socket编程

套接字通信 socket 接口 守护进程 一.套接字通信 端口号&#xff1a; 端口号是一个2字节16位的整数;端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理; 一台主机可以根据ip地址定位另一台主机&#xff0c;而两台主机之间的通信本质是进程在通信。…

stm32-编码器测速

一、编码器简介 编码电机 旋转编码器 A,B相分别接通道一和二的引脚&#xff0c;VCC&#xff0c;GND接单片机VCC&#xff0c;GND 二、正交编码器工作原理 以前的代码是通过触发外部中断&#xff0c;然后在中断函数里手动进行计次。使用编码器接口的好处就是节约软件资源。对于频…