> 文章列表 > 游戏服务器开发指南(一):设置合适的Socket选项

游戏服务器开发指南(一):设置合适的Socket选项

游戏服务器开发指南(一):设置合适的Socket选项

前言

上周写完系列序言,得到不少读者朋友的关注,这也给了我额外的动力。写东西就是这样,都希望获得更多的关注,如果写出来没什么人看,那就无异于闭门造车、自娱自乐。欢迎朋友们在文后留言,我也会根据大家的反馈适时调整写作的内容和方式。

我在这里整理了一个系列目录游戏服务器开发指南(目录),用于分门别类存放文章的链接地址,方便读者快速索引。目录按不同的主题组织,如网络通信、数据存储等,每个主题会包含若干篇文章。个人计划是平均每周写一篇,写作基于平时工作中产生的灵感,因此不一定会按照主题的排列顺序来。

本周先上第一道开胃菜:为游戏服务器设置合适的Socket选项。这是网络通信主题下的第一篇。

适合游戏服务器的Socket选项

Socket是网络通信中对不同进程收发信息的端点的抽象。Socket本身与协议无关,它既可以支持TCP,也可以支持UDP。

Socket选项的设置会影响Socket本身的行为。在游戏服务器编程中,我们需要为常见的Socket选项设置合适的值。设置时应充分考虑游戏服务器的特点,例如开启TCP_NODELAY来保证尽量低的通信延时。如果设置不当或者忘记设置,程序可能会以违反我们预期的方式工作。以下的Java程序显示了对四种常见Socket选项的设置,以及它们在当前操作系统上的默认值:

        try (Socket socket = new Socket()) {// SO_KEEPALIVE 默认为falseSystem.out.println(socket.getKeepAlive());// SO_REUSEADDR 默认为falseSystem.out.println(socket.getReuseAddress());// SO_TIMEOUT 默认为0System.out.println(socket.getSoTimeout());// TCP_NODELAY 默认为falseSystem.out.println(socket.getTcpNoDelay());socket.setKeepAlive(true);socket.setReuseAddress(true);socket.setSoLinger(true, 0);socket.setSoTimeout(2000);socket.setTcpNoDelay(true);}

值得注意的是,同一个Socket选项在不同的操作系统上可能会有不同的默认值。因此,如果一个Socket选项是我们关心的,那么应该在使用前显式地为其赋值,从而避免在不同的平台上出现行为差异。

以上四种Socket选项,只有TCP_NODELAY命名以TCP开头,表示TCP专用;其他都以SO开头,表示通用选项,与具体协议无关。以下分别对它们做介绍。

TCP_NODELAY

这个选项的作用是禁用nagle算法。nagle算法的作用是把小包合并成大包,从而提高带宽利用率。

在TCP传输的过程中,如果一个包过小,例如只有1字节,那么单独传输非常不划算,因为还需要给它加上40字节的包头。为了解决这个问题,设计了nagle算法:只有当收到已发出包的ack消息,或者加上新包后缓冲区凑满一个MSS时,才会发送新包;否则,新包会放入缓冲区延迟发送。

nagle算法对提升带宽利用率效果显著,因此系统默认开启。但是它不适合在强调低延时的游戏服务器中使用。以MSS在以太网中的一般长度1460字节为基准,游戏中通常有相当大比例的包是小包,如果每个包都要等待前一个包的ack,或者等待凑满MSS,那么会极大地影响发送效率,造成显著的延迟。特别是在网络状态不佳时,对延迟的影响更加明显。因此,游戏服务器中一般会开启TCP_NODELAY,禁用nagle算法。

在这里插入图片描述

即使开启TCP_NODELAY,我们还是要注意过多小包对网络通信的影响。一种优化思路是在应用层做小包的合并。例如,对于服务器驱动战斗的状态同步,可以将同步频率由每条新战报产生就同步,改为每帧合并后同步一次,由于客户端实际上也是逐帧计算,所以这种改动不会对客户端表现造成任何影响。更多的合并策略会另起文章讲述,在此不再赘述。

SO_KEEPALIVE

这个选项的作用是定时判断连接是否存活。

它的运行机制如下:

  1. 如果通信双方超过两小时没有数据交换,那么开启SO_KEEPALIVE的一方会发送一个keep-alive包给对方。
  2. 如果对方返回ack,那么表示连接正常,本方会间隔两小时再发送keep-alive包。
  3. 如果对方返回rst,那么表示对方程序已在奔溃后重启,这时本方也应该关闭连接。
  4. 如果对方一直没有回应,本方会间隔75秒重新发送keep-alive包,循环反复直至次数上限,最后关闭连接。

SO_KEEPALIVE的意义是维持长连接不断开,减少重复创建连接带来的性能开销。它的缺点是发送keep-alive包的间隔时间过长,达两小时。在实际的游戏服务器中,很难想象会有如此长的时间没有通信数据。因此在游戏服务器开发中,一般会选择自己实现应用层的心跳包机制,用更短的心跳包间隔,实现对TCP连接和玩家在线更灵活及时的判定。在这种情况下,SO_KEEPALIVE的开启不是必需的,但是可以考虑开启,作为极限情况下的一种保底。

SO_REUSEADDR

这个选项通常被用来保证服务器重启后绑定的地址可重用。

当TCP连接关闭时,连接可能在关闭后的一段时间内保持TIME_WAIT状态,时间持续2MSL。在此期间,拥有此连接的应用程序无法将Socket重复绑定到该连接对应的地址和端口。

如果开启SO_REUSEADDR,那么即使以前的连接处于TIME_WAIT状态,也可以绑定Socket。游戏服务器一般都希望重启能立即完成,而且地址能重复使用,因此会选择开启这个选项。

需要注意的是,设置SO_REUSEADDR需要在Socket绑定地址之前进行,否则会没有效果。

以下代码展示了SO_REUSEADDR选项的用法:

public class ReuseAddrServer {public static void main(String[] args) throws IOException{try (ServerSocket ss = new ServerSocket()) {// 在 Linux 和 Mac 下该选项默认值是trueSystem.out.println(ss.getReuseAddress());// 注释掉下面这行,就不会再出现地址已占用报错ss.setReuseAddress(false);ss.bind(new InetSocketAddress("127.0.0.1", 1302));while(true){Socket socket = ss.accept();}}}
}

当启动ReuseAddrServer程序后,再使用nc 127.0.0.1 1302模拟客户端连接,然后kill掉ReuseAddrServer程序并重启,这时会报错:

 java.net.BindException: Address already in use (Bind failed)

如果把ss.setReuseAddress(false);这行注释掉,重新尝试,就不会再出现上述报错。注意以上代码需要在Linux或Mac环境下运行,这两个平台下SO_REUSEADDR默认值为true。而在Windows上有所不同,即使没有开启SO_REUSEADDR也能实现绑定地址的重用,因为Windows提供了另一种机制:通过系统设置默认开启“端口重用”功能。

SO_TIMEOUT

这个选项用来设置Socket的阻塞操作的超时时间。

阻塞操作包括accept、read以及UDP的receive。其中影响最大的是read操作。在游戏服务器中,典型的应用场景是先发送消息后接收回包,存在于客户端-服务器以及服务器-服务器之间,在这些场合使用阻塞读时需要为此设置超时时间。

如果不设置超时时间,SO_TIMEOUT会取系统默认的超时时间0,即超时时间无限。这样有可能永久阻塞进行IO操作的线程,造成严重的问题。笔者曾经遇到一个实例,是在生产中使用了某著名的第三方消息推送SDK,该SDK提送了API用于推送消息并接收响应,但是它底层使用的Socket没有设置SO_TIMEOUT,在一次网络出现问题时,造成用于推送消息的线程池中全部线程“假死”。因此,在任何情况下都应该为阻塞操作设置一个超时时间,避免无限等待。

小结

本文总结了游戏服务器中常用的四种Socket选项,平时开发中应该根据游戏服务器的特点进行合理的设置。为了保证服务器低延时和避免IO线程无限阻塞,应该始终开启TCP_NODELAY和SO_TIMEOUT;为了支持服务器快速重启且地址可复用,应该开启SO_REUSEADDR(系统默认开启);在业务层自己实现心跳机制的前提下,SO_KEEPALIVE作为可选项。