http服务器参考实现-1 client server channel分离

我们准备实现一个简单的http服务器。它具备有接收客户端请求,并通过读取相应的数据进行协议分析,最后返回相应的数据的能力。

从根本上来说,http服务器也就是一个网络服务器,nio框架netty也有一个参考的http协议实现。本参考实现不使用第三方网络包,仅通过javaSE来完成一个http协议实现,以用于在读取http协议时有一个更清晰的认识。
本实现的细节在一定程度上参考了netty,tomcat等现有的实现,但这里详细地把相应的理论细节解释清楚。
本篇为第1节,即通过构建一个nio服务端来作基本的网络处理。

在nio中,服务端建立起相应的serverSocket,并向selector注册相应的事件。当事件发生时去做相应的事情即可,在这个过程中数据的传输和代码实现是异步的,即在有数据的情况下才能做相应的事情,但数据和操作之间并不同步,即不能一次性地把所有的数据都读取完毕,或者不能一次性地把所有的数据都返回给客户端。所以,在整个实现中,需要在事件处理中不断地监听相应的事件,并通过在处理数据和事件切换中不断地变换具体的业务处理,直到一个完整地业务被处理掉。

在整个实现中,涉及到2个问题。一是事件的划分和处理,二是具体事件的隔离和处理。

1 事件的划分和处理

在服务端,需要处理3类事件信息,接收数据请求,读取请求数据,写入响应数据。读取数据和写入数据都是在connection已经建立好的情况下进行,而接收数据请求则表示服务端已经准备好接收由客户端发起的一个请求。这里将数据的处理和请求的接收分开,即分成2个大的处理部分。一个简单的原因即在于当连接已经建立起之后,服务端是一定要处理这个数据请求的;而连接还没有建立时,是可以拒绝处理的。即在当请求端有大量的请求在准备接收时,服务端不会因为请求的处理太多影响到后端的数据处理。

这里涉及到线程网络处理的问题,如果大量的处理线程都处理请求接收时,就意味着后台有大量的连接需要处理,而之前已经接收到的请求处理就会受到相应的影响,进而影响到整个系统的稳定性。将接收和处理分离,有利于更好地实现请求限制和分发。

继续阅读“http服务器参考实现-1 client server channel分离”

nio通讯中cpu 100%的问题及处理(转)

本文转自:http://marlonyao.iteye.com/blog/1005690 原文作者:marlonyao

在经典的nio通讯例子中,通常是这样写的:

if (key.isAcceptable()) {  
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();  
                        SocketChannel client = server.accept();  
                        System.out.println("Accepted connection from " + client);  
                        client.configureBlocking(false);  
//开始注册读写事件
                        SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);  
}
//开始写
if (key.isWritable()) {  
                        // System.out.println("is writable...");  
                        SocketChannel client = (SocketChannel) key.channel();  
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  
                        buffer.flip();  
                        client.write(buffer);  
                        buffer.compact();  
                    }  

在测试例子中,该例子也能够正常的输出,不过就是有一个现象:

但是如果你这时top用看一下发现服务器进程CPU占用到95%以上,如果取消掉32行的注释,服务器会不断地输出"is writable…",这是为什么呢?让我们来分析当第一个客户端连接上时发生什么情况。

  1. 在连接之前,服务器第11行:selector.select()处阻塞。当阻塞时,内核会将这个进程调度至休眠状态,此时基本不耗CPU。
  2.     当客户端发起一个连接时,服务器检测到客户端连接,selector.select()返回。selector.selectedKeys()返回已就绪的SelectionKey的集合,在这种情况下,它只包含一个key,也就是53行注册的acceptable key。服务器开始运行17-25行的代码,server.accept()返回代码客户端连接的socket,第22行在socket上注册OP_READ和OP_WRITE,表示当socket可读或者可写时就会通知selector。
  3.     接着服务器又回到第11行,尽管这时客户端还没有任何输入,但这时selector.select()不会阻塞,因为22行在socket注册了写操作,而socket只要send buffer不满就可以写,刚开始send buffer为空,socket总是可以写,于是server.select()立即返回,包含在22行注册的key。由于这个key可写,所以服务器会运行31-38行的代码,但是这时buffer为空,client.write(buffer)没有向socket写任何东西,立即返回0。
  4.     接着服务器又回到第11行,由于客户端连接socket可以写,这时selector.select()会立即返回,然后运行31-38行的代码,像步骤3一样,由于buffer为空,服务器没有干任何事又返回到第11行,这样不断循环,服务器却实际没有干事情,却耗大量的CPU。

从上面的分析可以看出问题在于我们在没有数据可写时就在socket上注册了OP_WRITE,导致服务器浪费大量CPU资源,解决办法是只有数据可以写时才注册OP_WRITE操作。上面的版本还不只浪费CPU那么简单,它还可能导致潜在的死锁。虽然死锁在我的机器上没有发生,对于这个简单的例子似乎也不大可能发生在别的机器上,但是在对于复杂的情况,比如我写的端口转发工具中就发生了,这还依赖于jdk的实现。

继续阅读“nio通讯中cpu 100%的问题及处理(转)”