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

前言

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

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

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

适合游戏服务器的Socket选项

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

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

        try (Socket socket = new Socket()) {
        	// SO_KEEPALIVE 默认为false
        	System.out.println(socket.getKeepAlive());
        	// SO_REUSEADDR 默认为false
        	System.out.println(socket.getReuseAddress());
        	// SO_TIMEOUT 默认为0
        	System.out.println(socket.getSoTimeout());
        	// TCP_NODELAY 默认为false
        	System.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 下该选项默认值是true
        	System.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作为可选项。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《Android开发案例驱动教程》 配套代码。 注: 由于第12,13,14章代码太大,无法上传到一个包中。 这三节代码会放到其他压缩包中。 作者:关东升,赵志荣 Java或C++程序员转变成为Android程序员 采用案例驱动模式展开讲解知识点,即介绍案例->案例涉及技术->展开知识点->总结的方式 本书作者从事多年一线开发和培训,讲解知识点力求细致,深入浅出 目 录 前言 第1章 Android操作系统概述 1 1.1 Android历史介绍 1 1.2 Android架构 1 1.3 Android平台介绍 2 1.4 现有智能手机操作系统比较 4 第2章 Android开发环境搭建 5 2.1 Eclipse和ADT插件 5 2.1.1 Eclipse安装 5 2.1.2 ADT插件 6 2.2 Android SDK 8 2.2.1 Android SDK的获得 8 2.2.2 Android SDK版本说明 10 2.2.3 ADT配置 10 2.3 Android开发模拟器 11 2.3.1 创建模拟器 11 2.3.2 启动模拟器 13 2.3.3 键盘映射与模拟器控制 13 2.3.4 横屏与竖屏切换 14 第3章 第一个Android程序 15 3.1 HelloAndroid 15 3.1.1 在Eclipse中创建项目 15 3.1.2 编写程序项目代码 17 3.1.3 运行HelloAndroid 18 3.1.4 Android工程目录 19 3.1.5 AndroidManifest.xml文件 21 3.2 Android中的组件介绍 22 3.3 使用Android SDK帮助 23 3.3.1 Android SDK API文档 23 3.3.2 Android SDK开发指南 24 3.3.3 Android SDK samples 24 3.4 使用DDMS帮助调试程序 26 3.4.1 启动DDMS 26 3.4.2 Device 28 3.4.3 Emulator Control 29 3.4.4 File Explorer 30 3.4.5 LogCat 31 3.5 使用ADB帮助调试程序 33 3.5.1 查询模拟器实例和设备 34 3.5.2 进入shell 34 3.5.3 导入导出文件 35 3.6 应用程序的打包、安装和卸载 37 3.6.1 应用程序打包 37 3.6.2 应用程序安装 40 3.6.3 应用程序卸载 40 本章小结 42 第4章 UI基础知识 43 4.1 Android UI组件概述 43 4.1.1 View 43 4.1.2 ViewGroup 44 4.1.3 布局管理器 44 4.2 UI设计工具 44 4.2.1 DroidDraw工具 44 4.2.2 ADT插件UI设计工具 46 4.3 事件处理模型 47 4.3.1 接口实现事件处理模型 47 4.3.2 内部类事件处理模型 49 4.3.3 匿名内部类事件处理模型 51 4.4 Activity中的常用事件 53 4.4.1 触摸事件 53 4.4.2 键盘事件 55 4.5 菜单 57 4.5.1 文本菜单 57 4.5.2 图片文本菜单 59 本章小结 60 第5章 UI基础控件 61 5.1 按钮 61 5.1.1 Button 62 5.1.2 ImageButton 63 5.1.3 ToggleButton 64 5.2 TextView 64 5.3 EditText 65 5.4 RadioButton和RadioGroup 66 5.4.1 RadioButton 66 5.4.2 RadioGroup 67 5.5 CheckBox 68 5.6 ImageView 70 5.7 Progress Bar 70 5.7.1 条状进度条 71 5.7.2 圆形进度条 73 5.7.3 对话框进度条 74 5.7.4 标题栏中进度条 75 5.8 SeekBar 76 5.9 RatingBar 78 本章小结 82 第6章 UI高级控件 83 6.1 列表类控件 83 6.1.1 Adapter概念 83 6.1.2 AutoComplete 84 6.1.3 Spinner 87 6.1.4 ListView 90 6.1.5 GridView 96 6.1.6 Gallery 99 6.2 Toast 103 6.2.1 文本类型 103 6.2.2 图片类型 104 6.2.3 复合类型 105 6.2.4 自定义显示位置Toast 106 6.3 对话框 107 6.3.1 文本信息对话框 107 6.3.2 简单列表项对话框 109 6.3.3 单选项列表项对话框 111 6.3.4 复选框列表项对话框 113 6.3.5 复杂布局列表项对话框 115 6.4 Android国际化和本地化 118 本章小结 121 第7章 UI布局 122 7.1 FrameLayout 122 7.1.1 TextSwitcher 124 7.1.2 ImageSwitcher 126 7.1.3 DatePicker 129 7.1.4 TimePicker 131 7.1.5 ScrollView 133 7.1.6 选项卡 134 7.2 LinearLayout 138 7.3 RelativeLayout 139 7.4 AbsoluteLayout 141 7.5 TableLayout 143 7.6 布局嵌套 146 7.7 屏幕旋转 152 本章小结 154 第8章 多线程 155 8.1 多线程案例--计时器 155 8.2 线程概念 156 8.2.1 进程概念 156 8.2.2 线程概念 156 8.3 Java中的线程 157 8.3.1 Java中的实现线程体方式1 157 8.3.2 Java中的实现线程体方式2 160 8.3.3 Java中的实现线程体方式3 162 8.4 Android中的线程 163 8.4.1 Android线程应用中的问题与分析 164 8.4.2 Message和MessageQueue 169 8.4.3 Handler 169 8.4.4 Looper和HandlerThread 172 本章小结 178 第9章 Activity和Intent 179 9.1 Activity 179 9.1.1 创建Activity 179 9.1.2 Activity生命周期 180 9.2 Intent 183 9.2.1 显式Intent 184 9.2.2 隐式Intent 186 9.2.3 匹配组件 186 9.3 多Activity之间跳转 188 9.3.1 多个Activity之间数据传递 189 9.3.2 跳转与返回 192 9.3.3 任务与标志 196 9.4 Android系统内置Intent 199 本章小结 201 第10章 数据存储 203 10.1 健康助手案例 203 10.2 Android数据存储概述 205 10.3 本地文件 205 10.3.1 访问SD卡 207 10.3.2 访问应用文件目录 212 10.4 SQLite数据库 216 10.4.1 SQLite数据类型 216 10.4.2 Android平台下管理SQLite数据库 216 10.5 编写访问SQLite数据库组件 220 10.5.1 DBHelper类 220 10.5.2 数据插入 222 10.5.3 数据删除 224 10.5.4 数据修改 224 10.5.5 数据查询 227 10.6 案例重构 229 10.6.1 系统架构设计 229 10.6.2 重构数据访问层 230 10.7 为案例增加参数设置功能 238 10.7.1 Shared Preferences 240 10.7.2 Preferences控件介绍 243 10.7.3 使用Preferences控件的案例 248 本章小结 250 第11章 Content Provider 251 11.1 Content Provider概述 251 11.2 Content URI 252 11.2.1 Content URI含义 252 11.2.2 内置的Content URI 253 11.3 通过Content Provider访问联系人 253 11.3.1 查询联系人 255 11.3.2 通过联系人ID查询联系人的Email 258 11.3.3 按照过滤条件查询Email 259 11.3.4 查询联系人的电话 261 11.4 通过Content Provider访问通话记录 262 11.4.1 查询通话记录 262 11.4.2 按照过滤条件查询通话记录 264 11.5 通过Content Provider访问短信 266 11.6 自定义Content Provider实现数据访问 269 11.6.1 编写Content Provider 269 11.6.2 在不同的应用中调用Content Provider 277 11.6.3 重构Content Provider调用 278 本章小结 281 第12章 多媒体 282 12.1 多媒体文件介绍 282 12.1.1 音频多媒体文件介绍 282 12.1.2 视频多媒体文件介绍 283 12.2 Android音频播放 284 12.2.1 Android音频/视频播放状态 284 12.2.2 音频播放案例介绍 286 12.2.3 资源音频文件播放 287 12.2.4 本地音频文件播放 291 12.2.5 网络音频文件播放 292 12.2.6 完善案例其他功能 293 12.3 Android音频录制 303 12.3.1 Android音频/视频录制状态 303 12.3.2 音频录制案例介绍 303 12.3.3 音频录制案例实现 305 12.4 Android视频播放 309 12.4.1 视频播放案例 309 12.4.2 采用MediaPlayer类播放视频 310 12.4.3 使用VideoView控件重构案例 315 本章小结 316 第13章 Service 317 13.1 Service概述 317 13.1.1 本地Service生命周期 317 13.1.2 远程Service生命周期 318 13.2 本地Service 319 13.2.1 本地Service案例 319 13.2.2 编写AudioService 320 13.2.3 调用Service 322 13.2.4 重构案例 323 13.3 远程Service 325 13.3.1 远程Service调用原理 325 13.3.2 远程Service案例 326 13.3.3 设计AIDL文件 327 13.3.4 编写AudioService 331 13.3.5 调用远程Service 336 13.3.6 组件间参数传递 343 本章小结 347 第14章 Broadcast Receiver和Notification 348 14.1 Broadcast Receiver 348 14.1.1 音频播放案例 349 14.1.2 编写音频播放Broadcast Receiver 350 14.1.3 注册音频播放Broadcast Receiver 351 14.1.4 接收系统的广播 353 14.1.5 MP3下载服务案例 353 14.2 Notification 358 14.2.1 完善MP3下载服务案例 358 14.2.2 完善音频播放案例 363 14.2.3 其他形式的Notification 369 本章小结 371 第15章 云端应用 372 15.1 典型云端应用--城市天气信息服务 372 15.2 网络通信技术与实现 374 15.2.1 网络通信技术介绍 376 15.2.2 Java URL类实现方式 377 15.2.3 Apache HttpClient实现方式 378 15.3 数据交换格式 380 15.3.1 纯文本格式 381 15.3.2 XML格式 381 15.3.3 JSON格式 385 15.4 自定义服务器端程序实例 387 15.4.1 Java Servlet概述 387 15.4.2 编写城市信息服务的Servlet 388 15.4.3 编写城市天气服务的Servlet 393 15.4.4 再次探讨HttpClient的POST请求 395 15.5 云端应用案例优化 400 本章小结 404 第16章 Google Map和定位服务 405 16.1 MyMap服务系统案例 405 16.2 Android Google Map 406 16.2.1 申请Google Map Android API Key 407 16.2.2 编写Android Google Map骨架程序 409 16.2.3 控制地图 412 16.2.4 地图的显示模式 416 16.2.5 地图的图层 419 16.2.6 查询与定位 422 16.3 Android定位服务 430 16.3.1 开启定位服务 431 16.3.2 模拟测试 433 16.3.3 GPS与Google Map结合 435 16.4 案例重构 437 16.4.1 重构"定位查询"方法 438 16.4.2 重构"查询周围"方法 440 本章小结 443 第17章 Android通信应用 444 17.1 电话应用开发 444 17.1.1 拨打电话功能 444 17.1.2 呼入电话状态 446 17.2 短信和彩信应用开发 450 17.2.1 Android内置的发送短信/彩信功能 450 17.2.2 自己编写发送文本内容的短信 452 17.2.3 自己编写接收文本内容的短信 458 17.2.4 自己编写发送二进制内容的短信 459 17.2.5 自己编写接收二进制内容的短信 461 17.3 蓝牙通信 463 17.3.1 Android 2 BluetoothChat案例 464 17.3.2 Android 2 蓝牙API介绍 464 17.3.3 TCP Socket与蓝牙Socket的区别 465 17.3.4 BluetoothChat中的类 466 17.3.5 初始化本地蓝牙设备 467 17.3.6 查找蓝牙设备 471 17.3.7 管理连接 476 17.3.8 互相之间的通信 480 17.4 WiFi通信 484 17.4.1 管理WiFi 484 17.4.2 扫描热点 487 17.4.3 Socket通信 489

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值