UDP实现网络聊天室

本文介绍了一个使用UDP协议实现的聊天室软件设计。服务器通过链表保存客户端地址,客户端使用多线程同时处理发送和接收消息。登录、发送和退出操作被优化,客户端在捕获SIGINT信号时能发送退出消息并关闭。服务器端则通过线程发送消息,并管理链表中的客户端地址。
摘要由CSDN通过智能技术生成
        利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。

        关于如何保存客户端的地址结构,这里我用的是链表。

链表节点结构体:
struct node{
	struct sockaddr_in addr;
	struct node *next;
};

消息对应的结构体(同一个协议)
typedef struct msg_t
{
    int type;//L  C  Q  
    char name[32];//用户名
    char text[128];//消息正文
}MSG_t;
  • 客户端如何同时处理发送和接收?

客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。

这里给出流程图

这里根据流程图内容将代码写出,代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>

#define POST 7598

void handler();
typedef struct msg_t
{
    char type;      //L登录  C聊天  Q退出
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG;

int main(int argc, char const *argv[])
{
    int lfd, cfd, ret, m;
    int shu = 0;
    MSG c, s;

    //创建套接字
    lfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (lfd == -1)
        perror("socket error");

    //初始化地址结构
    struct sockaddr_in caddr;
    caddr.sin_family = AF_INET;
    caddr.sin_port = htons(POST);
    caddr.sin_addr.s_addr = inet_addr("172.20.10.12");

    socklen_t clen;
    clen = sizeof(caddr);

    pid_t pid;

    //设置信号捕捉函数,回收子进程
    signal(SIGCHLD, handler);
    pid = fork();
    if (pid == 0)
    {
        while (1)
        {
            scanf("%c %s", &c.type, c.name);
            getchar();

            //登录
            if (c.type == 'L' || c.type == 'l')
            {
                if (shu == 0)
                {
                    //发送。
                    m = sendto(lfd, (char *)&c, 256, 0, (struct sockaddr *)&caddr, clen);
                    if (m == -1)
                    {
                        perror("recvfeom error");
                    }
                    shu++;
                }
                else
                    printf("请勿重复登录!\n");
            }

            //退出
            else if (c.type == 'Q' || c.type == 'q')
            {
                if (shu > 0)
                {
                    strncpy(c.text, "Q", 1);
                    m = sendto(lfd, (char *)&c, 256, 0, (struct sockaddr *)&caddr, clen);
                    if (m == -1)
                    {
                        perror("recvfeom error");
                    }
                    exit(0);
                }
                else
                    printf("请先登录!\n");
            }

            //发消息
            else if (c.type == 'C' || c.type == 'c')
            {
                if (shu > 0)
                {
                    fgets(c.text, 256, stdin);
                    if (c.text[strlen(c.text) - 1] == '\n')
                        c.text[strlen(c.text) - 1] = '\0';
                    m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
                    if (m == -1)
                    {
                        perror("recvfeom error");
                    }
                    printf("发送完成!\n");
                }
                else 
                    printf("请先登录!\n");
            }
        }
    }
    if (pid > 0)
    {
        while (1)
        {
            printf("读取服务器终端.......\n");
            int n = recvfrom(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, &clen);
            if (n < 0)
            {
                perror("recvfrom error");
                return -1;
            }
            if (s.type == 'L' || s.type == 'l')
            {
                printf("name:%s\n", s.text);
            }
            else if (s.type == 'Q' || s.type == 'q')
            {
                printf("name:%s\n", s.text);
            }
            // else if (s.type == 'C' || s.type == 'c')
            else
            {
                printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
                printf("name:%s %s\n", s.name, s.text);
            }
        }
    }
    return 0;
}

//信号捕捉函数, 用于回收子线程
void handler()
{
    waitpid(-1, NULL, WNOHANG);
    exit(getppid());
}

服务器代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>

typedef struct node_t
{
    char ip[50];
    int port;
    struct node_t *next;
} link_node_t, *link_list_t;

typedef struct msg_t
{
    char type;      //L  C  Q
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG_t;

int main(int argc, char const *argv[])
{
    int lfd, cfd, ret;
    char buf[256], file[256], ip[50];
    MSG_t c, s;

    //创建套接字
    lfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (lfd == -1)
        perror("socket error");
    else
        printf("套接字创建成功!\n");

    //初始化地址结构
    struct sockaddr_in laddr, caddr;
    laddr.sin_family = caddr.sin_family = AF_INET;
    laddr.sin_port = htons(7598);
    laddr.sin_addr.s_addr = inet_addr("172.20.10.12");

    socklen_t llen, clen;
    llen = sizeof(laddr);
    clen = sizeof(caddr);

    ret = bind(lfd, (struct sockaddr *)&laddr, llen);
    if (ret == -1)
    {
        perror("bind error");
    }

    //创建一个单项链表
    link_list_t p = (link_list_t)malloc(sizeof(link_node_t));
    if (p == NULL)
    {
        perror("p malloc error");
    }
    p->port = -1;
    p->next = NULL;
    link_list_t q = p;

    while (1)
    {
        q = p;
        //读取
        printf("读取信息中!\n");
        int m = recvfrom(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&laddr, &llen);
        if (m == -1)
        {
            perror("recvfeom error");
            return -1;
        }
        else
        {
            printf("服务器收到消息!\n");
        }
        printf("%s,%d\n", inet_ntoa(laddr.sin_addr), ntohs(laddr.sin_port));

        if (c.type == 'L' || c.type == 'l')
        {
            printf("%s正在登录.....\n", c.name);
            link_list_t new = NULL;                         //创建新链表保存,发端的地址结构
            new = (link_list_t)malloc(sizeof(link_node_t));
            if (new == NULL)
            {
                perror("new malloc error");
            }
            new->port = ntohs(laddr.sin_port);
            strcpy(new->ip, inet_ntoa(laddr.sin_addr));
            new->next = NULL;
            q = p;

            //尾插,判断是不是只有头部
            if (q->next == NULL)
            {
                q->next = new;
            }

            //尾插
            else
            {
                q = q->next;
                while (q->next != NULL)
                {
                    q = q->next;
                }
                q->next = new;
            }

            //找到头部,开始轮循发送谁登录上来,
            //这地方我设置的一定是尾插,只用个前面发送登录信息即可,
            q = p;
            q = q->next;
            while (1)
            {   
                if (q->next == NULL)   //所以在这个位置,我检测到是最后一个的时候,直接跳出即可
                    break;
                else
                {
                    caddr.sin_port = htons(q->port);
                    caddr.sin_addr.s_addr = inet_addr(q->ip);

                    s.type = c.type;
                    strcpy(s.name, c.name);
                    strcpy(s.text, c.name);
                    strcat(s.text, " record!");

                    sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
                }
                q = q->next;
            }
            printf("%s登录完成\n", c.name);
        }
        else if (c.type == 'Q' || c.type == 'q')
        {
            printf("%s退出中......\n", c.name);

            q = p;
            while (1)
            {
                if (q->next->port == ntohs(laddr.sin_port))   //这个地方我先将推出的那个客户端的地址结构从链表上移除
                {                                             //然后我向剩下的所有人发退出信息即可
                    link_list_t pnew = q->next;
                    q->next = pnew->next;
                    free(pnew);
                    break;
                }
                else
                    q = q->next;
            }
            q = p;
            if (q->next == NULL)
            {
                printf("退出完成!\n");
                continue;
            }
            else    //轮循发送推出信息
                q = p->next;
            while (1)
            {
                if (q->next == NULL)            //这个地方是判断是不是最后一个,最后一个的话,发送给他之后,直接退出就行了
                {
                    caddr.sin_port = htons(q->port);
                    caddr.sin_addr.s_addr = inet_addr(q->ip);
                    s.type = c.type;
                    strcpy(s.text, c.name);
                    strcat(s.text, " quit!");
                    sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
                    break;
                }
                caddr.sin_port = htons(q->port);
                caddr.sin_addr.s_addr = inet_addr(q->ip);
                s.type = c.type;
                strcpy(s.text, c.name);
                strcat(s.text, " quit!");
                sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
                q = q->next;
            }
            printf("%s退出完成\n", c.name);
        }
        else if (c.type == 'C' || c.type == 'c')
        {
            printf("%s接收消息中......\n", c.name);
            s = c;
            printf("type:%c name:%s text:%s\n", s.type, s.name, s.text);

            q = p;
            q = q->next;
            while (1)
            {
                if (q->port == ntohs(laddr.sin_port))               //一个道理,即不要给自己发送消息
                {
                    if(q->next == NULL)
                        break;
                    q = q->next;
                    continue;
                }

                if (q->next == NULL)                                //判断是不是最后一个
                {
                    caddr.sin_port = htons(q->port);
                    caddr.sin_addr.s_addr = inet_addr(q->ip);
                    s = c;
                    sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
                    break;
                }
                else                                                    //不然轮循发送
                {
                    caddr.sin_port = htons(q->port);
                    caddr.sin_addr.s_addr = inet_addr(q->ip);
                    s = c;
                    sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
                    q = q->next;
                }
            }
            printf("%s发送消息完成\n", c.name);
        }
    }

    return 0;
}

这里第一次实现这个功能写的不是很好,经老师指正之后改进点如下:

        1.登录没有必要写道循环里面,写道循环里面不仅麻烦,反人类还会使代码的时间复杂度提高是很没有必要的做法。正确的是将登录写在程序运行的开始,先登录了才能收发消息和退出。

        2.退出信息和收发消息可以写到一块,客户不会说是刻意的打上名字再打出quit消息,所以再客户端收发消息的时候判断客户写的消息内容,如是“quit”则退出客户端,并且杀死所有进程即可。

        3.要注意的一个点,由于只是一个小小小的程序,所以在客户端这边常用Crtl+c来杀死进程,但是这样的话,服务器会残留杀死的这个客户端的链表信息,导致浪费,所以这里加入了判断SIGNAL函数,用以判断Crtl+c发送的sigint信号,当捕捉到该信号了以后,就先向服务器发送退出消息了以后,再将客户端退出了即可。

修改完的代码如下:

   客户端的:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>

#define POST 7598

void handler();
void quit();
typedef struct msg_t
{
    char type;      //L登录  C聊天  Q退出
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG;

struct sockaddr_in caddr;
socklen_t clen;
MSG c, s;
int lfd, m;

int main(int argc, char const *argv[])
{
    int cfd, ret;
    int shu = 0;

    //创建套接字
    lfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (lfd == -1)
        perror("socket error");

    //初始化地址结构
    caddr.sin_family = AF_INET;
    caddr.sin_port = htons(POST);
    caddr.sin_addr.s_addr = inet_addr("172.20.10.12");

    clen = sizeof(caddr);

    pid_t pid;

    //登录
    printf("请输入名称\n");
    scanf("%s", c.name);
    getchar();

    //发送。
    c.type = 'l';
    m = sendto(lfd, (char *)&c, 256, 0, (struct sockaddr *)&caddr, clen);
    if (m == -1)
    {
        perror("recvfeom error");
    }
    shu++;

    //设置信号捕捉函数,回收子进程
    signal(SIGCHLD, handler);
    signal(SIGINT, quit);
    pid = fork();
    if (pid == 0)
    {
        while (1)
        {
            fgets(c.text, 256, stdin);
            if (c.text[strlen(c.text) - 1] == '\n')
                c.text[strlen(c.text) - 1] = '\0';
            if (strncmp(c.text, "quit", 4) == 0)
            {
                c.type = 'q';
                m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
                if (m == -1)
                    perror("recvfeom error");
                break;
            }
            else
            {
                c.type = 'c';
                m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
                if (m == -1)
                    perror("recvfeom error");
            }
        }
        close(lfd);
        exit(-1);
        return 0;
    }
    if (pid > 0)
    {
        while (1)
        {
            printf("读取服务器终端.......\n");
            int n = recvfrom(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, &clen);
            if (n < 0)
            {
                perror("recvfrom error");
                return -1;
            }
            if (s.type == 'L' || s.type == 'l')
            {
                printf("name:%s\n", s.text);
            }
            else if (s.type == 'Q' || s.type == 'q')
            {
                printf("name:%s\n", s.text);
            }
            // else if (s.type == 'C' || s.type == 'c')
            else
            {
                printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
                printf("name:%s %s\n", s.name, s.text);
            }
        }
    }
    return 0;
}

//信号捕捉函数, 用于回收子线程
void handler()
{
    waitpid(-1, NULL, WNOHANG);
    exit(getppid());
}

void quit()
{
    c.type = 'q';
    m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
    if (m == -1)
        perror("recvfeom error");
    exit(-1);
}

服务器的代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>

typedef struct node_t
{
    char ip[50];
    int port;
    struct node_t *next;
} link_node_t, *link_list_t;

typedef struct msg_t
{
    char type;      //L  C  Q
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG_t;

void *server_send(void *arg);
MSG_t c, s;

int main(int argc, char const *argv[])
{
    int lfd, cfd, ret;
    char buf[256], file[256], ip[50];

    //创建套接字
    lfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (lfd == -1)
        perror("socket error");
    else
        printf("套接字创建成功!\n");

    //初始化地址结构
    struct sockaddr_in laddr, caddr;
    laddr.sin_family = caddr.sin_family = AF_INET;
    laddr.sin_port = htons(7598);
    laddr.sin_addr.s_addr = inet_addr("172.20.10.12");

    socklen_t llen, clen;
    llen = sizeof(laddr);
    clen = sizeof(caddr);

    ret = bind(lfd, (struct sockaddr *)&laddr, llen);
    if (ret == -1)
    {
        perror("bind error");
    }

    //创建一个单项链表
    link_list_t p = (link_list_t)malloc(sizeof(link_node_t));
    if (p == NULL)
    {
        perror("p malloc error");
    }
    p->port = -1;
    p->next = NULL;
    link_list_t q = p;

    pthread_t tid;
    ret = pthread_create(&tid, NULL, server_send, NULL);
    if (ret != 0)
    {
        perror("pthread err");
        return -1;
    }
    pthread_detach(tid);

    while (1)
    {
        q = p;
        //读取
        printf("读取信息中!\n");
        int m = recvfrom(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&laddr, &llen);
        if (m == -1)
        {
            perror("recvfeom error");
            return -1;
        }
        else
        {
            printf("服务器收到消息!\n");
        }
        printf("%s,%d\n", inet_ntoa(laddr.sin_addr), ntohs(laddr.sin_port));

        if (c.type == 'L' || c.type == 'l')
        {
            printf("%s正在登录.....\n", c.name);
            //找到头部,开始轮循发送谁登录上来,
            //这地方我设置的一定是尾插,只用个前面发送登录信息即可,
            q = p;
            while (q->next != NULL)
            {
                q = q->next;
                caddr.sin_port = htons(q->port);
                caddr.sin_addr.s_addr = inet_addr(q->ip);
                s.type = c.type;
                strcpy(s.name, c.name);
                strcpy(s.text, c.name);
                strcat(s.text, " record!");
                sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
            }

            //创建新链表保存,发端的地址结构 = (link_list_t)malloc(sizeof(link_node_t));
            link_list_t new = (link_list_t)malloc(sizeof(link_node_t));
            if (new == NULL)
            {
                perror("new malloc error");
            }
            new->port = ntohs(laddr.sin_port);
            strcpy(new->ip, inet_ntoa(laddr.sin_addr));
            new->next = NULL;

            q->next = new;

            printf("%s登录完成\n", c.name);
        }
        else if (c.type == 'Q' || c.type == 'q')
        {
            printf("%s退出中......\n", c.name);
            q = p;
            while (q->next != NULL)
            {
                q = q->next;
                if(q->next == NULL)
                {
                    break;
                }
                if (q->port == ntohs(laddr.sin_port)) //这个地方我先将推出的那个客户端的地址结构从链表上移除
                {                                           //然后我向剩下的所有人发退出信息即可
                    q->port = q->next->port;
                    strcpy(q->ip,q->next->ip);
                    q->next = q->next->next;
                    free(q->next);
                }
                printf("*****\n");
                caddr.sin_port = htons(q->port);
                caddr.sin_addr.s_addr = inet_addr(q->ip);
                s.type = c.type;
                strcpy(s.text, c.name);
                strcat(s.text, " quit!");
                sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
            }
            printf("%s退出完成\n", c.name);
        }
        else if (c.type == 'C' || c.type == 'c')
        {
            printf("%s接收消息中......\n", c.name);
            s = c;
            printf("type:%c name:%s text:%s\n", s.type, s.name, s.text);

            q = p;
            while (q->next != NULL)
            {
                q = q->next;
                if (q->port == ntohs(laddr.sin_port)) //一个道理,即不要给自己发送消息
                {
                    if (q->next == NULL)
                        break;
                    continue;
                }
                else //不然轮循发送
                {
                    caddr.sin_port = htons(q->port);
                    caddr.sin_addr.s_addr = inet_addr(q->ip);
                    sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
                }
            }
            printf("%s发送消息完成\n", c.name);
        }
    }

    return 0;
}

void *server_send(void *arg)
{
    while (1)
    {
        fgets(s.text, 256, stdin);
        if (s.text[strlen(s.text) - 1] == '\n')
            s.text[strlen(s.text) - 1] = '\0';
        // printf("%s\n", s.text);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值