【C-实践】网络聊天室(1.0)

news2024/12/28 1:56:25

概述

使用了 tcp + epoll ,实现网络聊天室


1.0 版,用户的显示框和输入框在一起

2.0 版,用户的显示框与输入框分离


功能


主要功能:用户连接服务器,就会自动进入网络聊天室与其他在线用户一起聊天



服务器搭建


  1. 创建用户数组

    1. 根据配置文件中的最大用户数量,创建用户数组,并初始化(用户名字、是否在线、通信套接字)
    2. 创建一个用户记录器,记录当前在线用户数量
  2. 建立一个tcp类型的正在监听的套接字

  3. 使用epoll管理所有套接字,监听所有用户的连接申请、发送消息和退出

    • 有新的用户连接

      • 如果用户数组已满,跳过

      • 如果有位置

        • 把新用户放在这(设为在线,记录名字,记录用户套接字)
        • 用户记录器++
        • 将新用户加入epoll
        • 给新用户发送欢迎信息
        • 通知其他在线用户,新用户的到来
    • 有用户发信息

      • 接收
      • 定位是哪个用户发消息
        • 如果用户断开
          • 合成用户退出信息
          • 将此用户下线
          • 用户记录器 - -
          • 移除epoll监听
          • 通知其他所有在线用户
        • 如果是正常消息,转发给其他所有在线用户


客户端搭建


  1. 连接聊天室服务器
  2. 输入自己的名字
  3. 进入聊天室聊天
    1. 服务器套接字就绪,接收信息并输出在屏幕上
    2. 标准输入就绪,接收信息发送给服务器


启动


启动服务器

1、在bin目录下生成可执行文件

w@Ubuntu20:bin $ gcc ../src/*.c -o server

2、启动服务器

w@Ubuntu20:bin $ ./server ../conf/server.conf

启动客户端

1、在客户端的目录下生成可执行文件

w@Ubuntu20:client $ gcc *.c -o client

2、启动客户端

w@Ubuntu20:client $ ./client client.conf


目录设计

服务器

  • bin:存放二进制文件
  • conf:存放配置文件
  • include:存放头文件
  • src:存放源文件
w@Ubuntu20:src $ tree ..
..
├── bin
│   └── server
├── conf
│   └── server.conf
├── include
│   └── qq.h
└── src
    ├── interact.c
    ├── main_server.c
    └── tcp_init.c

客户端

w@Ubuntu20:client $ tree
.
├── client
├── client.conf
├── interact_qq.c
└── main_client.c


配置文件


服务器配置文件 server.conf

存放服务器ip地址,服务器port端口,聊天室最大用户数量

根据实际情况自行更改

192.168.160.129
2000
10

客户端配置文件 client.conf

存放服务器ip地址,服务器port端口

根据实际情况自行更改

192.168.160.129
2000




代码


服务器代码


qq.h

#ifndef __QQ_H__
#define __QQ_H__

//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg);\
    return -1;} }

//用户信息
typedef struct {
    char _online;//是否在线
    int _usrfd;//用户套接字
    char _usrname[30];//用户名
}Usr_t, *pUsr_t;

//输入:服务器的ip地址,端口号
//输出:绑定了服务器ip和端口的,正在监听的套接字
int tcp_init(char *ip, int port);

//功能:服务器主进程处理来自用户的连接,发信息,退出
//参数:服务器套接字,用户数组,用户最大容量
int interact_usr(int sfd, pUsr_t pUsrArr, int max_capacity);

#endif

main_server.c

#include "../include/qq.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

//检查命令行参数个数
#define ARGS_CHECK(argc, num) { if (argc != num) {\
    fprintf(stderr, "Args error!\n");\
    return -1;  }}

int main(int argc, char *argv[])
{
    ARGS_CHECK(argc, 2);

    //从配置文件中取出服务器ip、port、最多的在线用户数
    FILE *fp = fopen(argv[1], "r");
    char ip[128] = {0};
    int port = 0;
    int max_capacity = 0;
    fscanf(fp, "%s%d%d", ip, &port, &max_capacity);
    fclose(fp);

    //创建用户数组,存储用户信息
    pUsr_t pUsrArr = (pUsr_t)calloc(max_capacity, sizeof(Usr_t));    

    //建立一个正在监听的tcp类型的服务器套接字
    int sfd = tcp_init(ip, port);
    printf("qq_server boot...\n");
    printf("max_capacity : %d\n", max_capacity);

    //处理用户端请求
    interact_usr(sfd, pUsrArr, max_capacity);

    //关闭服务器套接字
    close(sfd);
    printf("qq_server was closed!\n");
    //释放用户数组
    free(pUsrArr);
    pUsrArr = NULL;
    return 0;
}

tcp_init.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <arpa/inet.h>

#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror("msg");  return -1;} }

//输入:服务器的ip地址,端口号
//输出:绑定了服务器ip和端口的,正在监听的套接字
int tcp_init(char *ip, int port)
{
    //生成一个tcp类型的套接字
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(sfd, -1, "ser_socket");

    //将端口号设置为可重用, 不用再等待重启时的TIME_WAIT时间
    int reuse = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    //给套接字绑定服务端ip和port
    struct sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(struct sockaddr_in));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(ip);
    serverAddr.sin_port = htons(port);

    int ret = bind(sfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    ERROR_CHECK(ret, -1, "ser_bind");

    //将套接字设为监听模式,并指定最大监听数(全连接队列的大小)
    ret = listen(sfd, 10); 
    ERROR_CHECK(ret, -1, "ser_listen");

    /* printf("[ip:%s, port:%d] is listening...\n", ip, port); */

    return sfd;
}

interact_usr.c

#include "../include/qq.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>

//将fd加入epfd
int epollAddFd(int fd, int epfd)
{
    struct epoll_event event;
    memset(&event, 0, sizeof(event));

    event.events = EPOLLIN;
    event.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    ERROR_CHECK(ret, -1, "EPOLL_CTL_ADD");
    return 0;
}

//将fd从epfd中移除
int epollDelFd(int fd, int epfd)
{
    struct epoll_event event;
    memset(&event, 0, sizeof(event));

    event.events = EPOLLIN;
    event.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    ERROR_CHECK(ret, -1, "EPOLL_CTL_DEL");
    return 0;
}

//用fd查找用户
pUsr_t search_usr(pUsr_t pArr, int len, int fd)
{
    int i;
    for (i = 0; i < len; ++i) {
        if (fd == pArr[i]._usrfd) {
            return pArr + i;
        }
    }
    return NULL;
}


//功能:服务器主进程处理来自用户的连接,发信息,退出
//参数:服务器套接字,用户数组,用户最大容量
int interact_usr(int sfd, pUsr_t pUsrArr, int max_capacity)
{
    //使用epoll管理所有文件描述符
    int epfd = epoll_create(1);

    //将sfd添加进epfd
    epollAddFd(sfd, epfd);

    int ret = -1;
    char buf[256] = {0};//读写缓冲区
    int readyFdNum = 0;//就绪的文件描述符数量
    struct epoll_event evs[2]; //epoll_wait等待数组的大小
    int newfd = 0;//客户端的套接字
    int cur_count = 0; //当前在线用户数量

    //epoll等待就绪的文件描述符
    while (1) {
        readyFdNum = epoll_wait(epfd, evs, 2, -1);

        int i;
        for (i = 0; i < readyFdNum; ++i) {

            //服务端套接字就绪,有新用户连接
            if (evs[i].data.fd == sfd) {
                //接收用户端
                newfd = accept(sfd, NULL, NULL);

                //如果用户数组已满,不再接收新用户的连接
                if (cur_count == max_capacity) {
                    strcpy(buf, "网络聊天室人数已满,无法加入");
                    send(newfd, buf, strlen(buf), 0);
                    close(newfd);
                    continue;
                }

                //接收用户名
                memset(buf, 0, sizeof(buf));
                recv(newfd, buf, sizeof(buf) - 1, 0);
                //放入用户数组
                int j;
                for (j = 0; j < max_capacity; ++j) {
                    if (0 == pUsrArr[j]._online) {
                        pUsrArr[j]._usrfd = newfd;//记录用户通信fd
                        pUsrArr[j]._online = 1;//新用户状态更新成在线
                        strcpy(pUsrArr[j]._usrname, buf);//设置用户名

                        //将新用户加入epoll监听
                        epollAddFd(pUsrArr[j]._usrfd, epfd);
                        //给新用户发送欢迎信息
                        strcpy(buf, "===========================Welcome to the best Online Chat Romm==========================\n\n");
                        send(newfd, buf, strlen(buf), 0);

                        //通知其他在线用户,有新用户加入
                        memset(buf, 0, sizeof(buf));
                        sprintf(buf, "==[new user %s join!]==", pUsrArr[j]._usrname);
                        for (int k = 0; k < max_capacity; ++k) {
                            if (pUsrArr[k]._online && pUsrArr[k]._usrfd != pUsrArr[j]._usrfd) {
                                send(pUsrArr[k]._usrfd, buf, strlen(buf), 0);
                            }
                        }

                        break;
                    }
                }
                ++cur_count;//用户记录器++
                printf("cur_count : %d\n", cur_count);
            }

            //用户端发来消息
            else {
                //接收消息
                memset(buf, 0, sizeof(buf));
                ret = recv(evs[i].data.fd, buf, sizeof(buf) - 1, 0);

                //定位发消息的用户
                pUsr_t pCur = search_usr(pUsrArr, max_capacity, evs[i].data.fd);
                //如果用户退出
                if (0 == ret) {
                    --cur_count;//用户记录器--
                    pCur->_online = 0;//用户下线

                    sprintf(buf, "==[usr_%s exit!]==",pCur->_usrname);
                    puts(buf);
                    printf("cur_count : %d\n", cur_count);

                    //从epoll管理的红黑树中删除
                    epollDelFd(pCur->_usrfd, epfd);
                    //通知其他用户
                    for (int j = 0; j < max_capacity; ++j) {
                        if (pUsrArr[j]._online && pCur->_usrfd != pUsrArr[j]._usrfd) {
                            send(pUsrArr[j]._usrfd, buf, strlen(buf), 0);
                        }
                    }
                }
                //正常消息,进行转发给其他在线用户
                else {
                    char usr_info[1024] = {0};
                    sprintf(usr_info, "[%s]: %s", pCur->_usrname, buf);

                    for (int j = 0; j < max_capacity; ++j) {
                        if (pUsrArr[j]._usrfd != pCur->_usrfd && pUsrArr[j]._online) {
                            send(pUsrArr[j]._usrfd, usr_info, strlen(usr_info), 0);
                        }
                    }
                }
            }
        }
    }

    return 0;
}


客户端代码


main_client.c

#include "../include/qq.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>

#define ARGS_CHECK(argc, num) { if (argc != num) {\
    fprintf(stderr, "Args error!\n");\
    return -1;} }

//与聊天室服务器交互
int interact_qq(int sfd);

int main(int argc, char *argv[]) 
{
    ARGS_CHECK(argc, 2);
    //从配置文件中,取出服务器的ip和port
    FILE *fp = fopen(argv[1], "r");
    char ip[128] = {0};
    int port = 0;
    fscanf(fp, "%s%d", ip, &port);
    fclose(fp);

    //连接聊天室服务器
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    
    struct sockaddr_in serAddr;
    memset(&serAddr, 0, sizeof(serAddr));
    serAddr.sin_family = AF_INET;
    serAddr.sin_addr.s_addr = inet_addr(ip);
    serAddr.sin_port = htons(port);

    int ret = connect(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
    ERROR_CHECK(ret, -1, "connect");

    //发送用户名
    char username[30] = {0};
    printf("准备进入聊天室,请输入用户名:");
    scanf("%s", username);
    send(sfd, username, strlen(username), 0);
    //清空界面
    system("clear");

    //与聊天室交互
    interact_qq(sfd);

    //关闭服务器套接字
    close(sfd);
    return 0;
}

interact_qq.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>

//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg);\
    return -1;} }


//与聊天室服务器交互
int interact_qq(int sfd)
{
    //定义一个读操作集合
    fd_set rdset;
    FD_ZERO(&rdset);

    char buf[128] = {0};//读写缓冲区
    int ret = -1;
    while (1) {
        //每次select前,重置读集合,因为select会修改读集合(将未就绪的文件描述符置为0)
        FD_SET(STDIN_FILENO, &rdset);
        FD_SET(sfd, &rdset);

        //select阻塞在此,等待集合中任意一个文件描述符就绪后,解除阻塞
        //select接触阻塞后,找就绪的文件描述符,需要遍历集合去找
        ret = select(sfd + 1, &rdset, NULL, NULL, NULL);
        ERROR_CHECK(ret, -1, "select");

        //服务端套接字就绪,表示服务端有数据到来,接收并打印在终端
        if (FD_ISSET(sfd, &rdset)) {
            memset(buf, 0, sizeof(buf));
            ret = recv(sfd, buf, sizeof(buf) - 1, 0);
            if (0 == ret) {
                //服务端已关闭
                printf("server exit\n");            
                return -1;
            }
            printf("%s\n", buf);
        }

        //标准输入就绪,接收并发送给服务端
        if (FD_ISSET(STDIN_FILENO, &rdset)) {
            memset(buf, 0, sizeof(buf));
            ret = read(STDIN_FILENO, buf, sizeof(buf) - 1);
            send(sfd, buf, strlen(buf) - 1, 0);
        }
    }
}


演示

在这里插入图片描述



总结

一个练习tcpepoll的小项目

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

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

相关文章

探索EasyCVR与AI技术深度融合:视频汇聚平台的新增长点

随着5G、AI、边缘计算、物联网&#xff08;IoT&#xff09;、云计算等技术的快速发展&#xff0c;万物互联已经从概念逐渐转变为现实&#xff0c;AIoT&#xff08;物联网人工智能&#xff09;的新时代正在加速到来。在这一背景下&#xff0c;视频技术作为信息传输和交互的重要手…

常见Python GUI库分析

引言 在Python环境下进行桌面编程时&#xff0c;选择合适的GUI&#xff08;图形用户界面&#xff09;库至关重要。在Python环境下进行桌面编程GUI开发时&#xff0c;有多个优秀的库可供选择。以下是一些推荐的GUI库&#xff0c;包括它们的推荐理由、优劣势以及简单的demo示例。…

【论文阅读】SwiftTheft: A Time-Efficient Model Extraction Attack Framework(2024)

完整标题 SwiftTheft: A Time-Efficient Model Extraction Attack Framework Against Cloud-Based Deep Neural Networks 摘要 With the rise of artificial intelligence(人工智能) and cloud computing(云计算), machine-learning-as-a-service platforms(机器学习即…

SpringMVC基于注解使用:JSON

01-json处理--介绍 json数据格式回顾&#xff1a; 在pom.xml导入依赖 在web.xml里面导入配置文件 ResponseBody 注解是将返回值作为文本返回到客户端了而不是字符串了 当我们想返回bean对象的json数据的时候我们需要先导入jackson依赖在pom.xml里面 然后创建User类&#xff0c…

最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)

文章目录 一、自动配置概念二、半自动配置&#xff08;误~&#x1f64f;&#x1f64f;&#xff09;三、源码分析1、验证DispatcherServlet的自动配置2、源码分析入口SpringBootApplication3、SpringBootConfiguration的Configuration4、EnableAutoConfiguration的AutoConfigura…

猜测、实现 B 站在看人数

猜测、实现 B 站在看人数 猜测找到接口参数总结 实现 猜测 找到接口 浏览器打开一个 B 站视频&#xff0c;比如 《黑神话&#xff1a;悟空》最终预告 | 8月20日&#xff0c;重走西游_黑神话悟空 (bilibili.com) &#xff0c;打开 F12 开发者工具&#xff0c;经过观察&#xf…

【数据结构初阶】链表分类与双向带头循环链表接口实现

文章目录 1. 链表的分类2. 双向带头循环链表接口实现2. 1 结点声明2. 2 创建链表节点2. 3 初始化链表2. 4 打印链表2. 5 尾插2. 6 判空2. 7 尾删2. 8 头插2. 9 头删2. 10 查找2. 11 在指定位置删除与插入2. 12 销毁 3. 链表接口测试4. 单链表与双链表5. 顺序表与链表 1. 链表的…

Python安装:Mac 使用brew 安装Python2 和 Python3

安装python ## python2 brew install python ## python3 brew install python3出现错误 Error: An unexpected error occurred during the brew link step The formula built, but is not symlinked into /usr/local Permission denied dir_s_mkdir - /usr/local/Frameworks …

根据NVeloDocx Word模板引擎生成Word(一)

自从我们基于免费开放的E6开发平台&#xff0c;实现了根据Word模版生成Word文档的模版引擎后&#xff0c;也实实在在帮助到了一些有需要的朋友。但是由于制作Word模版的过程对于很多人会是一个很头疼的事情&#xff0c;虽然提供了详细的文档&#xff0c;但是我们也经常接到他们…

EcoPaste:重塑剪切板体验,让信息管理变得前所未有的高效

前言 科技&#xff0c;是连接梦想与现实的桥梁&#xff0c;它让人类的想象力得以飞翔&#xff0c;让未来的愿景变为现实。在它的引领下&#xff0c;我们跨越了时空的界限&#xff0c;打破了传统的束缚&#xff0c;以更加开放和包容的心态去迎接生活的挑战与机遇——在看似不相…

I2VGen-XL模型构建指南

一、介绍 VGen可以根据输入的文本、图像、指定的运动、指定的主体&#xff0c;甚至人类提供的反馈信号生成高质量的视频。它还提供了各类常用的视频生成模型工具&#xff0c;例如可视化、采样、训练、推理、使用图像和视频的联合训练&#xff0c;加速等各类工具和技术。 &quo…

图像处理基础篇-镜像仿射透视

一.图像镜像 图像镜像是图像旋转变换的一种特殊情况&#xff0c;通常包括垂直方向和水平方向的镜像。水平镜像通常是以原图像的垂直中轴为中心&#xff0c;将图像分为左右两部分进行堆成变换。如图7-1所示&#xff1a; 垂直镜像通常是以原图像的水平中轴线为中心&#xff0c;将…

快速搞定“照片调色”!50000+Lr预设滤镜模板,一键让你照片不再丑!

照片调色不仅仅是调整颜色&#xff0c;更是一种艺术表达。通过巧妙地运用 LR 预设&#xff0c;可以突出照片的主题&#xff0c;增强情感共鸣。比如&#xff0c;在风景照片中&#xff0c;使用特定的预设可以让天空更蓝、草地更绿&#xff0c;让大自然的美丽更加生动地展现出来。…

Java——踩坑Arrays.asList()

坑1&#xff1a;不能直接使用 Arrsys.asList() 来转换基本类型数据 public static void test1(){// 1、不能直接使用asList来转换基本类型数组int[] arr {1, 2, 3};List list Arrays.asList(arr);System.out.printf("list:%s size:%s class:%s", list, list.size(…

【卡码网C++基础课 18.开房门】

目录 题目描述与分析一、map的基本介绍二、map的使用三、代码编写四、范围for循环 题目描述与分析 题目描述&#xff1a; 假设你手里有一串钥匙&#xff0c;这串钥匙上每把钥匙都有一个编号&#xff0c;对应着一个房门的编号。现给你一个房门编号&#xff0c;你需要判断是否能…

【虚拟化】AIO主机安装PVE8,配置网络,安装win11(virtio,qcow2,scsi,oobe,adk)

【虚拟化】AIO主机安装PVE8&#xff0c;配置网络&#xff0c;安装win11&#xff08;virtio&#xff0c;qcow2&#xff0c;scsi&#xff0c;oobe&#xff0c;adk&#xff09; 文章目录 1、ESXI vs PVE&#xff0c;AIO主机系统二选一2、PVE网络配置&#xff08;DNS&#xff0c;换…

5G前传-介绍

1. 引用 知识分享系列一&#xff1a;5G基础知识-CSDN博客 5G前传的最新进展-CSDN博客 灰光和彩光_通信行业5G招标系列点评之二&#xff1a;一文读懂5G前传-光纤、灰光、彩光、CWDM、LWDM、MWDM...-CSDN博客 术语&#xff1a; 英文缩写描述‌BBU&#xff1a;Building Baseba…

3.门锁_STM32_矩阵按键设备实现

概述 需求来源&#xff1a; 门锁肯定是要输入密码&#xff0c;这个门锁提供了两个输入密码的方式&#xff1a;一个是蓝牙输入&#xff0c;一个是按键输入。对于按键输入&#xff0c;采用矩阵按键来实现。矩阵按键是为了模拟触摸屏的按键输入&#xff0c;后续如果项目结束前还…

关于 QImage原始数据格式与cv::Mat原始数据进行手码数据转换 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/141996117 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

神经网络骨架nn.Module

文章目录 一、认识nn.Module二、nn.Module的基础加1操作 一、认识nn.Module nn.Module 是 PyTorch 中的一个核心类&#xff0c;它是所有神经网络模块的基类。在 PyTorch 中构建模型时&#xff0c;通常会继承这个类来创建自定义的网络结构。nn.Module 提供了一系列用于构建神经…