计算机网络_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 通信前必须先建立双向连接,通信后必须优雅断开连接,这个过程是可靠传输的基础。

  • 三次握手(建立连接)
    1. 客户端 → 服务端:发送 SYN 包,请求建立连接;
      "你好,B,我想跟你建立连接。"
    2. 服务端 → 客户端:发送 SYN+ACK 包,确认收到请求,并同意建立连接;
      "收到你的请求了(ACK),我也想跟你建立连接(SYN)。"
    3. 客户端 → 服务端:发送 ACK 包,确认收到服务端的同意,连接正式建立。
      "收到你的回复了,那咱们连接建好了。"
      作用:确保双方的收发能力都正常,避免 "单边连接" 导致的数据丢失。
  • 四次挥手(断开连接)
    1. 主动方 → 被动方:发送 FIN 包,请求断开连接;
      "B,我的数据发完了,我想断开连接。"
    2. 被动方 → 主动方:发送 ACK 包,确认收到断开请求;
      "知道了(ACK)。但我这边可能还有点数据没发完,你先等会儿。"
    3. 被动方 → 主动方:发送 FIN 包,告知自己的数据已发完,准备断开;
      "好了 A,我这边也都发完了,现在可以彻底断开了。"
    4. 主动方 → 被动方:发送 ACK 包,确认断开,连接正式关闭。
      "好的,拜拜。"
      作用:确保双方的数据都传输完毕,避免断开时丢包。
(2)可靠传输:序列号 + 确认应答 + 重传机制

这是 TCP 最核心的 "可靠性保障",解决了丢包、重复、乱序三大问题:

  • 序列号:每个字节的数据都有唯一的序列号,接收方可以通过序列号判断数据是否乱序、是否重复。
  • 确认应答(ACK) :接收方收到数据后,会发送一个 ACK 包,告诉发送方 "我已经收到了序列号 X 之前的所有数据"。
  • 重传机制 :如果发送方在超时时间内没收到 ACK,就认为数据丢了,会自动重传:
    • 超时重传:等待超时后重传(通用场景);
    • 快速重传 :如果收到 3 个重复的 ACK,直接重传(不用等超时,效率更高)。
(3)流量控制 + 拥塞控制:避免网络过载
  • 流量控制 :通过滑动窗口实现,接收方会告诉发送方 "我当前能接收的最大数据量",发送方不会超过这个量,避免接收方处理不过来导致丢包。
  • 拥塞控制:TCP 会根据网络状况动态调整发送速率(比如慢启动、拥塞避免、快速恢复),防止大量数据涌入网络导致拥堵。
(4)基于字节流:无数据边界,可能出现粘包

TCP 把数据当成连续的字节流传输,没有 "数据包边界" 的概念。

  • 比如发送方分 3 次发 ABC,接收方可能一次收到 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;
}
相关推荐
leiming62 小时前
手写Linux C UDP通信
linux·c语言·udp
技术性摸鱼2 小时前
计算机网络-知识点考点
计算机网络·系统架构
DARLING Zero two♡2 小时前
【计算机网络】简学深悟启示录:序列化&&反序列化
开发语言·计算机网络·php
阿拉伯柠檬3 小时前
网络层与网络层协议IP(一)
linux·网络·网络协议·tcp/ip·面试
天上飞的粉红小猪3 小时前
Socket编程TCP
服务器·网络·tcp/ip
枫叶丹44 小时前
【Qt开发】Qt系统(八)-> Qt UDP Socket
c语言·开发语言·c++·qt·udp
程序猿编码4 小时前
无状态TCP技术:DNS代理的轻量级实现逻辑与核心原理(C/C++代码实现)
c语言·网络·c++·tcp/ip·dns
2401_865854884 小时前
腾讯云的IP是原生IP吗?
tcp/ip·云计算·腾讯云
头发还没掉光光4 小时前
Linux网络之TCP协议
linux·运维·开发语言·网络·网络协议·tcp/ip