TCP与UDP的C语言聊天室

发布时间:2026/10/22 13:08:04
TCP与UDP的C语言聊天室 Github:https://github.com/Emy-Lxo/TCP_UDP_Chat_C.git1. 引言聊天室是理解网络编程非常好的实战项目。它既涉及 Socket 编程、多路复用也涉及数据转发和并发处理。本文分别使用 C 语言实现 TCP 聊天室和 UDP 聊天室覆盖服务器端与客户端完整代码并对比两种协议在开发上的差异。2. TCP 与 UDP 的核心差异在动手写代码之前先明确 TCP 和 UDP 的基本特点。TCP 需要三次握手建立连接服务器必须维护每个客户端的连接状态UDP 无需连接服务器只需要知道客户端地址即可。这也是两种聊天室实现方式产生差异的根本原因。3. TCP 聊天室实现3.1 服务器端代码这是部分代码/** * 服务器主流程 * NOTE 固定四步socket 创建 - bind 绑定端口 - listen 开始监听 - accept 等连接 * param 无 * return 0 正常结束-1 启动失败 */ int main(void) { int i_serverFd; /* 监听用的 socket */ int i_clientFd; /* 每个新连接的 socket */ int i_on 1; /* setsockopt 用 */ struct sockaddr_in st_serverAddr; /* 服务器地址本机 */ struct sockaddr_in st_clientAddr; /* 客户端地址accept 时填进来 */ socklen_t u32_addrLen; /* 地址结构长度 */ pthread_t st_tid; st_threadArg *p_stArg; /* AF_INET IPv4SOCK_STREAM TCP0 自动选协议 */ i_serverFd socket(AF_INET, SOCK_STREAM, 0); if (i_serverFd 0) { perror(socket); return -1; } /* 允许端口被立即重用避免重启程序时报 Address already in use */ if (setsockopt(i_serverFd, SOL_SOCKET, SO_REUSEADDR, i_on, sizeof(i_on)) 0) { perror(setsockopt); } /* 填写本机地址INADDR_ANY 表示监听所有网卡本机和局域网都能连进来 */ memset(st_serverAddr, 0, sizeof(st_serverAddr)); st_serverAddr.sin_family AF_INET; st_serverAddr.sin_addr.s_addr htonl(INADDR_ANY); /* htonl主机字节序 - 网络字节序 */ st_serverAddr.sin_port htons(PORT); /* htons端口同样要转成网络字节序 */ if (bind(i_serverFd, (struct sockaddr *)st_serverAddr, sizeof(st_serverAddr)) 0) { perror(bind); printf(绑定端口 %d 失败端口可能被别的程序占用\n, PORT); close(i_serverFd); return -1; } /* listen 的第二个参数是已完成连接但还没被 accept 的排队上限 */ if (listen(i_serverFd, 5) 0) { perror(listen); close(i_serverFd); return -1; } printf(TCP 聊天室服务器已启动监听 0.0.0.0:%d\n, PORT); printf(等待客户端加入按 CtrlC 关闭服务器\n); while (1) { /* accept 会阻塞直到有新客户端连接上来才返回 */ u32_addrLen sizeof(st_clientAddr); i_clientFd accept(i_serverFd, (struct sockaddr *)st_clientAddr, u32_addrLen); if (i_clientFd 0) { perror(accept); continue; /* 单个连接出错不影响整个服务器继续等下一个 */ } /* 为新客户端准备线程参数malloc 保证线程使用期间一直有效 */ p_stArg (st_threadArg *)malloc(sizeof(st_threadArg)); if (p_stArg NULL) { close(i_clientFd); continue; } p_stArg-i_fd i_clientFd; p_stArg-i_port ntohs(st_clientAddr.sin_port); inet_ntop(AF_INET, st_clientAddr.sin_addr, p_stArg-c_ip, sizeof(p_stArg-c_ip)); printf([系统] 新连接%s:%d\n, p_stArg-c_ip, p_stArg-i_port); /* 每个客户端单独开一个线程这样一个人发消息不会卡住其他人 */ if (pthread_create(st_tid, NULL, p_handleClient, p_stArg) ! 0) { perror(pthread_create); free(p_stArg); close(i_clientFd); } else { pthread_detach(st_tid); /* 分离线程线程结束后系统自动回收资源 */ } } close(i_serverFd); return 0; }3.2 客户端代码这是部分代码/** * 客户端主流程 * param argc 参数个数 * param argv argv[1] 可以是服务器 IP不填就用默认值 * return 0 正常退出-1 出错退出 */ int main(int argc, char *argv[]) { const char *p_cServerIp DEFAULT_SERVER_IP; struct sockaddr_in st_serverAddr; pthread_t st_tid; char c_nickname[NICK_SIZE]; char c_buf[BUF_SIZE]; char c_line[BUF_SIZE]; if (argc 1) { p_cServerIp argv[1]; /* 命令行给了 IP 就用命令行的 */ } /* ---------- 第 1 步读昵称 ---------- */ /* 用 fgets 比 scanf 安全能限制长度、能读空格、不残留垃圾字符 */ printf(请输入你的昵称); fflush(stdout); /* 刷新输出保证提示语先显示出来 */ if (fgets(c_nickname, sizeof(c_nickname), stdin) NULL) { return 0; } c_nickname[strcspn(c_nickname, \r\n)] \0; if (c_nickname[0] \0) { snprintf(c_nickname, sizeof(c_nickname), %s, 无名氏); } /* ---------- 第 2 步连接服务器 ---------- */ g_iSock socket(AF_INET, SOCK_STREAM, 0); if (g_iSock 0) { perror(socket); return -1; } memset(st_serverAddr, 0, sizeof(st_serverAddr)); st_serverAddr.sin_family AF_INET; st_serverAddr.sin_port htons(SERVER_PORT); if (inet_pton(AF_INET, p_cServerIp, st_serverAddr.sin_addr) 0) { printf(服务器 IP 格式不正确%s\n, p_cServerIp); close(g_iSock); return -1; } /* TCP 是先接通再说话连接不上就没法聊天 */ if (connect(g_iSock, (struct sockaddr *)st_serverAddr, sizeof(st_serverAddr)) 0) { perror(connect); printf(连接失败请检查1) 服务器程序是否已启动 2) IP/端口是否正确 3) 防火墙是否放行\n); close(g_iSock); return -1; } printf(已连接到服务器 %s:%d\n, p_cServerIp, SERVER_PORT); /* ---------- 第 3 步把昵称作为第一条消息发出去约定 ---------- */ snprintf(c_line, sizeof(c_line), %s\n, c_nickname); i_sendAll(g_iSock, c_line); /* ---------- 第 4 步开接收线程主线程负责读键盘发送 ---------- */ if (pthread_create(st_tid, NULL, p_recvThread, NULL) ! 0) { perror(pthread_create); close(g_iSock); return -1; } pthread_detach(st_tid); /* 分离线程结束后自动回收 */ printf(已进入聊天室输入消息回车发送输入 quit 退出。\n); while (1) { if (fgets(c_buf, sizeof(c_buf) - 1, stdin) NULL) { break; /* 输入结束CtrlD 或输入流关闭 */ } c_buf[strcspn(c_buf, \r\n)] \0; /* 去掉结尾换行 */ if (c_buf[0] \0) { continue; /* 空消息不发送 */ } snprintf(c_line, sizeof(c_line), %s\n, c_buf); /* 补上换行服务器按行读取 */ if (i_sendAll(g_iSock, c_line) 0) { break; /* 发送失败说明连接已断 */ } if (strcmp(c_buf, quit) 0) { break; /* 主动退出 */ } } close(g_iSock); printf(已退出聊天室\n); return 0; }4. UDP 聊天室实现4.1 服务器端代码这是部分代码/** * UDP 服务器主流程 * NOTE 只有三步socket - bind - 循环 recvfrom没有 listen / accept * param 无 * return 0 正常结束-1 启动失败 */ int main(void) { int i_serverFd; int i_recvLen; int i_idx; char c_buf[BUF_SIZE]; char c_msg[BUF_SIZE 32]; struct sockaddr_in st_serverAddr; struct sockaddr_in st_clientAddr; /* recvfrom 会把发信人地址填进来 */ socklen_t u32_addrLen; /* SOCK_DGRAM UDPTCP 用的是 SOCK_STREAM */ i_serverFd socket(AF_INET, SOCK_DGRAM, 0); if (i_serverFd 0) { perror(socket); return -1; } memset(st_serverAddr, 0, sizeof(st_serverAddr)); st_serverAddr.sin_family AF_INET; st_serverAddr.sin_addr.s_addr htonl(INADDR_ANY); /* 监听所有网卡 */ st_serverAddr.sin_port htons(PORT); if (bind(i_serverFd, (struct sockaddr *)st_serverAddr, sizeof(st_serverAddr)) 0) { perror(bind); printf(绑定端口 %d 失败端口可能被别的程序占用\n, PORT); close(i_serverFd); return -1; } printf(UDP 聊天室服务器已启动监听 0.0.0.0:%d\n, PORT); printf(等待客户端加入按 CtrlC 关闭服务器\n); while (1) { /* recvfrom收一个数据报同时得到寄信人地址UDP 最核心的函数 */ u32_addrLen sizeof(st_clientAddr); i_recvLen (int)recvfrom(i_serverFd, c_buf, sizeof(c_buf) - 1, 0, (struct sockaddr *)st_clientAddr, u32_addrLen); if (i_recvLen 0) { continue; /* 收到空报或出错忽略继续 */ } c_buf[i_recvLen] \0; c_buf[strcspn(c_buf, \r\n)] \0; /* 去掉结尾换行 */ if (c_buf[0] \0) { continue; } i_idx i_findClient(st_clientAddr); /* ---------- 情况 1没见过这个地址 —— 新用户上线第一条消息当昵称 ---------- */ if (i_idx 0) { i_idx i_addClient(st_clientAddr, c_buf); if (i_idx 0) { continue; /* 名单满了忽略 */ } printf([系统] %s 进入聊天室当前在线 %d 人\n, g_astClients[i_idx].c_nickname, g_iClientCount); snprintf(c_msg, sizeof(c_msg), *** %s 进入聊天室 ***\n, g_astClients[i_idx].c_nickname); v_broadcast(i_serverFd, c_msg, i_idx); /* 通知别人跳过自己 */ continue; } /* ---------- 情况 2已知用户发来 quit —— 退出 ---------- */ if (strcmp(c_buf, quit) 0) { printf([系统] %s 离开聊天室当前在线 %d 人\n, g_astClients[i_idx].c_nickname, g_iClientCount - 1); snprintf(c_msg, sizeof(c_msg), *** %s 离开聊天室 ***\n, g_astClients[i_idx].c_nickname); v_broadcast(i_serverFd, c_msg, i_idx); /* 先通知别人此时自己还在名单里 */ v_removeClient(i_idx); continue; } /* ---------- 情况 3普通消息 —— 转发给其他所有人 ---------- */ printf([%s] %s\n, g_astClients[i_idx].c_nickname, c_buf); snprintf(c_msg, sizeof(c_msg), [%s] %s\n, g_astClients[i_idx].c_nickname, c_buf); v_broadcast(i_serverFd, c_msg, i_idx); } close(i_serverFd); return 0; }4.2 客户端代码这是部分代码/** * 客户端主流程 * param argc 参数个数 * param argv argv[1] 可以是服务器 IP不填就用默认值 * return 0 正常退出 */ int main(int argc, char *argv[]) { const char *p_cServerIp DEFAULT_SERVER_IP; struct sockaddr_in st_serverAddr; pthread_t st_tid; char c_nickname[NICK_SIZE]; char c_buf[BUF_SIZE]; char c_line[BUF_SIZE]; if (argc 1) { p_cServerIp argv[1]; } /* ---------- 第 1 步读昵称 ---------- */ printf(请输入你的昵称); fflush(stdout); if (fgets(c_nickname, sizeof(c_nickname), stdin) NULL) { return 0; } c_nickname[strcspn(c_nickname, \r\n)] \0; if (c_nickname[0] \0) { snprintf(c_nickname, sizeof(c_nickname), %s, 无名氏); } /* ---------- 第 2 步创建 UDP socket ---------- * NOTE 没有 connect也不用 bind 端口 */ g_iSock socket(AF_INET, SOCK_DGRAM, 0); if (g_iSock 0) { perror(socket); return -1; } memset(st_serverAddr, 0, sizeof(st_serverAddr)); st_serverAddr.sin_family AF_INET; st_serverAddr.sin_port htons(SERVER_PORT); if (inet_pton(AF_INET, p_cServerIp, st_serverAddr.sin_addr) 0) { printf(服务器 IP 格式不正确%s\n, p_cServerIp); close(g_iSock); return -1; } /* ---------- 第 3 步先发昵称让服务器认识我们 ---------- * NOTE UDP 无连接服务器只有收到过我们的数据报 * 才知道我们的地址之后才能把别人的消息发给我们 */ snprintf(c_line, sizeof(c_line), %s\n, c_nickname); v_sendToServer(st_serverAddr, c_line); printf(已进入聊天室输入消息回车发送输入 quit 退出。\n); /* ---------- 第 4 步开接收线程主线程负责读键盘发送 ---------- */ if (pthread_create(st_tid, NULL, p_recvThread, NULL) ! 0) { perror(pthread_create); close(g_iSock); return -1; } pthread_detach(st_tid); while (1) { if (fgets(c_buf, sizeof(c_buf) - 1, stdin) NULL) { break; /* 输入结束 */ } c_buf[strcspn(c_buf, \r\n)] \0; if (c_buf[0] \0) { continue; } snprintf(c_line, sizeof(c_line), %s\n, c_buf); v_sendToServer(st_serverAddr, c_line); if (strcmp(c_buf, quit) 0) { break; /* 通知服务器自己走了然后退出 */ } } close(g_iSock); printf(已退出聊天室\n); return 0; }5. 编译与运行分别编译服务器和客户端gcc tcp_server.c -o tcp_server gcc tcp_client.c -o tcp_client gcc udp_server.c -o udp_server gcc udp_client.c -o udp_client先启动服务器再启动多个客户端即可看到多个客户端之间的消息转发。以下是现象TCPUDP6. 总结TCP 聊天室适合需要可靠消息、有序传输和有明确连接管理的场景UDP 聊天室更轻量适合实时性高、可容忍少量丢包的环境。通过这两个示例你可以进一步扩展出房间管理、用户昵称、心跳检测等功能。