java - 从经典的多线程到java.nio异步/非阻塞服务器

标签 java

我是在线游戏的主要开发商。
玩家使用特定的客户端软件,该客户端软件通过TCP/IP(TCP,而不是UDP)连接到游戏服务器

目前,服务器的体系结构是一个经典的多线程服务器,每个连接只有一个线程。
但是在高峰时段,通常有300或400个连接的人,服务器变得越来越迟钝。

我想知道,是否通过切换到具有管理多个连接的少量线程的java.nio。*异步I/O模型,性能是否会更好。
在网络上查找涵盖此类服务器体系结构基础知识的示例代码非常容易。但是,经过数小时的谷歌搜索,我没有找到一些更高级的问题的答案:

1-该协议(protocol)是基于文本的,而不是基于二进制的。客户端和服务器交换以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将无法工作,这与非阻塞模式对于使用我愿意使用的高性能 channel 选择API是强制性的这一事实相矛盾。因此,我怀疑这不是我想做的正确解决方案。
因此,第一个问题如下:如果我不能使用它,那么如何有效而适本地处理换行,并使用缓冲区和 channel 在nio API中将 native Java字符串从/转换为UTF-8编码的数据?
我必须用手玩get/put还是在包装好的字节数组中玩吗?如何从ByteBuffer转换为以UTF-8编码的字符串?我承认不太了解如何使用charset包中的类以及它如何工作。

2-在异步/非阻塞I/O领域中,对本质上必须依次执行的连续读/写的处理又如何呢?
例如,登录过程通常是基于质询-响应的:服务器发送一个问题(特定的计算),客户端发送响应,然后服务器检查客户端给出的响应。
我认为,答案是肯定不会在整个登录过程中都完成发送给工作线程的单个任务,因为它很长,并且有卡住工作线程太多时间的风险(想象一下,这种情况:10个池线程,则有10位玩家尝试同时连接;与已经在线的玩家相关的任务会延迟到再次准备好一个线程为止)。

3-如果两个不同的线程在同一Channel上同时调用Channel.write(ByteBuffer),会发生什么情况?
客户会收到杂乱无章的电话吗?例如,如果一个线程发送“aaaaa”,而另一个发送“bbbbb”,则客户端可以接收“aaabbbbbaa”,还是我确保所有内容均以一致顺序发送?通话返回后,我是否可以修改使用的缓冲区?
或换种说法,我是否需要额外的同步来避免这种情况?
如果我需要附加同步,如何知道写入完成后何时释放锁定等?
恐怕答案并不像在选择器中注册OP_WRITE那样简单。通过尝试这一操作,我注意到我一直在为所有客户端始终获取写就绪事件,大多数情况下尽早退出Selector.select,因为每个客户端每秒仅发送3或4条消息,而选择时每秒执行数百次循环。因此,从潜在的角度来看,主动等待是非常糟糕的。

4-多个线程可以同时在同一个选择器上调用Selector.select,而没有任何并发​​问题,例如丢失事件,安排两次事件等吗?

5-实际上,尼奥是否和据说的一样好?停留在经典的多线程模型上会很有趣,但是不是每个连接创建一个线程,而是使用更少的线程并在连接上循环以使用InputStream.isAvailable查找数据可用性吗?这个想法愚蠢和/或效率低下吗?

最佳答案

1)是的。我认为您需要编写自己的非阻塞readLine方法。另请注意,当缓冲区中有几行或行不完整时,可能会发出无阻塞读取的信号:

示例:(初读)

 USER foo
 PASS

(第二读)
 bar

您将需要存储(请参阅2)尚未使用的数据,直到准备好足够的信息来处理它为止。
 //channel was select for OP_READ
 read data from channel 
 prepend data from previous read
 split complete lines
 save incomplete line
 execute commands

2)您将需要保留每个客户端的状态。
    Map<SocketChannel,State> clients = new HashMap<SocketChannel,State>();

连接 channel 后,将新状态put编码到 map 中
    clients.put(channel,new State());

或将当前状态存储为SelectionKeythe attached object

然后,在执行每个命令时,更新状态。您可以将其编写为整体方法,也可以做其他更有趣的事情,例如State的多态实现,其中每个状态都知道如何处理某些命令(例如LoginState需要USER和PASS,然后将状态更改为新的AuthorizedState)。

3)我不记得使用NIO时每个 channel 有很多异步编写器,但是文档说它是线程安全的(我不会详细说明,因为我没有证明)。关于OP_WRITE,请注意,当写缓冲区为而不是满时,它将发出信号。换句话说,就像here:OP_WRITE几乎总是准备就绪,即套接字发送缓冲区已满时除外,因此您只会导致Selector.select()方法无意识地旋转。

4)是的。 Selector.select()执行blocking selection operation

5)我认为最困难的部分是从每个客户端线程的体系结构切换到将读写与处理分离的另一种设计。完成此操作后,使用 channel 比使用自己的方式阻止流更容易。

关于java - 从经典的多线程到java.nio异步/非阻塞服务器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15348035/

相关文章:

java - 为什么对非 volatile 的写入对主线程可见?

java - 解码简单 xml

java - 用于学习的示例 Hibernate 应用程序

java - 如何在事务内将文档添加到 Firebase 集合?

java - Java 中不可修改的 vector

java - jython 2.7.0 中的 SSLHandshakeException

java - 我是否正确序列化/反序列化?

java - 二维数组的元素组合数量是可能的吗?

java - 从 Java 项目中的文件中读取文本

java - JSP Javabean 生成错误