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,我想跟你建立连接。"- 服务端 → 客户端:发送
SYN+ACK包,确认收到请求,并同意建立连接;
"收到你的请求了(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,也可能分两次收到AB+C------ 这就是粘包问题,需要应用层自己定义 "分隔符" 或 "长度字段" 来解决。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 核心区别详细对比表
对比维度 TCP UDP 关键差异解读 连接性 面向连接 无连接 TCP 必须三次握手建立连接,UDP 直接发数据,延迟更低 可靠性 可靠传输 不可靠传输 TCP 保证数据不丢、不重、有序;UDP 不保证,丢包不重传 传输单位 字节流(无边界) 数据报(有边界) TCP 可能粘包,需应用层处理;UDP 不会粘包,直接按包接收 头部开销 20~60 字节 8 字节 TCP 头部字段多,开销大;UDP 极简,开销小 确认机制 确认应答(ACK) 无确认应答 TCP 收到数据必回 ACK,UDP 发完不管 重传机制 超时重传 + 快速重传 无重传机制 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
cpp//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_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); usvr->Init(); usvr->Start(); return 0; }Client:Windows
cpp//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
cpp//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
cpp//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_ptr<TcpServer> tsvr = make_unique<TcpServer>(port); tsvr->Init(); tsvr->Start(); return 0; }