服务器:公网服务器
客户端:外网手游客户端
玩家侧现象:大批处于同一局域网内的内侧玩家连接服务器超时,但是切换成 4G 之后连接顺畅。
服务端现象:netstat -s"
显示 “passive connections rejected because of time stamp” 数量增长快速,关闭 tcp_tw_recycle
或 tcp_timestamps
恢复正常,玩家侧现象消失。
tcp_tw_recycle
依赖于 tcp_timestamps
, 前者默认关闭,后者默认开启。如果二者都开启,当服务器收到 FIN 包时,这个 FIN 包中的时间戳值会按 IP 缓存起来(缓存 TCP_PAWS_MSL
, 即 60s),缓存时间内,当同一 IP 的 SYN 包时间戳小于该值,则直接丢弃。代码分析如下:
tcp_v4_conn_request()
/* VJ's idea. We save last timestamp seen
* from the destination in peer table, when entering
* state TIME-WAIT, and check against it before
* accepting new connection request.
*
* If "isn" is not zero, this request hit alive
* timewait bucket, so that all the necessary checks
* are made in the function processing timewait state.
*/
if (tmp_opt.saw_tstamp &&
tcp_death_row.sysctl_tw_recycle &&
(dst = inet_csk_route_req(sk, req)) != NULL &&
(peer = rt_get_peer((struct rtable *)dst)) != NULL &&
peer->v4daddr == saddr) {
if ((u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL &&
(s32)(peer->tcp_ts - req->ts_recent) >
TCP_PAWS_WINDOW) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
goto drop_and_release;
}
}
由于 peer->tcp_ts_stamp
记录的是之前收到 FIN 包时服务器的时间戳,peer->tcp_ts
记录的是 FIN 包中的时间戳,req->ts_recent
表示当前 SYN 包中的时间戳,因此 (u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL
表示缓存还未过期,(s32)(peer->tcp_ts - req->ts_recent) > TCP_PAWS_WINDOW
表示当前 SYN 包中的时间戳比之前 FIN 包中的时间戳还小 1s, 因此认为当前 SYN 包时之前重传的,直接丢弃。
req - struct request_sock
peer- struct inet_peer, inet_getpeer()
get_seconds() - 获取当前服务器时间
TCP_PAWS_MSL
#define TCP_PAWS_MSL 60 /* Per-host timestamps are invalidated
* after this time. It should be equal
* (or greater than) TCP_TIMEWAIT_LEN
* to provide reliability equal to one
* provided by timewait state.
TCP_PAWS_WINDOW
#define TCP_PAWS_WINDOW 1 /* Replay window for per-host
* timestamps. It must be less than
* minimal timewait lifetime.
*/
tcp_v4_tw_remember_stamp()
int tcp_v4_tw_remember_stamp(struct inet_timewait_sock *tw)
{
struct inet_peer *peer = inet_getpeer(tw->tw_daddr, 1);
if (peer) {
const struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
if ((s32)(peer->tcp_ts - tcptw->tw_ts_recent) <= 0 ||
((u32)get_seconds() - peer->tcp_ts_stamp > TCP_PAWS_MSL &&
peer->tcp_ts_stamp <= (u32)tcptw->tw_ts_recent_stamp)) {
peer->tcp_ts_stamp = (u32)tcptw->tw_ts_recent_stamp;
peer->tcp_ts = tcptw->tw_ts_recent;
}
inet_putpeer(peer);
return 1;
}
return 0;
}
peer->tcp_ts_stamp
同 tcptw->tw_ts_recent_stamp
, peer->tcp_ts
同 tcptw->tw_ts_recent
, 见 tcp_timewait_state_process()
.
tcptw - struct tcp_timewait_sock, struct inet_timewait_sock
tcp_timewait_state_process()
/* FIN arrived, enter true time-wait state. */
tw->tw_substate = TCP_TIME_WAIT;
tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
if (tmp_opt.saw_tstamp) {
tcptw->tw_ts_recent_stamp = get_seconds();
tcptw->tw_ts_recent = tmp_opt.rcv_tsval;
}
/* I am shamed, but failed to make it more elegant.
* Yes, it is direct reference to IP, which is impossible
* to generalize to IPv6. Taking into account that IPv6
* do not understand recycling in any case, it not
* a big problem in practice. --ANK */
if (tw->tw_family == AF_INET &&
tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp &&
tcp_v4_tw_remember_stamp(tw))
inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
TCP_TIMEWAIT_LEN);
else
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
TCP_TIMEWAIT_LEN);
return TCP_TW_ACK;
从此处可以看出,tcptw->tw_ts_recent_stamp
记录的是收到 FIN 包时服务器当前时间,而 tcptw->tw_ts_recent
记录的是 FIN 包中对端的时间戳。
由于大量内侧玩家位于同一 NAT 网络中,所有玩家的 IP 被转换成同一出口 IP, 但时间戳却保留,因此到达服务器的包中时间戳顺序跟到达顺序是不一致的,当同时开启 tcp_tw_recycle
和 tcp_timestamps
时,一个玩家断开连接可能导致其他玩家的 SYN 包被丢弃掉。
tcp_timestamp
建议开启。tcp_timestamp
是 RFC1323 定义的优化选项,主要用于 TCP 连接中 RTT(Round Trip Time) 的计算,开启 tcp_timestamp
有利于系统计算更加准确的 RTT,也就有利于 TCP 性能的提升。
tcp_tw_recycle
建议关闭。
多数系统的默认设置也是如此。
一是现网中很少出现批量用户处于同一 NAT 网络中且包被 NAT 网关转发之后顺序错乱,即使出现数量也很少;
二是有些客户端系统并没有使用时间戳,例如 XP 和多数 Windows 7.