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

时间:2013-03-11 20:31:47

标签: 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();
}

然后在运行中,使用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查找数据可用性?这个想法愚蠢和/或效率低吗?

1 个答案:

答案 0 :(得分:4)

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>();

当频道连接时,put新鲜状态进入地图

    clients.put(channel,new State());

或将当前状态存储为SelectionKey的{​​{3}}。

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

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

4)是的。 Selector.select()执行here

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