我是一款网络游戏的主要开发者。玩家使用特定的客户端软件通过 TCP/IP(TCP,不是 UDP)连接到游戏服务器
目前,服务器的架构是一个经典的多线程服务器,每个连接一个线程。但是在高峰时段,当经常有 300 或 400 个连接的人时,服务器变得越来越迟钝。
我想知道,如果切换到 java.nio.* 异步 I/O 模型,用很少的线程管理很多连接,性能会更好。在 Web 上查找涵盖此类服务器架构基础知识的示例代码非常容易。但是,经过数小时的谷歌搜索,我没有找到一些更高级问题的答案:
1 - 该协议是基于文本的,而不是基于二进制的。客户端和服务器交换以 UTF-8 编码的文本行。单行文本代表一个命令,每一行都以 \n 或 \r\n 正确终止。对于经典的多线程服务器,我有这样的代码:
public Connection (Socket sock) {
this.in = new BufferedReader( new InputStreamReader( sock.getInputStream(), "UTF-8" ));
this.out = new BufferedWriter( new OutputStreamWriter(sock.getOutputStream(), "UTF-8"));
new Thread(this) .start();
}
然后在运行中,使用 readLine 逐行读取数据。
在文档中,我找到了一个实用类 Channels,它可以从 SocketChannel 创建一个 Reader。但是据说如果 Channel 处于非阻塞模式,则生成的 Reader 将无法工作,这与非阻塞模式必须使用我愿意使用的高性能通道选择 API 的事实相矛盾。所以,我怀疑这不是我想做的正确解决方案。因此,第一个问题如下:如果我不能使用它,如何有效和正确地处理断行并将本机 java 字符串从/转换为 nio API 中的 UTF-8 编码数据,带有缓冲区和通道?我是否必须手动使用 get/put 或在包装的字节数组中?如何从 ByteBuffer 转到以 UTF-8 编码的字符串?我承认不
2 - 在异步/非阻塞 I/O 世界中,如何处理本质上一个接一个地依次执行的连续读/写?例如,典型的基于挑战-响应的登录过程:服务器发送一个问题(特定计算),客户端发送响应,然后服务器检查客户端给出的响应。答案是,我认为,当然不要为整个登录过程创建一个任务发送到工作线程,因为它很长,有冻结工作线程太长时间的风险(想象一下这种情况:10 个池线程, 10 个玩家同时尝试连接;与已经在线的玩家相关的任务被延迟,直到有一个线程再次准备好)。
3 - 如果两个不同的线程同时在同一个 Channel 上调用 Channel.write(ByteBuffer) 会发生什么?客户可能会收到混淆的线路吗?例如,如果一个线程发送“aaaaa”而另一个线程发送“bbbbb”,客户端是否可以接收“aaabbbbbaa”,或者我是否确保每个线程都以一致的顺序发送?我可以在调用返回后立即修改使用的缓冲区吗?或者换一种方式问,我是否需要额外的同步来避免这种情况?如果我需要额外的同步,如何知道何时释放锁等,写完成后?恐怕答案并不像在选择器中注册 OP_WRITE 那样简单。通过尝试,我注意到我一直都得到了 write-ready 事件,并且总是针对所有客户端,主要是提前退出 Selector.select,因为每个客户端每秒只发送 3 或 4 条消息,而选择循环每秒执行数百次。因此,从潜在的角度来看,积极等待是非常糟糕的。
4 - 多个线程是否可以同时在同一个选择器上调用 Selector.select 而不会出现任何并发问题,例如丢失事件、调度两次等?
5 - 事实上,nio 是否像它所说的那样好?保留经典的多线程模型是否会很有趣,但不是为每个连接创建一个线程,而是使用更少的线程并循环连接以使用 InputStream.isAvailable 查找数据可用性?这个想法是愚蠢和/或低效的吗?