计算机网络_UDP和TCP

📅 2026/8/5 14:52:55
计算机网络_UDP和TCP
TCP传输控制协议和 UDP用户数据报协议是TCP/IP 协议簇中传输层的两大核心协议它们的设计理念完全相反 ——TCP 追求可靠传输UDP 追求高效传输这直接决定了它们在不同业务场景尤其是游戏开发中的选型差异。下面从协议本质、核心机制、详细区别、适用场景四个维度给你讲透两者的差异。一、协议本质与核心设计目标传输层的核心作用是“端到端” 的数据传输从一台主机的应用程序传到另一台主机的应用程序而 TCP 和 UDP 的目标完全不同TCP设计目标是“保证数据 100% 准确、有序地到达目的地”哪怕牺牲传输速度。可以类比成“挂号信”寄件前要确认收件人地址有效建立连接寄件后要等收件人签收确认应答丢件了要重寄重传机制全程可追踪。UDP设计目标是“以最快速度把数据发出去不保证是否到达”哪怕牺牲可靠性。可以类比成“明信片”写好地址直接投递不用确认收件人是否收到丢了也不补寄速度快、开销小。二、TCP 详解面向连接的可靠传输协议TCP 是面向连接、可靠、基于字节流的协议所有复杂机制都是为了实现 “可靠传输” 这个核心目标。1. 核心机制可靠传输的底层逻辑SYN (Synchronize Sequence Numbers)中文意思同步序列号作用发起连接。场景当一台计算机想要和另一台计算机建立连接时它发送的第一个包必须带有 SYN 标志。比喻相当于打电话时的“喂在吗”或者敲门声。它告诉对方“我想和你建立连接咱们来对一下暗号同步一下序列号方便后面传输数据。”ACK (Acknowledgment)中文意思确认作用回复/确认收到。场景TCP 是可靠传输所以每收到一个关键数据包都必须给对方回复一个 ACK告诉对方“我收到了”。比喻相当于对话中的“嗯”、“收到了”、“听清楚了”。如果发送方没收到 ACK它会认为你没听见就会重新发一遍。FIN (Finish)中文意思结束作用断开连接。场景当一方的数据发送完毕想要断开连接时会发送带有 FIN 标志的包。比喻相当于打电话结束时说的“我说完了”、“挂了啊”。1面向连接三次握手建立连接四次挥手断开连接TCP 通信前必须先建立双向连接通信后必须优雅断开连接这个过程是可靠传输的基础。三次握手建立连接客户端 → 服务端发送SYN包请求建立连接“你好B我想跟你建立连接。”服务端 → 客户端发送SYNACK包确认收到请求并同意建立连接“收到你的请求了ACK我也想跟你建立连接SYN。”客户端 → 服务端发送ACK包确认收到服务端的同意连接正式建立。“收到你的回复了那咱们连接建好了。”作用确保双方的收发能力都正常避免 “单边连接” 导致的数据丢失。四次挥手断开连接主动方 → 被动方发送FIN包请求断开连接“B我的数据发完了我想断开连接。”被动方 → 主动方发送ACK包确认收到断开请求“知道了ACK。但我这边可能还有点数据没发完你先等会儿。”被动方 → 主动方发送FIN包告知自己的数据已发完准备断开“好了 A我这边也都发完了现在可以彻底断开了。”主动方 → 被动方发送ACK包确认断开连接正式关闭。“好的拜拜。”作用确保双方的数据都传输完毕避免断开时丢包。2可靠传输序列号 确认应答 重传机制这是 TCP 最核心的 “可靠性保障”解决了丢包、重复、乱序三大问题序列号每个字节的数据都有唯一的序列号接收方可以通过序列号判断数据是否乱序、是否重复。确认应答ACK接收方收到数据后会发送一个ACK包告诉发送方 “我已经收到了序列号 X 之前的所有数据”。重传机制如果发送方在超时时间内没收到ACK就认为数据丢了会自动重传超时重传等待超时后重传通用场景快速重传如果收到 3 个重复的ACK直接重传不用等超时效率更高。3流量控制 拥塞控制避免网络过载流量控制通过滑动窗口实现接收方会告诉发送方 “我当前能接收的最大数据量”发送方不会超过这个量避免接收方处理不过来导致丢包。拥塞控制TCP 会根据网络状况动态调整发送速率比如慢启动、拥塞避免、快速恢复防止大量数据涌入网络导致拥堵。4基于字节流无数据边界可能出现粘包TCP 把数据当成连续的字节流传输没有 “数据包边界” 的概念。比如发送方分 3 次发A、B、C接收方可能一次收到ABC也可能分两次收到ABC—— 这就是粘包问题需要应用层自己定义 “分隔符” 或 “长度字段” 来解决。2. TCP 头部结构开销大TCP 头部至少 20 字节最多 60 字节可选字段包含序列号、确认号、窗口大小、标志位SYN/ACK/FIN 等等关键信息 —— 这些字段是实现可靠传输的基础但也带来了额外的网络开销。三、UDP 详解无连接的高效传输协议UDP 是无连接、不可靠、基于数据报的协议完全抛弃了 TCP 的复杂机制只保留最核心的 “发送数据” 功能。1. 核心特性高效传输的底层逻辑1无连接无需握手直接发送UDP 通信前不需要建立连接发送方不管接收方是否在线、是否准备好直接把数据封装成 “数据报” 发出去通信后也不需要断开连接。优点省去了三次握手 / 四次挥手的时间延迟极低缺点无法确认接收方是否存在数据丢了也不知道。2不可靠传输无确认、无重传、无排序UDP 没有序列号、ACK 包、重传机制数据一旦发出去就 “生死由命”数据可能丢包网络拥堵时路由器会优先丢弃 UDP 包数据可能乱序后发的包可能先到接收方不会自动排序数据可能重复同一个包可能被多次接收。3基于数据报有边界不会粘包UDP 把数据当成独立的数据包传输每个数据报都有明确的边界发送方发一个 100 字节的包接收方要么完整收到 100 字节要么完全收不到 ——不会出现粘包问题应用层处理更简单。4支持广播 / 组播一对多传输UDP 支持广播发给同一网段的所有主机和组播发给特定组的主机而 TCP 只能一对一传输。这一特性让 UDP 很适合游戏的 “房间广播”“服务器公告” 等场景。2. UDP 头部结构开销极小UDP 头部只有 8 字节只包含 4 个字段源端口、目的端口、数据长度、校验和 —— 没有任何额外控制字段传输效率极高。四、TCP 与 UDP 核心区别详细对比表对比维度TCPUDP关键差异解读连接性面向连接无连接TCP 必须三次握手建立连接UDP 直接发数据延迟更低可靠性可靠传输不可靠传输TCP 保证数据不丢、不重、有序UDP 不保证丢包不重传传输单位字节流无边界数据报有边界TCP 可能粘包需应用层处理UDP 不会粘包直接按包接收头部开销20~60 字节8 字节TCP 头部字段多开销大UDP 极简开销小确认机制确认应答ACK无确认应答TCP 收到数据必回 ACKUDP 发完不管重传机制超时重传 快速重传无重传机制TCP 丢包会重传UDP 丢包就丢了流量控制支持滑动窗口不支持TCP 会根据接收方能力调整速率UDP 无脑发拥塞控制支持慢启动等不支持TCP 会根据网络状况降速UDP 可能加剧网络拥堵通信方式一对一一对一 / 一对多广播 / 组播UDP 适合群发TCP 只能点对点延迟 / 效率延迟高效率低延迟低效率高TCP 机制复杂UDP 极简实时性更好五、适用场景结合游戏开发重点说明TCP 和 UDP 没有绝对的优劣只有场景适配性游戏开发中通常会两者混用。1. TCP 的适用场景数据不能丢准确性优先适合低频、关键、对实时性要求不高的数据传输比如游戏登录验证、账号密码传输、角色注册道具交易、金币充值、存档同步这些数据丢了会导致用户权益受损游戏公告、邮件推送不需要实时到达但必须准确到达。2. UDP 的适用场景实时性优先允许少量丢包适合高频、实时、对可靠性要求不高的数据传输比如多人游戏的角色移动、视角转动、技能释放哪怕丢 1-2 个包后续的新位置包也能补上不会影响体验实时语音聊天、游戏内的实时弹幕房间内的玩家状态广播比如 “玩家 A 进入房间”。3. 游戏开发的最佳实践TCP UDP 混用几乎所有大型多人在线游戏MMO都采用这种模式用UDP 处理实时性强的高频数据角色移动、战斗同步保证游戏流畅用TCP 处理关键的低频数据登录、交易、存档保证数据准确基于 UDP 自己实现 “轻量级可靠性”比如对关键的 UDP 包如技能释放指令手动加确认和重传机制兼顾实时性和可靠性虚幻引擎的NetDriver就有类似设计。六、案例UDP:Server:Linux//UdpServer.cc #include iostream #include string #include memory #include cerrno #include cstring #include unistd.h #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h const static uint16_t defaultport 8888; const static int defaultfd -1; const static int defaultsize 1024; enum { Usage_Err 1, Socket_Err, Bind_Err }; class UdpServer { public: UdpServer(uint16_t port defaultport) : _port(port), _sockfd(defaultfd) { } void Init() { // 1. 创建socket就是创建了文件细节 _sockfd socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd 0) { exit(Socket_Err); } // 2. 绑定指定网络信息 struct sockaddr_in local; bzero(local, sizeof(local)); // memset local.sin_family AF_INET; local.sin_port htons(_port); local.sin_addr.s_addr INADDR_ANY; // 1. 4字节IP 2. 变成网络序列 // 结构体填完设置到内核中了吗没有 int n ::bind(_sockfd, (struct sockaddr *)local, sizeof(local)); if (n ! 0) { exit(Bind_Err); } } void Start() { // 服务器永远不退出 char buffer[defaultsize]; for (;;) { struct sockaddr_in peer; socklen_t len sizeof(peer); // 不能乱写 ssize_t n recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len); if (n 0) { uint16_t clientport ntohs(peer.sin_port); std::string clientip inet_ntoa(peer.sin_addr); std::string prefix clientip : std::to_string(clientport); buffer[n] 0; std::cout prefix # buffer std::endl; std::string echo buffer; echo [udp server echo message]; sendto(_sockfd, echo.c_str(), echo.size(), 0, (struct sockaddr *)peer, len); } } } ~UdpServer() { } private: uint16_t _port; int _sockfd; }; void Usage(std::string proc) { std::cout Usage : \n\t proc local_port\n std::endl; } // ./udp_server 8888 int main(int argc, char *argv[]) { if (argc ! 2) { Usage(argv[0]); return Usage_Err; } uint16_t port std::stoi(argv[1]); std::unique_ptrUdpServer usvr std::make_uniqueUdpServer(port); usvr-Init(); usvr-Start(); return 0; }Client:Windows//UdpClient.cc #include iostream #include cstdio #include thread #include string #include cstdlib #include WinSock2.h #include Windows.h #pragma warning(disable : 4996) #pragma comment(lib, ws2_32.lib) std::string serverip ; // 填写你的云服务器ip uint16_t serverport 8888; // 填写你的云服务开放的端口号 int main() { WSADATA wsd; WSAStartup(MAKEWORD(2, 2), wsd); struct sockaddr_in server; memset(server, 0, sizeof(server)); server.sin_family AF_INET; server.sin_port htons(serverport); //? server.sin_addr.s_addr inet_addr(serverip.c_str()); SOCKET sockfd socket(AF_INET, SOCK_DGRAM, 0); if (sockfd SOCKET_ERROR) { std::cout socker error std::endl; return 1; } std::string message; char buffer[1024]; while (true) { std::cout Please Enter ; std::getline(std::cin, message); if(message.empty()) continue; sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)server, sizeof(server)); struct sockaddr_in temp; int len sizeof(temp); int s recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)temp, len); if (s 0) { buffer[s] 0; std::cout buffer std::endl; } } closesocket(sockfd); WSACleanup(); return 0; }TCP:Sever:Linux//TcpClient.cc #include winsock2.h #include iostream #include string #pragma warning(disable : 4996) #pragma comment(lib, ws2_32.lib) std::string serverip ; // 填写你的云服务器ip uint16_t serverport 8888; // 填写你的云服务开放的端口号 int main() { WSADATA wsaData; int result WSAStartup(MAKEWORD(2, 2), wsaData); if (result ! 0) { std::cerr WSAStartup failed: result std::endl; return 1; } SOCKET clientSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSocket INVALID_SOCKET) { std::cerr socket failed std::endl; WSACleanup(); return 1; } sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_port htons(serverport); // 替换为服务器端口 serverAddr.sin_addr.s_addr inet_addr(serverip.c_str()); // 替换为服务器IP地址 result connect(clientSocket, (SOCKADDR *)serverAddr, sizeof(serverAddr)); if (result SOCKET_ERROR) { std::cerr connect failed std::endl; closesocket(clientSocket); WSACleanup(); return 1; } while (true) { std::string message; std::cout Please Enter ; std::getline(std::cin, message); if(message.empty()) continue; send(clientSocket, message.c_str(), message.size(), 0); char buffer[1024] {0}; int bytesReceived recv(clientSocket, buffer, sizeof(buffer) - 1, 0); if (bytesReceived 0) { buffer[bytesReceived] \0; // 确保字符串以 null 结尾 std::cout Received from server: buffer std::endl; } else { std::cerr recv failed std::endl; } } closesocket(clientSocket); WSACleanup(); return 0; }Client:Windows//TcpServer.cc #include iostream #include string #include cerrno #include cstring #include cstdlib #include memory #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/wait.h #include unistd.h const static int default_backlog 6; enum { Usage_Err 1, Socket_Err, Bind_Err, Listen_Err }; #define CONV(addr_ptr) ((struct sockaddr *)addr_ptr) class TcpServer { public: TcpServer(uint16_t port) : _port(port), _isrunning(false) { } // 都是固定套路 void Init() { // 1. 创建socket, file fd, 本质是文件 _listensock socket(AF_INET, SOCK_STREAM, 0); if (_listensock 0) { exit(0); } int opt 1; setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, opt, sizeof(opt)); // 2. 填充本地网络信息并bind struct sockaddr_in local; memset(local, 0, sizeof(local)); local.sin_family AF_INET; local.sin_port htons(_port); local.sin_addr.s_addr htonl(INADDR_ANY); // 2.1 bind if (bind(_listensock, CONV(local), sizeof(local)) ! 0) { exit(Bind_Err); } // 3. 设置socket为监听状态tcp特有的 if (listen(_listensock, default_backlog) ! 0) { exit(Listen_Err); } } void ProcessConnection(int sockfd, struct sockaddr_in peer) { uint16_t clientport ntohs(peer.sin_port); std::string clientip inet_ntoa(peer.sin_addr); std::string prefix clientip : std::to_string(clientport); std::cout get a new connection, info is : prefix std::endl; while (true) { char inbuffer[1024]; ssize_t s ::read(sockfd, inbuffer, sizeof(inbuffer)-1); if(s 0) { inbuffer[s] 0; std::cout prefix # inbuffer std::endl; std::string echo inbuffer; echo [tcp server echo message]; write(sockfd, echo.c_str(), echo.size()); } else { std::cout prefix client quit std::endl; break; } } } void Start() { _isrunning true; while (_isrunning) { // 4. 获取连接 struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensock, CONV(peer), len); if (sockfd 0) { continue; } ProcessConnection(sockfd, peer); } } ~TcpServer() { } private: uint16_t _port; int _listensock; // TODO bool _isrunning; }; using namespace std; void Usage(std::string proc) { std::cout Usage : \n\t proc local_port\n std::endl; } // ./tcp_server 8888 int main(int argc, char *argv[]) { if (argc ! 2) { Usage(argv[0]); return Usage_Err; } uint16_t port stoi(argv[1]); std::unique_ptrTcpServer tsvr make_uniqueTcpServer(port); tsvr-Init(); tsvr-Start(); return 0; }