Unity 网络通信极限选型:TCP 和 UDP 到底该怎么分工,选错一次可能就是性能灾难?

TCP 可靠、UDP 快,所以"重要数据走 TCP,位置同步走 UDP"------这句话没错,但只说到了表面。

游戏网络真正要判断的并不是"哪个协议更快",而是:

这条消息如果晚到、丢失、重复,甚至被下一条消息覆盖,到底有没有关系?

MyFramework 同时维护 TCP 和 UDP 两条通信链路,业务层仍然只调用统一的 sendPacket()。这一篇就结合实际代码看看,两种协议到底应该怎么分工。

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

一、业务层根本不直接选择 TCP 或 UDP

发送一条协议时,业务代码通常只有:

bash 复制代码
CSChangePosition packet = CSChangePosition.get();
...
sendPacket(packet);

真正选择传输通道的是 NetManager

bash 复制代码
public void sendPacket(NetPacket packet)
{
	if (mNetPacketTypeManager.isUDPPacket(packet.getPacketType()))
	{
		Character myself = mCharacterManager.getMyself();
		if (myself != null)
		{
			mUDPServer.setToken(myself.getGUID());
		}
		mUDPServer.sendNetPacket(packet);
	}
	else
	{
		mTCPServer.sendNetPacket(packet);
	}
}

所以整个业务层仍然面对同一种:

bash 复制代码
NetPacket

只是协议注册阶段决定:

bash 复制代码
这个Packet
   ↓
TCP 还是 UDP

这样业务代码不需要到处写:

bash 复制代码
if (useUDP)
{
	...
}
else
{
	...
}

传输方式成为了协议本身的属性。

二、选 TCP 还是 UDP,先问一个问题

假设角色连续产生三次位置:

bash 复制代码
A:100,100
B:105,100
C:110,100

如果 A 丢了,但 C 已经到了:

bash 复制代码
A ×
B √
C √

还需要想办法把 A 找回来吗?

显然没必要。

因为:

bash 复制代码
最新位置 C
已经让旧位置 A 失去价值

这就是 UDP 最适合的一类数据:

旧数据会被新数据自然覆盖。

但换成:

bash 复制代码
购买物品
扣除金币
领取奖励
交易确认

情况完全不同。

"购买成功"这个消息不能因为下一条消息到了,就认为上一条可以不要。

所以第一条判断原则其实是:

新消息能不能代替旧消息?

能,才有资格考虑 UDP。

不能,优先 TCP。

三、为什么位置同步特别适合 UDP

项目中的 CSChangePosition 就是一条典型的位置同步协议:

bash 复制代码
public BIT_VECTOR2_INT mPixelPos = new();
public BIT_LONG mTimeStamp = new();
public BIT_USHORT mNormalMoveSpeed = new();
public BIT_USHORT mMoveTimeMS = new();
public BIT_USHORT mMoveSpeed = new();
public BIT_USHORT mMoveDelta = new();
public BIT_USHORT mMoveDeltaAdjusted = new();
public BIT_USHORT mMapID = new();
public BIT_BOOL mRun = new();

这里甚至包含:

bash 复制代码
位置
时间戳
移动速度
移动时间
地图ID
移动状态

也就是说它表达的是:

角色此时此刻是什么状态。

如果 100ms 前的位置包丢了,而新的位置已经到达,通常继续使用最新状态即可。

而且 CSChangePosition 自己还携带:

bash 复制代码
mTimeStamp

这类时间信息也给业务层判断数据新旧留下了空间。

UDP 不可靠,并不意味着协议设计就必须完全不知道"新旧"。

四、真正麻烦的是:TCP 和 UDP 之间根本没有顺序

这一点比"UDP 会丢包"更容易被忽略。

假设服务器依次产生:

bash 复制代码
TCP:玩家离开场景
UDP:玩家位置同步

或者位置包其实更早发出,只是在网络上晚到了。

客户端完全可能先收到:

bash 复制代码
玩家离开场景

然后又收到一个旧的:

bash 复制代码
玩家位置同步

因为:

TCP 只能保证 TCP 自己内部有序。UDP 和 TCP 两条链路之间,没有一个共同的消息顺序。

项目里的 SCOtherPlayerPosition 就专门处理了这个问题:

bash 复制代码
CharacterPlayer player = mOtherPlayerManager.getPlayerOther(mPlayerGUID[i]);
if (player == null)
{
	return;
}

// 位置同步使用UDP,其他消息使用TCP,
// 所以玩家离开场景后仍可能收到位置消息
if (!mMapSceneManager.getMap().isPlayerInScene(player))
{
	return;
}

这段代码非常有代表性。

TCP 已经告诉客户端:

bash 复制代码
这个玩家离开场景了

但 UDP 的旧位置包可能随后才飞过来。

所以不能看到位置包就无脑应用,而是必须再次确认:

bash 复制代码
这个玩家现在还在不在场景?

这其实就是混合 TCP + UDP 后必须面对的跨通道时序问题

五、UDP 不只是"更快",而是不会等待旧数据

TCP 还有一个非常关键的特性。

假设:

bash 复制代码
Packet A
Packet B
Packet C

底层属于同一条 TCP 字节流。

如果 A 对应的一部分数据丢失,即使 B、C 的网络数据已经到达,应用层也不能越过 A 直接拿到后面的字节。

TCP 必须先把缺失数据恢复出来。

对于:

bash 复制代码
交易
背包
任务
登录

这是我们想要的。

但对于高频位置:

bash 复制代码
A:1秒前的位置
B:500ms前的位置
C:现在的位置

如果 A 已经过时,却还阻塞着后面的 C,就没有那么划算了。

UDP 的思路完全不同:

bash 复制代码
A 丢了
↓
算了

B 到了
↓
处理

C 到了
↓
继续处理

它不保证可靠,却避免了旧数据拖住新数据。

所以实时同步真正看重的不只是:

bash 复制代码
延迟低

而是:

最新状态能够尽快到达业务层。

六、哪些消息绝对不能直接扔给 UDP

例如购买道具:

bash 复制代码
客户端:
购买商品1001

服务器:
扣100金币
增加1个商品

如果用当前这种没有 ACK、重传、去重机制的 UDP:

bash 复制代码
请求丢了
→ 到底要不要重发?

重发了
→ 第一条是不是其实已经到了?

到了两次
→ 会不会购买两次?

马上就会引入:

bash 复制代码
请求ID
ACK
超时
重传
去重
幂等
顺序控制

写到最后,本质上是在 UDP 上重新实现一套可靠协议。

而 TCP 已经帮你做了这些底层工作。

所以类似:

bash 复制代码
登录
购买
交易
邮件
任务提交
背包操作
装备操作
奖励领取

这种每一次状态变化都有业务意义的消息,更适合直接交给 TCP。

七、UDP 最大的问题不是丢包,而是你必须接受它丢包

MyFramework 当前 NetConnectUDPBit 没有实现:

bash 复制代码
ACK
丢包重传
乱序重排
重复包过滤

这其实反而让 UDP 的定位非常清晰。

它不是:

用 UDP 实现另外一套 TCP。

而是:

只把真正允许丢失的数据放进去。

所以判断一条消息是否适合 UDP,可以连续问四个问题:

bash 复制代码
丢一次能接受吗?

晚到能接受吗?

新消息能覆盖旧消息吗?

乱序到达时业务能识别或容忍吗?

只要其中某一个答案是:

bash 复制代码
不能

就应该非常谨慎。

八、不要为了省十几个字节强行用 UDP

从纯协议开销看,在最基础的 IPv4 情况下,不考虑额外选项:

bash 复制代码
TCP Header:至少20 Byte
UDP Header:8 Byte

UDP 确实更轻。

但游戏里选 UDP 的核心原因绝对不是:

bash 复制代码
每个包少12 Byte

而是它的传输语义更适合:

bash 复制代码
高频
实时
允许丢失
最新状态优先

如果为了省一点包头,把交易系统改成 UDP,然后自己增加:

bash 复制代码
ACK + Sequence + Retry + Timeout + Duplicate Check

最后可能不仅没省多少,复杂度还直接爆炸。

九、MyFramework 的 TCP / UDP 分工

最终可以总结成:

bash 复制代码
TCP
────────────────
可靠、有序
旧数据不能随便丢
适合业务状态变化

登录
背包
装备
交易
任务
邮件
购买
奖励


UDP
────────────────
允许丢包
最新数据可以覆盖旧数据
不等待历史状态

角色位置
高频实时状态
允许丢失的同步数据
UDP心跳

而 MyFramework 在上层把两套传输统一成了:

bash 复制代码
          NetPacket
             ↓
        sendPacket()
             ↓
     根据PacketType判断
          ↙       ↘
       TCP         UDP
        ↓           ↓
 NetConnectTCP  NetConnectUDP

业务层不需要关心 Socket。

真正需要想清楚的是:

这条消息到底属于"事件",还是"状态"。

一次购买、一次交易、一次领取奖励,是不能消失的事件

角色现在在哪里、朝向哪里、正在以什么速度移动,更接近可以不断覆盖的状态

这往往比"TCP 和 UDP 谁性能更高"更能决定一条游戏协议到底应该走哪条链路。

而一旦同时使用两条链路,还必须记住最后一个坑:

TCP 内部有顺序,UDP 内部不保证顺序,而 TCP 和 UDP 之间更不存在任何全局顺序。

这也是为什么真正成熟的游戏网络代码,绝不会只是简单地把 Send() 换成 SendTo()

相关推荐
SmalBox1 天前
【节点】[ParallaxOcclusionMapping节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[ParallaxMapping节点]原理解析与实际应用
unity3d·游戏开发·图形学
跟着群主去吃肉3 天前
Cinemachine的应用-第三人称视角
前端·unity3d·游戏开发
SmalBox3 天前
【节点】[Flipbook节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox4 天前
【节点】[SubgraphDropdownnode节点]原理解析与实际应用
unity3d·游戏开发·图形学
白金大神5 天前
DFA敏感词匹配算法与优化
unity3d
SmalBox5 天前
【节点】[Subgraph节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox6 天前
【节点】[Preview节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox7 天前
【节点】[LocalVariableRegister节点]原理解析与实际应用
unity3d·游戏开发·图形学