我们准备实现一个简单的http服务器。它具备有接收客户端请求,并通过读取相应的数据进行协议分析,最后返回相应的数据的能力。
从根本上来说,http服务器也就是一个网络服务器,nio框架netty也有一个参考的http协议实现。本参考实现不使用第三方网络包,仅通过javaSE来完成一个http协议实现,以用于在读取http协议时有一个更清晰的认识。
本实现的细节在一定程度上参考了netty,tomcat等现有的实现,但这里详细地把相应的理论细节解释清楚。
本篇为第1节,即通过构建一个nio服务端来作基本的网络处理。
在nio中,服务端建立起相应的serverSocket,并向selector注册相应的事件。当事件发生时去做相应的事情即可,在这个过程中数据的传输和代码实现是异步的,即在有数据的情况下才能做相应的事情,但数据和操作之间并不同步,即不能一次性地把所有的数据都读取完毕,或者不能一次性地把所有的数据都返回给客户端。所以,在整个实现中,需要在事件处理中不断地监听相应的事件,并通过在处理数据和事件切换中不断地变换具体的业务处理,直到一个完整地业务被处理掉。
在整个实现中,涉及到2个问题。一是事件的划分和处理,二是具体事件的隔离和处理。
1 事件的划分和处理
在服务端,需要处理3类事件信息,接收数据请求,读取请求数据,写入响应数据。读取数据和写入数据都是在connection已经建立好的情况下进行,而接收数据请求则表示服务端已经准备好接收由客户端发起的一个请求。这里将数据的处理和请求的接收分开,即分成2个大的处理部分。一个简单的原因即在于当连接已经建立起之后,服务端是一定要处理这个数据请求的;而连接还没有建立时,是可以拒绝处理的。即在当请求端有大量的请求在准备接收时,服务端不会因为请求的处理太多影响到后端的数据处理。
这里涉及到线程网络处理的问题,如果大量的处理线程都处理请求接收时,就意味着后台有大量的连接需要处理,而之前已经接收到的请求处理就会受到相应的影响,进而影响到整个系统的稳定性。将接收和处理分离,有利于更好地实现请求限制和分发。
这里使用了2个线程池和2个监听器来处理相应的接收和数据处理。即1个线程池只处理请求的接收和处理,另一个线程池则负责数据的读或写。简单的示意代码如下所示:
serverExecutorService.submit(new Runnable() { @Override public void run() { while(acceptSelector.isOpen()) { ...... Set<SelectionKey> selectionKeys = acceptSelector.selectedKeys(); for(Iterator<SelectionKey> keyIterator = selectionKeys.iterator();keyIterator.hasNext();) { keyIterator.next(); keyIterator.remove(); SocketChannel socketChannel = serverSocketChannel.accept(); logger.info("接收到连接,来自:" + socketChannel.getRemoteAddress()); ...... SelectionKey selectionKey = socketChannel.register(ioSelector, SelectionKey.OP_READ); } } } });
如上代码所示,在接收线程中,不断地处理事件(这里只有请求接收事件),一旦有相应的事件发生时,就创建相应的socket数据,同时向数据处理selector中注册。因为这里是接收到数据请求,则立即注册一个读事件,表示需要进行数据的获取操作。
而在另一个数据处理线程中,则只需要不断地轮循这个selector即可。当发生特定的事件时,只需要进行相应的业务处理即可。
2 通道的封装
在一次的数据处理时,处理的业务逻辑不仅要关注具体的channel,同时也要关注相应的selectionKey。因为这里随时会涉及到事件本身的状态变化。那么,如果我们每次在处理业务时都从key中获取数据,则会造成对key的依赖,同时在逻辑处理上,会把key到处进行传递。如果需要传递更多的数据,则在实现层面就会有更多的对象需要传输。
因此这里我们将涉及到整个数据处理中的元素进行了封装,即通过一个特定的通道对象将涉及到的对象封装在一起,并统一向外暴露相应的对象方法即可。而且针对一些状态变化操作,并不需要将原生对象直接向外暴露,而是通过特定的接口方法即可。封装的socketHolder对象如下所示:
public class SocketHolder { private SelectionKey selectionKey; private SocketChannel socketChannel; }
这个对象,在注册事件的时候即以附件的方式注册到key中,在后续的操作中,我们都是通过这个holder进行处理,也不排除将更多的对象添加到这个对象中。在最初接收到请求中,相应的holder即添加到attach当中,如下所示:
SelectionKey selectionKey = socketChannel.register(ioSelector, SelectionKey.OP_READ); selectionKey.attach(new SocketHolder(selectionKey, socketChannel));
以上的方法,保证每一个key都有相对应的holder,这样就可以避免在进行业务处理时不会造成数据之间的冲突,因为每个channel都有相对应的数据对象。
3 数据处理对象的分离
在简单的数据处理中,在处理读写事件时,都是直接在处理函数中进行。这样就把数据的读写和相应的业务耦合在一起。而通过一个具体负责处理数据的类,可以将这部分的工作隔离开来。具体点就是,在数据处理线程中,只需要接收到相应的事件,在事件发生时只需要将具体业务进行分发即可。
这里定义了一个receiver对象,其负责具体的数据处理,即处理数据的读,写,关闭,异常等。如下所示:
public interface Receiver { /** 读数据 */ public void read(SocketHolder socketHolder) throws IOException; /** 写数据 */ public void write(SocketHolder socketHolder) throws IOException; /** 处理错误 */ public void error(SocketHolder socketHolder); /** 关闭连接 */ public void close(SocketHolder socketHolder); }
此对象也是直接放到socketHolder中,以隔离业务处理,即每个receiver只处理当前socket相对应的数据。而在具体io线程中,只需要获取每个socketHolder,并提取相应的receiver,分发具体的数据处理即可。如下所示:
SocketHolder socketHolder = (SocketHolder) selectionKey.attachment(); if(socketHolder == null) continue; try { if(selectionKey.isReadable()) { socketHolder.getReceiver().read(socketHolder); continue; } if(selectionKey.isWritable()) { socketHolder.getReceiver().write(socketHolder); continue; } } catch(Exception e) { logger.error("socket处理异常", e); error(socketHolder); }
4 总结
通过以上的一个分离,我们初步建立起一个服务端,通道,数据处理的概念,通过在这几个对象之上进一步地封装数据处理,即可详细地搭建起整个网络处理程序。相比一些常见的nio例子,此处更仔细地是处理一个类似容器的实现,而不简单是一个demo,这里面要考虑的则不再是程序的正常运行,而是作为一个容器实现所需要的分析,设计和实现了。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201408100001.html