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] = '\0'; printf("[%s:%hd] -> %s\n", 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] ...\n", 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] = '\0'; printf("[%s:%hd] -> %s\n", 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> // 名字长度包含'\0' #define _INT_NAME (64) // 报文最大长度,包含'\0' #define _INT_TEXT (512) //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏 #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __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]\n", 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\n") == 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] = '\0'; switch(msg.type){ case '1':printf("%s 登录了聊天室!\n", msg.name);break; case '2':printf("%s 说了: %s\n", msg.name, msg.text);break; case '3':printf("%s 退出了聊天室!\n", msg.name);break; default://未识别的异常报文,程序直接退出 fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", 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最后一个字符必须是 '\0'. 其它都是业务代码.再扯一点
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> // 名字长度包含'\0' #define _INT_NAME (64) // 报文最大长度,包含'\0' #define _INT_TEXT (512) //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏 #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __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]\n", 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] = '\0'; fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]\n", 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]\n", 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 应该都了解了.从上面代码也许能看出来. 设计比较重要. 设计决定大思路.
下次有机会 要么分享开源的网络库,要么分享数据库开发.
后记
错误是难免的,欢迎吐槽交流. ( ^_^ )/~~拜拜