本文主要是介绍闲聊:最近想要做一个用在网络游戏领域的网络控制协议小玩具,大体聊聊构思的想法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
这个控制协议跟 “猛禽 UDP/IP Aggligator 宽频聚合器”,是不同的,因为预期该网络控制协议,预计侧重在网络游戏加速这块。
所以它与多数网络控制协议,会有一定的不同。
但要先分析具体的目标场景,基本现代所有的 UDP/IP 应用层网络协议,都具备丢包重传控制,当然有一些是不在乎丢包的。
所以,一般来说保证 UDP/IP 不丢包,是没有太大意义的,因为这只会出现以下两种情况:
1、UDP/IP 网络应用,可以丢包,它们会忽略这类问题,因为不重要。
2、UDP/IP 网络应用,自己带了,可靠CC(类似 KCP/TCP/QUIC 的控制协议)。
那么类似网络应用产品(有):
1、灵境奇谈 (封灵笔记)
2、CS:GO (反恐精英:全球攻势/cs2)
3、Warframe(星际战甲)
回到正题,既然知晓了问题的所在,那么就来开始按照不同的应用场景来调优,每个不同的网络应用调优方法是不同的。
但,我们通常预期目标都是为了减少网络抖动、只要产生丢包,那么网络抖动就会非常严重,而且在现代网络之中,因为路由跃点之间的不同,客户端到服务器之间的网络路由是随时波动的,即便路由没有变动,也会因为路由本身的QOS管理策略,导致帧的先后顺序发生改变(即乱序问题)产生抖动问题,因为若客户端需要排序,那么在收到来自未来帧时,是先缓存在接收方队列之中,等待确认帧(包)的到达,在这个空窗期就会产生抖动问题。
所以对于游戏加速,为了质量最好,会有两个以下解决方案:
1、对于UDP/IP带控制协议的网络游戏
我们可以采用,当收到游戏UDP数据包之后立即发出到服务器,并且为每个数据包标记帧序号,接收方,可以根据在1 ~ 5个毫秒内跳过的帧号,来评估网络的丢包层度。
注意:是采用旁路观察的办法,即控制协议来决定,发送方应该发送多少个冗余包,即:当收到客户端A包之后,发出几个包到服务器,如果丢失了一个包、由于冗余多发,虽然占用了更大的带宽,但延迟、抖动这块是基本被抹平了。
比如:
我们可以通过测算出来的每个帧需要发送几次,可以评估为:10个包丢一个包,则冗余发送一个包,5个包丢一个包,则冗余发送两个包,虽然这可能浪费更多的网络带宽。
网络之中5个包丢一个包,放大到3倍,产生一个包都无法到达的可能性几乎是没有的,在保证RTT、抖动不变大的情况,这是最佳解决方案,即通过冗余传送来保证。
但接收方一般拿需要确保不会出现重复帧转发的问题,即可:接收方收到已经转发过的帧不能再次转发,但为了可靠性也可以再次转发过去,让目的服务器自己处理它,但前提是目的是服务器允许这样的行为,这个要靠人们自己去测算。
网络游戏加速一般都是定向分析,定向研究加速解决方案的。
另外一种是做一个带ARQ(自动请求重传)的控制协议,这种像TCP/IP流控协议的做法就没有那么好了。
我们可以如此这般设计并处理,但它仍旧会产生一个抖动问题。
1、帧带上序号跟确认号。
2、接收方收到帧后,如果是当前确认号就确认并且交付
如果序号小于确认号,只要不是序号回绕问题,就丢弃帧。
如果序号大于确认号,就预先缓存在接收方队列之中。
当然如果缓存的数据包,已经占用了窗口大小的 50% 以上,就立即发出ACK+NAK包
另外为了提高效率,当连续收到 “2 ~ 3” 个来自未来的包(跳过),那么可以立即发出ACK+NAK 包,但建议是放在跟下面 Delay 一毫秒后在处理。(可以理解为:快速重传机制)
否则检查设定一个毫秒后ACK的计时器句柄,在一个毫秒之后检查,因为这一个毫秒过后可能网卡会达到无数个包,此时在判断那些包已经收到,那些包被跳过,在发出ACK+NAK数据包告诉服务器,可以一定减少昂贵的带宽资源浪费。
接收方收到ACK+NAK包以后,更新发送方的窗口大小,并且删除自己的已经缓存的数据包(尽快释放内存资源占用)、并且NAK上面指示的没有收到包重新发送出去。
注意:NAK为否定应答(即接收方发出没有收到的帧到发送方),对的网络游戏加速,比较适合按照帧来确认,帧填充的方式来传输,而不是流式传输。
重传时间(测算):默认情况下单位为1秒,在第一个测量出来的RTT(往返时间)这个可以从链接握手时确定。
即:客户端发送链接建立的帧、到服务器应答客户端应答帧的第一个RTT时间作为SRTT、RTT_VAL(RTT/2)初始值。
关于重传时间的时间可以采用下面的公式,TCP/IP(但需要魔改)
R = RTT
SRTT = 平滑RTT时间
RTTVAL = RTT因子
第一个RTT测量:
K = 4(常数)
SRTT = R
RTTVAR = R/2
RTO = SRTT + max(G,K*RTTVAR)
第二个RTT测量:(及之后)
注意:它需要通过 “NPU/SSE” 浮点数来运算
alpha=1/8
beta=1/4
RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R|
SRTT = (1 - alpha) * SRTT + alpha * R
RTO = SRTT + max (G, K*RTTVAR)
补充:
G为时间精度常数,在内核之中它被设定为200毫秒。
Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl (pracucci.com)
#define TCP_RTO_MAX ((unsigned)(120*HZ))
#define TCP_RTO_MIN ((unsigned)(HZ/5))
下述为KCP控制协议算法的RTO重传时间计算公式:
它与上面的公式区别不大,只是对系数微调的区别,即:KCP约为TCP/IP控制协议重传时间除以1.25~1.5倍左右。
static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt)
{IINT32 rto = 0;if (kcp->rx_srtt == 0) {kcp->rx_srtt = rtt;kcp->rx_rttval = rtt / 2;} else {long delta = rtt - kcp->rx_srtt;if (delta < 0) delta = -delta;kcp->rx_rttval = (3 * kcp->rx_rttval + delta) / 4;kcp->rx_srtt = (7 * kcp->rx_srtt + rtt) / 8;if (kcp->rx_srtt < 1) kcp->rx_srtt = 1;}rto = kcp->rx_srtt + _imax_(kcp->interval, 4 * kcp->rx_rttval);kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX);
}
但同时,需要设置一个最小RTO时间的边界值(临界值),这个值根据实际生产需要,可以设置为40、100毫秒即可,但对于游戏加速这种场景设置为40 ~ 60毫秒,一般会比较合理,RTO不能太小,否则会浪费大量带宽的,我们应该通过快速重传的方式来处理,而不是依赖于设计的控制协议RTO自动重发来处理。
关于发送方,因为根据接收方传递过来的窗口大小来发送数据,为了提高效率:应该是对面接收方有足够接受能力收到足够大的字节数时,一次性把发送队列之中带传输的数据包输出过去,在根据接收方投递的ACK/NAK来实时更新窗口大小,以便评估是否需要继续发送数据到接收方。
但需要注意一点:接收方跟发送方之间,建议采用 “背负式确认” 的方式来处理,这可以减少没必要单独的 ACK 报文发出,节约两端的网络带宽资源,但这可能会带来一定对于网络控制协议栈编程的复杂度。
RTO 值得计算用KCP修改后的系数会比较好一些,当然人们仍旧可以大胆得重新调整系数,无外乎是牺牲更多带宽,还是更小网络带宽得问题,另外也可以选用 BBRplus(BBR+)得调效模式,尽量避免提高RTO重传时间,以避免网络吞吐效率的降低,但这会增加网络拥塞层度的问题。
补充:
简单说一说SWS问题(糊涂窗口综合症问题)
在控制协议之中,我们大家都是通过窗口来评估对方的接收能力,如果对方窗口堆满,我们就不能发送。
所以对方只要收取且交付数据就需要更新窗口大小,但不可能为了更新窗口大小,每次都去立即发出ACK报文。
我在上篇文中没有着重的探讨这个问题,而是说推迟确认(延迟1毫秒)都是为了解决,类似如这个SWS糊涂窗口问题。
但仍可以通过算法评估解决该问题的ACK发出时机。
可以窗口边缘滑动了:
1/4缓冲区 or 4个MSS大小
来评估是否立即发出ACK报文,迫使发送方更新接收方窗口大小,否则应在下个正常发出帧数据背负式确认。
这篇关于闲聊:最近想要做一个用在网络游戏领域的网络控制协议小玩具,大体聊聊构思的想法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!