草庐IT

基于TCP的网络聊天室实现(C语言)

T^F 2024-05-09 原文

基于TCP的网络聊天室实现(C语言)


一、网络聊天室的功能

有新用户登录,其他在线的用户可以收到登录信息
有用户发送群聊消息,其他在线的用户可以收到群聊信息
有用户退出,其他在线的用户可以收到退出信息
服务器可以发送系统信息


二、网络聊天室的结果展示

1.已经加入群聊的用户可以看到新加入群聊的用户
2.用户退出或者断线,其他用户也可以看到
3.server端可以发送系统消息给所有在聊天室的用户

三、实现思路及流程

客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程。
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程。
服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存。
数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据。

四、代码及说明

对登录聊天室的用户,需要保存用户信息,本文用链式存储来存放用户信息,因此需要用到链队列,动态分配空间。链式队列的便利之处就在于往队列中插入用户信息的时候,不用想数组那样可能需要大量移动数据。

1.LinkList.h

代码如下:

#ifndef __LINKLIST_H__
#define __LINKLIST_H__

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define ERRLOG(msg)                                         \
    do {                                                    \
        printf("%s:%s:%d\n", __FILE__, __func__, __LINE__); \
        perror(msg);                                        \
        exit(-1);                                           \
    } while (0)

#define N 128

#define LEN 128
#define NUM_USR 64

#define datatype int

//自定义结构体,用来保存所有连接的客户的IP地址、端口号以及acceptfd文件描述符这三个参数
typedef struct info {
    struct sockaddr_in clientaddr;
    int acceptfd;
    char name[16];
    char named;
    struct info* next;
    char sayHi;
} usr_info_t;

usr_info_t* info_head;

usr_info_t* LinkListNodeCreate(void);
int LinkListInsertHead(usr_info_t* head, usr_info_t* node);
usr_info_t* LinkListSearchUsrByAcceptfd(usr_info_t* h, int acceptfd);


#endif

2.LinkList.c

代码如下:

#include "LinkList.h"
#include <string.h>
/*
 *功能:创建单链表
 *参数:
 *	@:无
 *返回值:成功返回单链表节点的首地址,失败返回NULL
 *注:创建链表节点,将指针域指向NULL,并将节点内容清零
 */
usr_info_t* LinkListNodeCreate(void)
{
    usr_info_t* h;

    h = (usr_info_t*)malloc(sizeof(*h));
    if (h == NULL) {
        printf("alloc memory error\n");
        return NULL;
    }
    memset(h, 0, sizeof(*h));
    h->next = NULL;

    return h;
}

/*
 *功能:单链表按照头插法插入数据
 *参数:
 *	@head: 用户信息链表头节点的首地址
 *   @node: 要插入的节点的地址
 *返回值:成功返回0
 */
int LinkListInsertHead(usr_info_t* head, usr_info_t* node)
{
    // 1.将node插入到head中即可
    node->next = head->next;
    head->next = node;

    // 2.成功返回0
    return 0;
}

/*
 *功能:通过Acceptfd查询用户数据
 *参数:
 *	@h:用户信息链表头指针
 *  @acceptfd:accept函数产生的文件描述符
 *返回值:成功返回0,失败返回-1
 */
usr_info_t* LinkListSearchUsrByAcceptfd(usr_info_t* h, int acceptfd)
{
    while (h->next != NULL) {
        if (h->next->acceptfd == acceptfd) {
            return h->next;
        }
        h = h->next;
    }

    printf("用户不存在,查询失败\n");
}

3.client.c

代码如下:

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define ERRLOG(msg)                                         \
    do {                                                    \
        printf("%s:%s:%d\n", __FILE__, __func__, __LINE__); \
        perror(msg);                                        \
        exit(-1);                                           \
    } while (0)

void recycle()
{
    wait(NULL);
}

void tuichu()
{
    exit(0);
}

int main(int argc, const char* argv[])
{
    //参数合理性检查
    if (3 != argc) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(-1);
    }

    //创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd) {
        ERRLOG("socket error");
    }

    //填充服务器网络信息结构体
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);

    socklen_t serveraddr_len = sizeof(serveraddr);

    //与服务器建立连接
    if (-1 == connect(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
        ERRLOG("connect error");
    }

    printf("与服务器连接成功..\n");

    char buff[128] = { 0 };
    int num = 0;

    //等待子进程退出信号SIGCHLD,回收子进程资源
    signal(SIGCHLD, recycle);

    //需要分出来一个子进程,专门用于接收消息,父进程用于发送消息
    pid_t pid = fork();
    int named = 0;
    while (1) {
        if (pid == 0) { //子进程,接收消息
            signal(SIGINT, tuichu); //子进程捕获到SIGINT信号就退出
            //接收应答消息
            memset(buff, 0, sizeof(buff));
            if (-1 == (num = recv(sockfd, buff, 128, 0))) {
                ERRLOG("recv ersockfdror");
            }
            printf("%s\n", buff);

            //如果收到自己的退出消息,就结束子进程

        } else { //父进程,发送消息
            if (named == 0) {
                printf("请输入群聊名称:\n");
                named = 1;
            } else if (named == 1) {
                printf("请再次输入群聊名称:\n");
                named = 2;
            }

            fgets(buff, 128, stdin);
            if (strlen(buff) != 0) {
                buff[strlen(buff) - 1] = '\0';
            }

            //如果用户输入quit就退出
            if (!strcmp(buff, "quit")) {
                //如果退出了,最后也要发送数据"quit"
                if (-1 == send(sockfd, buff, 128, 0)) {
                    ERRLOG("send error");
                }
                char buf[22] = { 0 };
                sprintf(buf, "kill %d", pid);
                system(buf);
                wait(NULL);
                break;
            }

            //发送数据
            if (-1 == send(sockfd, buff, 128, 0)) {
                ERRLOG("send error");
            }
        }
    }
    //关闭套接字
    close(sockfd);

    return 0;
}

4.server.c

代码如下:

#include "LinkList.h"

int max_fd = 0;
int acceptfd = 0;
int ret = 0;
int i = 0;
int nbytes = 0;
char buff[N] = { 0 };
int loop = 0;
char send_buf[128] = { 0 };
//创建要监视的文件描述集合
fd_set readfds; //母本
fd_set readfds_temp; //给select擦除用的

void* sendThread(void* arg)
{
    char sys_send_buf[256] = { 0 };
    while (1) {
        memset(send_buf, 0, sizeof(send_buf));
        scanf("%s", send_buf);
        /*for (loop = 4; loop < max_fd + 1 && ret != 0; loop++)
            这样写是不对的,因为,和主线程共享ret,主线程中的ret每次结束都是会减到0的,
            所以如果这么写,一次循环也不会进入!!!!
        */
        memset(sys_send_buf, 0, sizeof(sys_send_buf));
        sprintf(sys_send_buf, "[系统消息]:%s\n", send_buf);
        printf("%s\n", sys_send_buf);

        for (loop = 4; loop < max_fd + 1; loop++) {
            if (FD_ISSET(loop, &readfds)) {
                if (-1 == send(loop, sys_send_buf, N, 0)) {
                    ERRLOG("send error");
                }
            }
        }
    }
}

int main(int argc, const char* argv[])
{
    //参数合理性检查
    if (3 != argc) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(-1);
    }

    //创建套接字 流式套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd) {
        ERRLOG("socket error");
    }

    //填充网络信息结构体
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    socklen_t serveraddr_len = sizeof(serveraddr);

    //绑定
    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
        ERRLOG("bind error");
    }

    //将套接字设置成被动监听状态
    if (-1 == listen(sockfd, 5)) {
        ERRLOG("listen error");
    }

    //清空集合
    FD_ZERO(&readfds);
    FD_ZERO(&readfds_temp);
    //将sockfd添加到集合中
    FD_SET(sockfd, &readfds);

    //更新最大文件描述符
    max_fd = max_fd > sockfd ? max_fd : sockfd;

    socklen_t clientaddr_len = sizeof(struct sockaddr_in);
    usr_info_t* usr_info_head = LinkListNodeCreate();
    usr_info_t* usr_info_node;
    usr_info_t* usr = NULL;

    pthread_t tid;
    int err_code;
    if (0 != (err_code = pthread_create(&tid, NULL, sendThread, NULL))) {
        printf("pthread_create error %s\n", strerror(err_code));
        exit(-1);
    }

    while (1) {
        readfds_temp = readfds;

        if (-1 == (ret = select(max_fd + 1, &readfds_temp, NULL, NULL, NULL))) {
            ERRLOG("select error");
        }
        //遍历集合 看哪个文件描述符就绪了
        for (i = 3; i < max_fd + 1 && ret != 0; i++) {
            if (FD_ISSET(i, &readfds_temp)) {
                if (sockfd == i) {
                    //说明有新客户端连接了,创建一个新节点保存客户信息
                    usr_info_node = LinkListNodeCreate();
                    if (-1 == (acceptfd = accept(sockfd, (struct sockaddr*)&usr_info_node->clientaddr, &clientaddr_len))) {
                        ERRLOG("accept error");
                    }
                    usr_info_node->acceptfd = acceptfd;
                    LinkListInsertHead(usr_info_head, usr_info_node); //用户信息入队

                    printf("客户端[%d]号连接到服务器..\n", usr_info_node->acceptfd);
                    printf("客户端[%d]已连接\n", ntohs(usr_info_node->clientaddr.sin_port));
                    memset(send_buf, 0, sizeof(send_buf));

                    //将新客户端的acceptfd加入到集合
                    FD_SET(acceptfd, &readfds);
                    //更新最大文件描述符
                    max_fd = max_fd > acceptfd ? max_fd : acceptfd;
                } else {
                    //说明有客户端发来数据了
                    usr = LinkListSearchUsrByAcceptfd(usr_info_head, i); //先根据acceptfd找一下用户信息

                    if (-1 == (nbytes = recv(i, buff, N, 0))) {
                        ERRLOG("recv error");
                    } else if (0 == nbytes) {
                        printf("客户端[%d]断开连接..\n", i);
                        memset(send_buf, 0, sizeof(send_buf));
                        sprintf(send_buf, "用户[%s]断开连接..", usr->name);
                        for (loop = 4; loop < max_fd + 1 && ret != 0; loop++) {
                            if (FD_ISSET(loop, &readfds) && loop != i) {
                                if (-1 == send(loop, send_buf, N, 0)) {
                                    ERRLOG("send error");
                                }
                            }
                        }
                        //将该客户端的文件描述符在集合中删除
                        FD_CLR(i, &readfds);
                        close(i);
                        continue;
                    }
                    if (!strncmp(buff, "quit", 4)) {
                        printf("客户端[%d]退出了..\n", i);
                        memset(send_buf, 0, sizeof(send_buf));
                        sprintf(send_buf, "用户[%s]退出了..", usr->name);
                        for (loop = 4; loop < max_fd + 1 && ret != 0; loop++) {
                            if (FD_ISSET(loop, &readfds) && loop != i) {
                                if (-1 == send(loop, send_buf, N, 0)) {
                                    ERRLOG("send error");
                                }
                            }
                        }
                        //将该客户端的文件描述符在集合中删除
                        FD_CLR(i, &readfds);
                        close(i);
                        continue;
                    }

                    //第一次发过来的是用户的名字,所以,应该保存一下
                    if (usr->named != 9) {
                        strcpy(usr->name, buff);
                        usr->named = 9;
                        continue;
                    }

                    printf("客户端[%d]发来数据[%s]..\n", i, buff);
                    //组装应答
                    if (usr->sayHi == 7) {
                        memset(send_buf, 0, sizeof(send_buf));
                        sprintf(send_buf, "用户[%s]:%s\n", usr->name, buff);
                    } else { //只有第一次才会对所有已经加入群聊的用户显示**加入群聊
                        memset(send_buf, 0, sizeof(send_buf));
                        sprintf(send_buf, "用户[%s]加入群聊..\n", usr->name);
                        usr->sayHi = 7;
                    }
                    //如果用户的acceptfd还在readfds里,就接收本次用户发送的消息.注意不能从3开始,3是sockfd,给sockfd发消息,会出错!!!!
                    for (loop = 4; loop < max_fd + 1 && ret != 0; loop++) {
                        if (FD_ISSET(loop, &readfds) && loop != i) {
                            if (-1 == send(loop, send_buf, N, 0)) {
                                ERRLOG("send error");
                            }
                        }
                    }
                    ret--;
                }
            }
        }
    }
    close(sockfd);

    return 0;
}

有关基于TCP的网络聊天室实现(C语言)的更多相关文章

  1. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  4. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  5. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  6. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  7. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  8. 7个大一C语言必学的程序 / C语言经典代码大全 - 2

    嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

随机推荐