C 基于UDP实现一个简易的聊天室
这是 2016 年那会在学习网络 socket api 构建一个简单教程, 挺有热度的. 现在 2024 年底, 刚好闲暇在家,
花了点时间对这个教程简单升级了下. 适用于初学者和计算机学生代码升级经验包, 用于了解 linux UDP 相关开发.
你想用这些案例提升功力之前, 最好你已经在网上搜索很多 UDP, TCP 科普的简单知识.
这里主要帮你从手上, 从代码层面帮忙拉一把. 拉到门内.
正式上手前, 会先带着写一个简单 demo 热热身, 用于了解 socket 相关的 sento / recvfrom api 代码实战开发. 后面再说
UDP 简单聊天室. 文中出现相关 api 可以多看 man 手册.
那从一个简单的 UDP client 和 server demo 开始. 这里是个大致 echo 服务设计 udp_client_demo 和 udp_server_demo
有点 low. 简单看看. 感兴趣的可以通过看代码, 自己私下画一画, 也是不错设计练习项目
udp client demo
#include <errno.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/time.h> #include <sys/socket.h> #define UDP_SERVER_PORT ((uint16_t)8888) // // udp client demo // // gcc -g -Wall -Wextra -O2 -o udp_client_demo udp_client_demo.c // int main(void) { // 创建 udp IPv4 client socket int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (fd == -1) { perror("main socket dgram"); exit(EXIT_FAILURE); } socklen_t al = sizeof (struct sockaddr_in); struct sockaddr_in ar = { .sin_family = AF_INET, // IPv4 address protocol .sin_port = htons(UDP_SERVER_PORT), }; char msg[BUFSIZ] = ":) 谁也不会喜欢工作狂 ~"; ssize_t msglen = strlen(msg); // 开始发送消息包到服务器, 默认目标地址是 INADDR_ANY (本地 local send) ssize_t len = sendto(fd, msg, msglen, 0, (struct sockaddr *)&ar, al); if (len < msglen) { perror("main sendto msg"); exit(EXIT_FAILURE); } // 开始接收服务器回过来的报文, demo client 里使用 阻塞模式 + 超时时间 方式 // 设置 socket recvfrom 超时时间 struct timeval timeout = { .tv_sec = 1, .tv_usec = 0 }; if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == -1) { perror("man setsockopt SO_RCVTIMEO"); exit(EXIT_FAILURE); } /* UDP is a stateless protocol, unlike TCP which is connection oriented. Your receiving code will not know whether or not the sender has closed its socket, it only knows whether or not there is data waiting to be read. According to the man page for recvfrom on Linux: If no messages are available at the socket, the receive calls wait for a message to arrive, unless the socket is nonblocking (see fcntl(2)) in which case the value -1 is returned and the external variable errno set to EAGAIN. This seems to be what is happening for you Edit: Note that "resource temporarily unavailable" and EAGAIN are the same error, one is just the user friendly descption vs the define name. Basically its just telling you that you are trying to read from the socket and there is no data to read */ len = recvfrom(fd, msg, sizeof msg - 1, 0, (struct sockaddr *)&ar, &al); if (len == -1) { // 简单规定首次 udp client 没有读取到数据, 默认直接成功退出 if (errno == EAGAIN || errno == EWOULDBLOCK) { exit(EXIT_SUCCESS); } perror("main recvfrom"); exit(EXIT_FAILURE); } /* RETURN VALUE inet_pton() returns 1 on success (network address was successfully converted). 0 is returned if src does not contain a character string representing a valid network address in the specified address family. If af does not contain a valid address family, -1 is returned and errno is set to EAFNOSUPPORT. */ char ip[INET_ADDRSTRLEN]; int res = inet_pton(ar.sin_family, ip, &ar.sin_addr); if (res < 0) { perror("main inet_pton"); exit(EXIT_FAILURE); } // UDP 进入这个分支很迷惑, 在这个简单业务里假定认为是 server close if (len == 0) { close(fd); printf("udp server [%s:%hu] close\n", ip, UDP_SERVER_PORT); exit(EXIT_SUCCESS); } // print udp server msg msg[len] = '\0'; printf("udp server [%s:%hu] msg: %s\n", ip, UDP_SERVER_PORT, msg); // 程序结束, 回收通信的 socket fd 文件描述符 close(fd); exit(EXIT_SUCCESS); }
udp server demo
#include <errno.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/time.h> #include <sys/socket.h> #define UDP_SERVER_PORT ((uint16_t)8888) // // udp server demo // // gcc -g -Wall -Wextra -O2 -o udp_server_demo udp_server_demo.c // int main(void) { // 创建 udp IPv4 client socket int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (fd == -1) { perror("main socket dgram"); exit(EXIT_FAILURE); } int opt = 1; // 用于对 TCP 套接字处于 TIME_WAIT 状态下的 socket, 才可以重复绑定使用 int res = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); if (res == -1) { perror("main setsockopt SO_REUSEADDR"); // socket fd IP 地址复用失败, 不影响下面的正常流程 } printf("udp server start [%d][0.0.0.0][%hu] ...\n", fd, UDP_SERVER_PORT); socklen_t al = sizeof (struct sockaddr_in); struct sockaddr_in ar = { .sin_family = AF_INET, // IPv4 address protocol .sin_port = htons(UDP_SERVER_PORT), }; // 服务器绑定地址 if (bind(fd, (struct sockaddr *)&ar, al) == -1) { perror("main bind INADDR_ANY"); exit(EXIT_FAILURE); } // 阻塞的接收不同客户端数据来回搞 char msg[BUFSIZ]; for (;;) { ssize_t msglen = recvfrom(fd, msg, sizeof msg - 1, 0, (struct sockaddr *)&ar, &al); if (msglen < 0) { fprintf(stderr, "man recvfrom msg len = %zd, %d:%s\n", msglen, errno, strerror(errno)); continue; } if (msglen == 0) { // UDP 进入这个分支很迷, 不同平台行为未定义, linux udp client send 0 size msg or close // 业务认为 client 退出, 不再做额外工作 continue; } char ip[INET_ADDRSTRLEN]; int res = inet_pton(ar.sin_family, ip, &ar.sin_addr); if (res < 0) { perror("main inet_pton"); continue; } // print udp cleint msg msg[msglen] = '\0'; printf("udp clent [%s:%hu] msg: %s\n", ip, ntohs(ar.sin_port), msg); // 回显继续发送给客户端 ssize_t len = sendto(fd, msg, msglen, 0, (struct sockaddr *)&ar, al); if (len < msglen) { fprintf(stderr, "man sendto msg len = %zd, %zd, %d:%s\n", msglen, len, errno, strerror(errno)); continue; } } // 程序结束, 回收通信的 socket fd 文件描述符 close(fd); exit(EXIT_SUCCESS); }
演示
将上面代码 敲几遍琢磨琢磨基本上 udp 一套 api 就会使用了. 后面进入正题设计聊天室代码.
那 UDP multiple 简单多人聊天室正式开始吧
客户端设计代码. 主要思路是子进程处理数据的输出, 父进程处理服务器数据的接收.
#include <time.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <stdint.h> #include <inttypes.h> #include <unistd.h> #include <sys/time.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // PRINTF fprintf 包装操作宏. time_t x64 8字节 window %lld, linux %ld #define PRINTF(stream, error, fmt, ...) \ fprintf(stream, "[%"PRId64"]["#stream"][%s:%s:%d][%d:%s]"fmt"\n", \ time(NULL), \ __FILE__, __func__, __LINE__, error, strerror(error), ##__VA_ARGS__) // // PERR - 打印错误信息 // EXIT - 打印错误信息, 并 exit // IF - 条件判断异常退出的辅助宏 // #define PERR(fmt, ...) \ PRINTF(stderr, errno, fmt, ##__VA_ARGS__) #define EXIT(fmt, ...) \ do { \ PERR(fmt, ##__VA_ARGS__); \ exit(EXIT_FAILURE); \ } while(0) #define IF(cond) \ if ((cond)) EXIT(#cond) /* 以太网帧在局域网中的 MTU 是 1500byte, 但是在非局域网环境, 如: Internet 下的时候, MTU 是各个路由器进行一个配置的. 所以, 通常路由器默认的 MTU 为 576 字节. 最好将 UDP 的数据长度控件在 548 字节 (576 - 8 UDP 头 - 20 IP 头) 以内 实际操作上, 为了适应网络环境, 一般一次报文不要超过 512 字节. */ #define INT_UMSG_NAME (31) #define INT_UMSG_TEXT (512 - INT_UMSG_NAME - 1) struct umsg { /* 业务交互协议约定 'l' : 客户端登录服务器, 并携带客户端名字 's' : 向服务器发送文本信息 'q' : 向服务器发送退出协议 */ char type; char name[INT_UMSG_NAME]; // 默认最后一个字符为 0 char text[INT_UMSG_TEXT]; // 文本信息, 这里简单, 默认带上 \0 }; static int recvfrom_umsg(int fd, struct sockaddr_in * sa, struct umsg * msg) { memset(msg, 0, sizeof(struct umsg)); socklen_t sl = sizeof(struct sockaddr_in); ssize_t len = recvfrom(fd, msg, sizeof(struct umsg), 0, (struct sockaddr *)sa, &sl); if (len < (ssize_t)sizeof(struct umsg)) { PERR("recvfrom len = %zd < sizeof msg", len); return -1; } msg->name[INT_UMSG_NAME-1] = msg->text[INT_UMSG_TEXT-1] = '\0'; switch(msg->type){ case 'l': printf("%s 登录了聊天室!\n", msg->name); break; case 's': printf("%s 说了: %s\n", msg->name, msg->text); break; case 'q': printf("%s 退出了聊天室!\n", msg->name); break; default : { // 未识别的异常报文, 程序直接退出 char ip[INET_ADDRSTRLEN] = {}; if (inet_pton(sa->sin_family, ip, &sa->sin_addr) < 0) { PERR("inet_pton error"); } PERR("msg is error! [%s:%hd] => [%c:%s:%s]", ip, ntohs(sa->sin_port), msg->type, msg->name, msg->text); return -1; } } return 0; } // 首次 client 构建 'n' 协议, 发送给服务器, 服务器应答 static int login(int fd, struct sockaddr_in * sa, struct umsg * msg) { // 真实工程开发, 需要给每种协议设置特定 request 和 response , 先读取 type 随后读取 data ssize_t len = sendto(fd, msg, sizeof(struct umsg), 0, (struct sockaddr *)sa, sizeof(struct sockaddr_in)); if (len < (ssize_t)sizeof(struct umsg)) { return -1; } // 等待服务器通知登录的应答信息 socklen_t timevalsz = (socklen_t)sizeof(struct timeval); struct timeval old = {}; if (getsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &old, &timevalsz) == -1) { PERR("getsockopt SO_RCVTIMEO error"); return -1; } // 设置 socket recvfrom 超时时间 struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 }; if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, timevalsz) == -1) { PERR("setsockopt SO_RCVTIMEO error"); return -1; } struct umsg newmsg = {}; int res = recvfrom_umsg(fd, sa, &newmsg); if (res < 0) { // 简单规定首次 udp client 没有读取到数据, 默认直接成功退出 if (errno == EAGAIN || errno == EWOULDBLOCK) { PERR("login udp multiple server 未应答, 当前程序主动退出中 ..."); return -2; } return -1; } // rollback 回滚之前 SO_RCVTIMEO if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &old, timevalsz) == -1) { PERR("setsockopt SO_RCVTIMEO rollback error"); return -1; } return 0; } // // udp client multiple project // udp 聊天室的客户端, 子进程发送信息, 父进程接受信息 // // gcc -g -Wall -Wextra -O2 -o udp_client_multiple udp_client_multiple.c // ./udp_client_multiple 0.0.0.0 8088 "阿 J" // ./udp_client_multiple 0.0.0.0 8088 abc // ./udp_client_multiple 0.0.0.0 8088 小王 int main(int argc, char * argv[]) { // argc = 4; // char * nargv[] = { "./udp_server_multiple", "0.0.0.0", "8088", "阿 J" }; // argv = nargv; // 先简单健壮性检查 if(argc != 4) { fprintf(stderr, "usage : %s [ip] [port] [name]\n", argv[0]); exit(EXIT_FAILURE); } int port = atoi(argv[2]); if (port < 1024 || port > 65535) { fprintf(stderr, "atoi port = %s is error!\n", argv[2]); exit(EXIT_FAILURE); } socklen_t sl = sizeof(struct sockaddr_in); struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(port), }; // 接着判断 ip 数据, inet_aton 默认这里用 ipv4 地址, return 0 is error IF(inet_aton(argv[1], &sa.sin_addr) == 0); // 创建 socket 连接 int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); IF(fd == -1); const ssize_t msgsz = sizeof (struct umsg); // 构建 'n' 协议, 初始 send name 协议 struct umsg msg = { .type = 'l' }; strncpy(msg.name, argv[3], INT_UMSG_NAME - 1); // 首次发送登录信息给 udp multiple server int res = login(fd, &sa, &msg); if (res < 0) { close(fd); exit(res == -1 ? EXIT_FAILURE : EXIT_SUCCESS); } // 这里也可以等待 udp multiple server 回复, 用于判断 udp multiple server 是否存活 // 忽略子进程退出处理防止成为僵尸进程 IF(signal(SIGCHLD, SIG_IGN) == SIG_ERR); // 开启一个进程, 子进程处理发送信息, 父进程接收信息 pid_t pid = fork(); IF(pid == -1); // pid == 0 is 子进程 if(pid == 0) { while (fgets(msg.text, INT_UMSG_TEXT, stdin)) { // 尝试去除最后一个 \n 字符 char * find = strchr(msg.text, '\n'); if(find) { *find = 0; } // "q" "quit" 表示退出 if(strcasecmp(msg.text, "q") == 0 || strcasecmp(msg.text, "quit") == 0) { // 构建 "q" 协议, quit 消息通知服务器 msg.type = 'q'; ssize_t len = sendto(fd, &msg, msgsz, 0, (struct sockaddr *)&sa, sl); IF(len < msgsz); break; } // 构建 's' 协议, 发送普通信息 msg.type = 's'; ssize_t len = sendto(fd, &msg, msgsz, 0, (struct sockaddr *)&sa, sl); IF(len < msgsz); } // 子进程正常退出, 捎带父进程一起退出 close(fd); kill(getppid(), SIGKILL); // 子进程正常退出 exit(EXIT_SUCCESS); } // 这里 pid == child pid 是父进程, 处理数据的读取 for (;;) { if (recvfrom_umsg(fd, &sa, &msg) == -1) { break; } } // 杀死并等待子进程退出 close(fd); kill(pid, SIGKILL); waitpid(pid, NULL, -1); exit(EXIT_SUCCESS); }
主要业务设计看 struct umsg 数据结构设计
/* 以太网帧在局域网中的 MTU 是 1500byte, 但是在非局域网环境, 如: Internet 下的时候, MTU 是各个路由器进行一个配置的. 所以, 通常路由器默认的 MTU 为 576 字节. 最好将 UDP 的数据长度控件在 548 字节 (576 - 8 UDP 头 - 20 IP 头) 以内 实际操作上, 为了适应网络环境, 一般一次报文不要超过 512 字节. */ #define INT_UMSG_NAME (31) #define INT_UMSG_TEXT (512 - INT_UMSG_NAME - 1) struct umsg { /* 业务交互协议约定 'l' : 客户端登录服务器, 并携带客户端名字 's' : 向服务器发送文本信息 'q' : 向服务器发送退出协议 */ char type; char name[INT_UMSG_NAME]; // 默认最后一个字符为 0 char text[INT_UMSG_TEXT]; // 文本信息, 这里简单, 默认带上 \0 };
udp 聊天室的服务器设计, 服务器会维护一个客户端链表. 有信息来就广播. 好简单吧. 就是这样. 正常的事都简单.
简单的往往容易突出美好的东西, 不过简单可不容易哦(⊙o⊙)?. 好了看 server 代码总设计和实现.
#include <time.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <stdint.h> #include <stdbool.h> #include <inttypes.h> #include <unistd.h> #include <sys/time.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // PRINTF fprintf 包装操作宏. time_t x64 8字节 window %lld, linux %ld #define PRINTF(stream, error, fmt, ...) \ fprintf(stream, "[%"PRId64"]["#stream"][%s:%s:%d][%d:%s]"fmt"\n", \ time(NULL), \ __FILE__, __func__, __LINE__, error, strerror(error), ##__VA_ARGS__) // // PERR - 打印错误信息 // EXIT - 打印错误信息, 并 exit // IF - 条件判断异常退出的辅助宏 // #define PERR(fmt, ...) \ PRINTF(stderr, errno, fmt, ##__VA_ARGS__) #define EXIT(fmt, ...) \ do { \ PERR(fmt, ##__VA_ARGS__); \ exit(EXIT_FAILURE); \ } while(0) #define IF(cond) \ if ((cond)) EXIT(#cond) /* 以太网帧在局域网中的 MTU 是 1500byte, 但是在非局域网环境, 如: Internet 下的时候, MTU 是各个路由器进行一个配置的. 所以, 通常路由器默认的 MTU 为 576 字节. 最好将 UDP 的数据长度控件在 548 字节 (576 - 8 UDP 头 - 20 IP 头) 以内 实际操作上, 为了适应网络环境, 一般一次报文不要超过 512 字节. */ #define INT_UMSG_NAME (31) #define INT_UMSG_TEXT (512 - INT_UMSG_NAME - 1) struct umsg { /* 业务交互协议约定 'l' : 客户端登录服务器, 并携带客户端名字 's' : 向服务器发送文本信息 'q' : 向服务器发送退出协议 */ char type; char name[INT_UMSG_NAME]; // 默认最后一个字符为 \0 char text[INT_UMSG_TEXT]; // 文本信息, 这里简单, 默认带上 \0 }; // 维护一个客户端链表信息, 记录登录信息 struct ucnode { struct sockaddr_in addr; struct ucnode * next; }; // 新建一个结点对象 static struct ucnode * new_ucnode(struct sockaddr_in * pa) { struct ucnode * node = calloc(sizeof(struct ucnode), 1); IF(node == NULL); node->addr = *pa; return node; } // 插入数据, 这里 head 默认头结点是当前服务器结点 static void insert_ucnode(struct ucnode * head, struct sockaddr_in * pa) { struct ucnode * node = new_ucnode(pa); node->next = head->next; head->next = node; } static int sendto_umsg(int fd, struct sockaddr_in * sa, struct umsg * msg) { ssize_t len = sendto(fd, msg, sizeof(struct umsg), 0, (struct sockaddr *)sa, sizeof(struct sockaddr_in)); if (len < (ssize_t)sizeof(struct umsg)) { char ip[INET_ADDRSTRLEN] = {}; if (inet_pton(sa->sin_family, ip, &sa->sin_addr) < 0) { PERR("inet_pton error"); } PERR("sendto msg error [%s:%hd] => [%c:%s:%s]", ip, ntohs(sa->sin_port), msg->type, msg->name, msg->text); return -1; } return 0; } // 这里是有用户登录处理 static void login_ucnode(struct ucnode * head, int fd, struct sockaddr_in * pa, struct umsg * msg) { insert_ucnode(head, pa); // 从头结点之后 为保存的以前的链表 while (head->next) { head = head->next; sendto_umsg(fd, &head->addr, msg); } } // 信息广播 static void broadcast_ucnode(struct ucnode * head, int fd, struct sockaddr_in * pa, struct umsg * msg) { bool find = false; // true 找到了 发送信息 udp client, 默认这个不需要广播 while(head->next) { head = head->next; if((find) || !(find = memcmp(pa, &head->addr, sizeof(struct sockaddr_in)) == 0)) { sendto_umsg(fd, &head->addr, msg); // 如果 send 失败, 又一套逻辑保证清理. 也可以 client 和 server 采用 ping pong 保活业务处理 } } } // 有人退出群聊 static void quit_ucnode(struct ucnode * head, int fd, struct sockaddr_in * pa, struct umsg * msg) { bool find = false; while(head->next) { struct ucnode * node = head->next; if((find) || !(find = memcmp(pa, &node->addr, sizeof(struct sockaddr_in)) == 0)) { sendto_umsg(fd, &node->addr, msg); head = node; } else { // 删除这个退出的用户 head->next = node->next; free(node); } } } // 销毁维护的对象池,没有往复杂的考虑了简单处理退出了 static void destroy_ucnode(struct ucnode * head) { while (head) { struct ucnode * node = head->next; free(head); head = node; } } // udp server multiple project // udp 聊天室的服务器, 接受 client 信息, 并广播 // // gcc -g -Wall -Wextra -O2 -o udp_server_multiple udp_server_multiple.c // ./udp_server_multiple 0.0.0.0 8088 int main(int argc, char * argv[]) { // argc = 3; // char * nargv[] = { "./udp_server_multiple", "0.0.0.0", "8088" }; // argv = nargv; // 这里简单检测 if(argc != 3) { fprintf(stderr, "usage : %s [ip] [port]\n", argv[0]); exit(EXIT_FAILURE); } int port = atoi(argv[2]); if (port < 1024 || port > 65535) { fprintf(stderr, "atoi port = %s is error!\n", argv[2]); exit(EXIT_FAILURE); } // 创建 socket 连接 int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); IF(fd == -1); int opt = 1; // 用于对 TCP 套接字处于 TIME_WAIT 状态下的 socket, 才可以重复绑定使用 int res = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); if (res == -1) { PERR("setsockopt SO_REUSEADDR"); // socket fd IP 地址复用失败, 不影响下面的正常流程 } socklen_t sl = sizeof(struct sockaddr_in); struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(port), }; // 接着判断 ip 数据, inet_aton 默认这里用 ipv4 地址, return 0 is error IF(inet_aton(argv[1], &sa.sin_addr) == 0); // 服务器绑定地址 IF (bind(fd, (struct sockaddr *)&sa, sl) == -1) ; // 开始处理聊天业务 struct ucnode * head = new_ucnode(&sa); for(;;) { struct umsg msg = {}; ssize_t len = recvfrom(fd, &msg, sizeof(struct umsg), 0, (struct sockaddr *)&sa, &sl); if (len < (ssize_t)sizeof(struct umsg)) { PERR("recvfrom len = %zd < sizeof msg", len); continue; } msg.name[INT_UMSG_NAME-1] = msg.text[INT_UMSG_TEXT-1] = 0; char ip[INET_ADDRSTRLEN]; int res = inet_pton(sa.sin_family, ip, &sa.sin_addr); if (res < 0) { perror("main inet_pton"); continue; } fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]\n", ip, ntohs(sa.sin_port), msg.type, msg.name, msg.text); // 开始判断处理 switch (msg.type) { case 'l': login_ucnode(head, fd, &sa, &msg); break; case 's': broadcast_ucnode(head, fd, &sa, &msg); break; case 'q': quit_ucnode(head, fd, &sa, &msg); break; default : // 未识别的异常报文, 程序把其踢走 fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", ip, ntohs(sa.sin_port), msg.type, msg.name, msg.text); quit_ucnode(head, fd, &sa, &msg); break; } } // 程序正常退出流程 close(fd); destroy_ucnode(head); exit(EXIT_SUCCESS); }
server 业务主要围绕的数据结构就两个 struct umsg 和 下面的 struct ucode 用于纪录所有 socket addr 用于信息的广播.
// 维护一个客户端链表信息, 记录登录信息 struct ucnode { struct sockaddr_in addr; struct ucnode * next; };
对于 struct ucnode * head list 结构维护, 头结点是服务器本身, 后面 head->next 全部挂的是 client, 来一个新客户端,
在链表头插入一下. 演示下最终效果
很好玩, 欢迎尝试.到这里基本上udp 基础 api 应该都了解了.从上面代码也许能看出来. 数据结构设计, 业务结构的设计比较重要. 设计决定大思路.
有心的同学或者老师可以尝试使用以上素材, 欢迎交流, 互相完善.