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

概述

使用了 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 
#include 
#include 
#include 
#include 
#include 

//检查命令行参数个数
#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 
#include 
#include 

#include 

#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 
#include 
#include 
#include 

#include 
#include 
#include 

//将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 
#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

//检查系统调用返回值
#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);
        }
    }
}


演示

【C-实践】网络聊天室(1.0)_第1张图片



总结

一个练习tcpepoll的小项目

你可能感兴趣的:(#,C语言实践,网络,c语言)