zoukankan      html  css  js  c++  java
  • C 基于UDP实现一个简易的聊天室

    引言

      本文是围绕Linux udp api 构建一个简易的多人聊天室.重点看思路,帮助我们加深

    对udp开发中一些api了解.相对而言udp socket开发相比tcp socket开发注意的细节要少很多.

    但是水也很深. 本文就当是一个demo整合帮助开发者回顾和继续了解 linux udp开发的基本流程.

    首先我们来看看 linux udp 和 tcp的异同.

    /*
    这里简单比较一下TCP和UDP在编程实现上的一些区别:
    
    TCP流程
         建立一个TCP连接需要三次握手,而断开一个TCP则需要四个分节。当某个应用进程调用close(主动端)后
    (可以是服务器端,也可以是客户 端),这一端的TCP发送一个FIN,表示数据发送完毕;另一端(被动端)发送一
    个确认,当被动端待处理的应用进程都处理完毕后,发送一个FIN到主动端,并关闭套接口,主动端接收到这个
    FIN后再发送一个确认,到此为止这个TCP连接被断开。 UDP套接口   UDP套接口是无连接的、不可靠的数据报协议;既然他不可靠为什么还要用呢?
      其一:当应用程序使用广播或多播是只能使用UDP协议;
      其二:由于它是无连接的,所以速度快。因为UDP套接口是无连接的,如果一方的数据报丢失,那另一方将无
    限等待,解决办法是设置一个超时。在编写UDP套接口程序时,有几点要注意:建立套接口时socket函
    数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口;由于UDP是无连接的,所以服务器端
    并不需要listen或accept函数;当UDP套接口调用connect函数时,内核只记录连接放的IP地址 和端
    口,并立即返回给调用进程.
    */

     参照

        linux udp api简介   http://blog.csdn.net/wocjj/article/details/8315559

         tcp 和udp区别    http://www.cnblogs.com/Jessy/p/3536163.html

    这里简单引述一下 udp相比tcp 用到的两个api .  recvfrom()/sendto() 具体细节如下

    #include <sys/types.h>  
    #include <sys/socket.h> 
    
    /*
     * 这两个函数基本等同于 一个 send 和 recv . 详细参数解释如下
     * s        : 文件描述符,等同于 socket返回的值
     * buf        : 数据其实地址
     * len        : 发送数据长度或接受数据缓冲区最大长度
     * flags    : 发送标识,默认就用O.带外数据使用 MSG_OOB, 偷窥用MSG_PEEK .....
     * addr     : 发送的网络地址或接收的网络地址
     * alen     : sento标识地址长度做输入参数, recvfrom表示输入和输出参数.可以为NULL此时addr也要为NULL
     *        : 返回0表示执行成功,否则返回<0 . 更多细节查询man手册
     */
    extern int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *addr, int alen);  
    extern int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *addr, int *alen); 

    上面就是两个函数的大致用法. 具体可以查看linux api帮助手册. 最好就用 man sendto / man recvfrom 把那一系列函数都看看.

    现在很多文章都是转载,但是找不见转载的地址, 下面会举一个简易的UDP回显服务器的demo加深理解.

    前言

      首先看设计图

    有点low. 简单看看吧. 那我们先看 客户端代码  udpclt.c 代码

    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define _SHORT_PORT    (8088)
    
    //
    // udp client heoo
    //
    int main(int argc, char * argv[]) {
        int fd, len;
        struct sockaddr_in ar = { AF_INET };
        socklen_t al = sizeof (struct sockaddr_in);
        char msg[BUFSIZ] = ":) 谁也不会喜欢工作狂 ~";
    
        // 创建 client socket
        if ((fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) {
            perror("main socket dgram");
            exit(EXIT_FAILURE);
        }
    
        ar.sin_port = htons(_SHORT_PORT);
        // 开始发送消息包到服务器, 默认走 INADDR_ANY
        sendto(fd, msg, sizeof msg - 1, 0, (struct sockaddr *)&ar, al);
        
        // 开始接收服务器回过来的报文
        len = recvfrom(fd, msg, sizeof msg - 1, 0, (struct sockaddr *)&ar, &al);
        if (len == -1) {
            perror("main recvfrom");
            exit(EXIT_FAILURE);
        }
        msg[len] = '';
        printf("[%s:%hd] -> %s
    ", inet_ntoa(ar.sin_addr), ntohs(ar.sin_port), msg);
    
        // 程序结束
        return close(fd);
    }

    编译是

    gcc -g -Wall -o udpclt.out udpclt.c

    udp 服务器 udpsrv.c

    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define _SHORT_PORT    (8088)
    
    //
    // udp server heoo
    //
    int main(int argc, char * argv[]) {
        int fd, len;
        struct sockaddr_in ar = { AF_INET };
        socklen_t al = sizeof (struct sockaddr_in);
        char msg[BUFSIZ];
    
        if ((fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) {
            perror("main socket dgram");
            exit(EXIT_FAILURE);
        }
        
        printf("udp server start [%d][0.0.0.0][%hd] ...
    ", fd, _SHORT_PORT);
    
        // 服务器绑定地址
        ar.sin_port = htons(_SHORT_PORT);
        if (bind(fd, (struct sockaddr *)&ar, al) == -1) {
            perror("main bind INADDR_ANY");
            exit(EXIT_FAILURE);
        }
    
        // 阻塞的接收不同客户端数据来回搞
        while ((len = recvfrom(fd, msg, sizeof msg - 1, 0, (struct sockaddr *)&ar, &al)) > 0) {
            msg[len] = '';
            printf("[%s:%hd] -> %s
    ", inet_ntoa(ar.sin_addr), ntohs(ar.sin_port), msg);
    
            // 回显继续发送给客户端
            sendto(fd, msg, len, 0, (struct sockaddr *)&ar, al);
        }
    
        return close(fd);
    }

    编译是

    gcc -g -Wall -o udpsrv.out udpsrv.c

    后面运行结果如下 udp服务器如下 (Ctrl + C 退出)

    udp 客户端如下(修改了一版本, 当前版本更加简单, 容易理解.)

    到这里将上面代码 敲一遍基本上 udp  一套 api 就会使用了. 后面进入正题设计聊天室代码.

    正文

      首先看客户端设计代码. 主要思路是子进程处理数据的输出, 父进程处理服务器数据的接收. 具体设计如下(画的图有点low就不画了.../(ㄒoㄒ)/~~)

    udpmulclt.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <signal.h>
    #include <sys/wait.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    // 名字长度包含''
    #define _INT_NAME (64)
    // 报文最大长度,包含''
    #define _INT_TEXT (512)
    
    //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏
    #define CERR(fmt, ...) 
        fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "
    ",
             __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
    
    //4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
    #define CERR_EXIT(fmt,...) 
        CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
    /*
     * 简单的Linux上API错误判断检测宏, 好用值得使用
     */
    #define IF_CHECK(code) 
        if((code) < 0) 
            CERR_EXIT(#code)
    
    // 发送和接收的信息体
    struct umsg{
        char type;                //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
        char name[_INT_NAME];    //保存用户名字
        char text[_INT_TEXT];    //得到文本信息,空间换时间
    };
    
    /*
     * udp聊天室的客户端, 子进程发送信息,父进程接受信息
     */
    int main(int argc, char* argv[]) {
        int sd, rt;
        struct sockaddr_in addr = { AF_INET };
        socklen_t alen = sizeof addr;
        pid_t pid;
        struct umsg msg = { '1' };    
    
        // 这里简单检测
        if(argc != 4) {
            fprintf(stderr, "uage : %s [ip] [port] [name]
    ", argv[0]);
            exit(-1);
        }    
        // 下面对接数据
        if((rt = atoi(argv[2]))<1024 || rt > 65535)
            CERR("atoi port = %s is error!", argv[2]);
        // 接着判断ip数据
        IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
        addr.sin_port = htons(rt);
        // 这里拼接用户名字
        strncpy(msg.name, argv[3], _INT_NAME - 1);
        
        //创建socket 连接
        IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
        // 这里就是发送登录信息给udp聊天服务器了
        IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));    
        
        //开启一个进程, 子进程处理发送信息, 父进程接收信息
        IF_CHECK(pid = fork());
        if(pid == 0) { //子进程,先忽略退出处理防止成为僵尸进程
            signal(SIGCHLD, SIG_IGN);                
            while(fgets(msg.text, _INT_TEXT, stdin)){
                if(strcasecmp(msg.text, "quit
    ") == 0){ //表示退出
                    msg.type = '3';
                    // 发送数据并检测
                    IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
                    break;
                }
                // 洗唛按发送普通信息
                msg.type = '2';
                IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
            }
            // 处理结算操作,并杀死父进程
            close(sd);
            kill(getppid(), SIGKILL);
            exit(0);
        }
        // 这里是父进程处理数据的读取
        for(;;){
            bzero(&msg, sizeof msg);
            IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
            msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '';
            switch(msg.type){
            case '1':printf("%s 登录了聊天室!
    ", msg.name);break;
            case '2':printf("%s 说了: %s
    ", msg.name, msg.text);break;
            case '3':printf("%s 退出了聊天室!
    ", msg.name);break;
            default://未识别的异常报文,程序直接退出
                fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]
    ", inet_ntoa(addr.sin_addr),
                        ntohs(addr.sin_port), msg.type, msg.name, msg.text);
                goto __exit;
            }
        }    
    
    __exit:    
        // 杀死并等待子进程退出
        close(sd);
        kill(pid, SIGKILL);
        waitpid(pid, NULL, -1);    
    
        return 0;
    }

    这里主要需要注意的是

    // 发送和接收的信息体
    struct umsg{
        char type;                //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
        char name[_INT_NAME];    //保存用户名字
        char text[_INT_TEXT];    //得到文本信息,空间换时间
    };

    传输和接收的数据格式, type表示协议或行为. 我这里细心了处理 name, text最后一个字符必须是 ''. 其它都是业务代码.再扯一点

    struct sockaddr_in addr = { AF_INET };

    等价于

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;

    也是一个C开发中技巧吧. 再扯一点linux上提供 bzero函数, 但是window上没有. 写了个通用的如下

    //7.0 置空操作
    #ifndef BZERO
    //v必须是个变量
    #define BZERO(v) 
        memset(&v,0,sizeof(v))
    #endif/* !BZERO */    

    可以试试吧毕竟跨平台....

    好了那我们说 udp 聊天室的服务器设计思路. 就是服务器会维护一个客户端链表. 有信息来就广播. 好简单吧.就是这样.正常的事都简单.

    简单的是美的. 好了看代码总设计和实现. udpmulsrv.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <signal.h>
    #include <sys/wait.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    // 名字长度包含''
    #define _INT_NAME (64)
    // 报文最大长度,包含''
    #define _INT_TEXT (512)
    
    //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏
    #define CERR(fmt, ...) 
        fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "
    ",
             __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
    
    //4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
    #define CERR_EXIT(fmt,...) 
        CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
    /*
     * 简单的Linux上API错误判断检测宏, 好用值得使用
     */
    #define IF_CHECK(code) 
        if((code) < 0) 
            CERR_EXIT(#code)
    
    // 发送和接收的信息体
    struct umsg{
        char type;                //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
        char name[_INT_NAME];    //保存用户名字
        char text[_INT_TEXT];    //得到文本信息,空间换时间
    };
    
    // 维护一个客户端链表信息,记录登录信息
    typedef struct ucnode {
        struct sockaddr_in addr;
        struct ucnode* next;
    } *ucnode_t ;
    
    
    // 新建一个结点对象
    static inline ucnode_t _new_ucnode(struct sockaddr_in* pa){
        ucnode_t node = calloc(sizeof(struct ucnode), 1);    
        if(NULL == node)
            CERR_EXIT("calloc sizeof struct ucnode is error. ");
        node->addr = *pa;
        return node;
    }
    
    // 插入数据,这里head默认头结点是当前服务器结点
    static inline void _insert_ucnode(ucnode_t head, struct sockaddr_in* pa) {
        ucnode_t node = _new_ucnode(pa);
        node->next = head->next;
        head->next = node;    
    }
    
    // 这里是有用户登录处理
    static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
        _insert_ucnode(head, pa);
        head = head->next;
        // 从此之后才为以前的链表
        while(head->next){
            head = head->next;
            IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
        }
    }
    
    // 信息广播
    static void _broadcast_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
        int flag = 0; //1表示已经找到了
        while(head->next) {
            head = head->next;
            if((flag) || !(flag=memcmp(pa, &head->addr, sizeof(struct sockaddr_in))==0)){
                IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
            }
        }
    }
    
    // 有人退出群聊
    static void _quit_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
        int flag = 0;//1表示已经找到
        while(head->next) {
            if((flag) || !(flag = memcmp(pa, &head->next->addr, sizeof(struct sockaddr_in))==0)){
                IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->next->addr, sizeof(struct sockaddr_in)));
                head = head->next;
            }        
            else { //删除这个退出的用户
                ucnode_t tmp = head->next;
                head->next = tmp->next;
                free(tmp);
            }
        }        
    }
    
    // 销毁维护的对象池,没有往复杂的考虑了简单处理退出了
    static void _destroy_ucnode(ucnode_t* phead) {
        ucnode_t head;
        if((!phead) || !(head=*phead)) return;    
        while(head){
            ucnode_t tmp = head->next;
            free(head);
            head = tmp;
        }    
    
        *phead = NULL;
    }
    
    /*
     * udp聊天室的服务器, 子进程广播信息,父进程接受信息
     */
    int main(int argc, char* argv[]) {
        int sd, rt;
        struct sockaddr_in addr = { AF_INET };
        socklen_t alen = sizeof addr;
        struct umsg msg;    
        ucnode_t head;
    
        // 这里简单检测
        if(argc != 3) {
            fprintf(stderr, "uage : %s [ip] [port]
    ", argv[0]);
            exit(-1);
        }    
        // 下面对接数据
        if((rt = atoi(argv[2]))<1024 || rt > 65535)
            CERR("atoi port = %s is error!", argv[2]);
        // 接着判断ip数据
        IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
        addr.sin_port = htons(rt); //端口要采用网络字节序
        // 创建socket
        IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
        // 这里bind绑定设置的地址
        IF_CHECK(bind(sd, (struct sockaddr*)&addr, alen));
        
        //开始监听了
        head = _new_ucnode(&addr);    
        for(;;){
            bzero(&msg, sizeof msg);
            IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
            msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '';
            fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]
    ", inet_ntoa(addr.sin_addr),
                        ntohs(addr.sin_port), msg.type, msg.name, msg.text);
            // 开始判断处理
            switch(msg.type) {
            case '1':_login_ucnode(head, sd, &addr, &msg);break;
            case '2':_broadcast_ucnode(head, sd, &addr, &msg);break;
            case '3':_quit_ucnode(head, sd, &addr, &msg);break;
            default://未识别的异常报文,程序把其踢走
                fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]
    ", inet_ntoa(addr.sin_addr),
                        ntohs(addr.sin_port), msg.type, msg.name, msg.text);
                _quit_ucnode(head, sd, &addr, &msg);
                break;
            }        
        }
            
        // 这段代码是不会执行到这的, 可以加一些控制让其走到这. 看人
        close(sd);
        _destroy_ucnode(&head);    
        return 0;
    }

    这里主要围绕的结构就是

    // 维护一个客户端链表信息,记录登录信息
    typedef struct ucnode {
        struct sockaddr_in addr;
        struct ucnode* next;
    } *ucnode_t ;

    注册添加登录广播退出等.这里再扯一下. 关于C static开发技巧. C中有一种 *.h 开发模式, 全部采用static 内嵌代码段. 这样

    可以省略*.c 文件. 小巧的封装可以使用. 继续扯一点. 开发也写C++,虽然鄙视. C++ 中有个 *.hpp文件. 比较好. 它表达的意思

    是这个代码是开源的. 全部采用充血模型. 类中代码都放在类中实现.非常值得提倡. 这也是学boost的时候学到的. 很实在.

    好了说代码吧. 也比较随大流. 看看也都明白了. 简单分析一处吧

    // 这里是有用户登录处理
    static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
        _insert_ucnode(head, pa);
        head = head->next;
        // 从此之后才为以前的链表
        while(head->next){
            head = head->next;
            IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
        }
    }

    因为我采用的头查法. 那就除了刚插入的头的下一个结点都需要发送登录信息. 比较精巧.

    好看编译命令

    gcc -g -Wall -o udpmulsrv.out udpmulsrv.c
    gcc -g -Wall -o udpmulclt.out udpmulclt.c

    最后测试截图如下

    很好玩,欢迎尝试.到这里基本上udp基础api 应该都了解了.从上面代码也许能看出来. 设计比较重要. 设计决定大思路.

    下次有机会 要么分享开源的网络库,要么分享数据库开发.

    后记

       错误是难免的,欢迎吐槽交流. ( ^_^ )/~~拜拜

         
    别董大(其一)
            高适
        千里黄云白日曛,
        北风吹雁雪纷纷。
        莫愁前路无知己,
        天下谁人不识君。
     
    (作者注: 别董大意思是 分别了我的朋友董大)

       

  • 相关阅读:
    多色图标字体
    css编写规则BEM
    css处理工具PostCss
    vue2.0点击其他任何地方隐藏dom
    vue2.0多页面开发
    Dijkstra算法(邻接矩阵存储)
    kmp算法c++代码实现
    最小生成树(prim算法,Kruskal算法)c++实现
    字符串匹配的KMP算法(转)
    筛选法求素数
  • 原文地址:https://www.cnblogs.com/life2refuel/p/5358072.html
Copyright © 2011-2022 走看看