C 基于UDP实现一个简易的聊天室
2016-05-03 10:03
609 查看
引言
本文是围绕Linux udp api 构建一个简易的多人聊天室.重点看思路,帮助我们加深
对udp开发中一些api了解.相对而言udp socket开发相比tcp socket开发注意的细节要少很多.
但是水也很深. 本文就当是一个demo整合帮助开发者回顾和继续了解 linux udp开发的基本流程.
首先我们来看看 linux udp 和 tcp的异同.
参照
linux udp api简介 /article/9255019.html
tcp 和udp区别 /article/5104845.html
这里简单引述一下 udp相比tcp 用到的两个api . recvfrom()/sendto() 具体细节如下
上面就是两个函数的大致用法. 具体可以查看linux api帮助手册. 最好就用 man sendto / man recvfrom 把那一系列函数都看看.
现在很多文章都是转载,但是找不见转载的地址, 下面会举一个简易的UDP回显服务器的demo加深理解.
前言
首先看设计图
有点low. 简单看看吧. 那我们先看 客户端代码 udpclt.c 代码
编译是
udp 服务器 udpsrv.c
编译是
后面运行结果如下 udp服务器如下 (Ctrl + C 退出)
udp 客户端如下 (Ctrl + D 结束输入)
到这里将上面代码 敲一遍基本上udp 一套api就会使用了. 后面进入正题设计聊天室代码.
正文
首先看客户端设计代码. 主要思路是子进程处理数据的输出, 父进程处理服务器数据的接收. 具体设计如下(画的图有点low就不画了.../(ㄒoㄒ)/~~)
udpmulclt.c
这里主要需要注意的是
传输和接收的数据格式, type表示协议或行为. 我这里细心了处理 name, text最后一个字符必须是 '\0'. 其它都是业务代码.再扯一点
等价于
也是一个C开发中技巧吧. 再扯一点linux上提供 bzero函数, 但是window上没有. 写了个通用的如下
可以试试吧毕竟跨平台....
好了那我们说 udp 聊天室的服务器设计思路. 就是服务器会维护一个客户端链表. 有信息来就广播. 好简单吧.就是这样.正常的事都简单.
简单的是美的. 好了看代码总设计和实现. udpmulsrv.c
这里主要围绕的结构就是
注册添加登录广播退出等.这里再扯一下. 关于C static开发技巧. C中有一种 *.h 开发模式, 全部采用static 内嵌代码段. 这样
可以省略*.c 文件. 小巧的封装可以使用. 继续扯一点. 开发也写C++,虽然鄙视. C++ 中有个 *.hpp文件. 比较好. 它表达的意思
是这个代码是开源的. 全部采用充血模型. 类中代码都放在类中实现.非常值得提倡. 这也是学boost的时候学到的. 很实在.
好了说代码吧. 也比较随大流. 看看也都明白了. 简单分析一处吧
因为我采用的头查法. 那就除了刚插入的头的下一个结点都需要发送登录信息. 比较精巧.
好看编译命令
最后测试截图如下
很好玩,欢迎尝试.到这里基本上udp基础api 应该都了解了.从上面代码也许能看出来. 设计比较重要. 设计决定大思路.
下次有机会 要么分享开源的网络库,要么分享数据库开发.
本文是围绕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简介 /article/9255019.html
tcp 和udp区别 /article/5104845.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 <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> // 测试端口和网络地址 #define _INT_PORT (8088) #define _INT_BUF 1024 // udp 服务器主函数 int main(int argc, char* argv[]) { int sd, len; struct sockaddr_in addr = { AF_INET }; socklen_t alen = sizeof addr; char msg[_INT_BUF]; //创建服务器socket 地址,客户端给它发送信息 if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) { perror("main socket "); exit(sd); } // 这里简单输出连接信息 printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT); //拼接对方地址 addr.sin_port = htons(_INT_PORT); addr.sin_addr.s_addr = INADDR_ANY; if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){ perror("main bind "); exit(-1); } // 循环处理消息读取发送到客户端 while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){ msg[len] = '\0'; printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), msg); //这里发送信息过去, 也可以事先connect这里就不绑定了 sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen); } close(sd); puts("udp server end ------------------------------<"); return 0; }
编译是
gcc -g -Wall -o udpclt.out udpclt.c
udp 服务器 udpsrv.c
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> // 测试端口和网络地址 #define _INT_PORT (8088) #define _INT_BUF 1024 // udp 服务器主函数 int main(int argc, char* argv[]) { int sd, len; struct sockaddr_in addr = { AF_INET }; socklen_t alen = sizeof addr; char msg[_INT_BUF]; //创建服务器socket 地址,客户端给它发送信息 if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) { perror("main socket "); exit(sd); } // 这里简单输出连接信息 printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT); //拼接对方地址 addr.sin_port = htons(_INT_PORT); addr.sin_addr.s_addr = INADDR_ANY; if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){ perror("main bind "); exit(-1); } // 循环处理消息读取发送到客户端 while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){ msg[len] = '\0'; printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), msg); //这里发送信息过去, 也可以事先connect这里就不绑定了 sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen); } close(sd); puts("udp server end ------------------------------<"); return 0; }
编译是
gcc -g -Wall -o udpsrv.out udpsrv.c
后面运行结果如下 udp服务器如下 (Ctrl + C 退出)
udp 客户端如下 (Ctrl + D 结束输入)
到这里将上面代码 敲一遍基本上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 应该都了解了.从上面代码也许能看出来. 设计比较重要. 设计决定大思路.
下次有机会 要么分享开源的网络库,要么分享数据库开发.
相关文章推荐
- xpath 语法
- 页面跳转
- php调用接口
- 292. Nim Game leetcode
- 入门级基本SQL语句学习(二)
- HttpClient抓取网页内容简单介绍
- GlobalSign 域名型 SSL 证书
- 乌云http://drops.wooyun.org/
- 找出一组数中指出现一次的数2
- Android常用第三方支付
- css笔记——移动端
- Android使用Application总结
- 制作动画或小游戏——CreateJS基础类(一)
- Android development tools line_endings hacking
- Java 简单的BFS爬虫
- [Java视频笔记]day14
- 跨域的三种方法总结:代理,JSONP,以及XHR2
- python调用siebel的webservice(2种方法)
- 百度定位
- [剑指Offer]把数组排成最小的数