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

Que*_*inC 9 java

我是在线游戏的主要开发者.玩家使用通过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();
}
Run Code Online (Sandbox Code Playgroud)

然后在运行中,使用readLine逐行读取数据.

在doc中,我发现了一个可以通过SocketChannel创建Reader的实用类Channels.但据说如果Channel处于非阻塞模式,生产的Reader将无法工作,这与非阻塞模式必须使用我愿意使用的高性能通道选择API的事实相矛盾.所以,我怀疑它不适合我想做的事情.因此,第一个问题如下:如果我不能使用它,如何有效和妥善地处理断行并使用缓冲区和通道将本机java字符串转换为nio API中的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 - 事实上,nio和它说的一样好吗?保持经典的多线程模型是否有趣,但不是每个连接创建一个线程,使用更少的线程并在连接上循环以使用InputStream.isAvailable查找数据可用性?这个想法是愚蠢和/或低效的吗?

Jav*_*ier 3

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

示例:(第一次阅读)

 USER foo
 PASS
Run Code Online (Sandbox Code Playgroud)

(第二读)

 bar
Run Code Online (Sandbox Code Playgroud)

您将需要存储(参见 2)未使用的数据,直到准备好足够的信息来处理它。

 //channel was select for OP_READ
 read data from channel 
 prepend data from previous read
 split complete lines
 save incomplete line
 execute commands
Run Code Online (Sandbox Code Playgroud)

2)您需要保留每个客户端的状态。

    Map<SocketChannel,State> clients = new HashMap<SocketChannel,State>();
Run Code Online (Sandbox Code Playgroud)

当通道连接时,put地图中会出现新状态

    clients.put(channel,new State());
Run Code Online (Sandbox Code Playgroud)

或者将当前状态存储SelectionKey.

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

3)我不记得每个通道使用NIO与许多异步编写器,但文档说它是线程安全的(我不会详细说明,因为我没有证据证明这一点)。关于 OP_WRITE,请注意,它会在写入缓冲区未满时发出信号。换句话说,正如这里所说: OP_WRITE 几乎总是准备就绪,即除非套接字发送缓冲区已满,所以您只会导致您的Selector.select()方法无意识地旋转。

4)是的。Selector.select()执行阻塞选择操作

5)我认为最困难的部分是从每个客户端线程架构切换到读取和写入与处理分离的不同设计。完成此操作后,使用通道比按照自己的方式使用阻塞流更容易。