虽然微服务工业云IMSA内部采用基于消息总线的异步消息处理机制,但是当前工业企业无论自动化App还是业务管理App,通常采用的是类REST的请求响应式接口,因此IMSA系统提供了门户Facade子系统,由该子系统与外部系统进行请求响应式交互,在内部则将请求转换为系统的消息,发送到消息总线Plato上,由消息驱动完成所需业务逻辑,最后门户Facade系统从消息总线中获得响应结果,发送给外部系统。因此系统启动主要指门户Facade的启动和消息总线的启动,而实现业务逻辑的微服务和基于微服务的事务管理器---微服务控制器,则是相对独立的子系统,可以独立于门户Facade和消息总线Plato所提供的基础架构,自主决定上线和下线操作。在本篇博文中,我们将首先来介绍门户Facade的启动过程。
门户Facade会启动基于NIO技术的服务器。我们知道在NIO之前,Java由于采用了阻塞性IO,处理并发请求的能力并不强,所以在实际项目中,通常会在Java应用服务器之前,加一个Nginx或Apache作为负载均衡器,以解决Java应用服务器并发能力不强的问题。而随着NIO和NIO2的推出,Java采用非阻塞性IO,性能已经接近甚至超过C++的Web服务器,而由于历史原因,当前主流的Java应用服务器Tomcat和JBoss等,虽然也支持NIO技术,但是在实际中部署很少。在这里,我们采用了重新发明轮子的方式,从头开始写一个基于NIO技术的应用服务器。大神们经常教育我们,千万不要重新发明轮子,有成熟的项目就用成熟的项目。为什么我们还来重新发明轮子呢?首先,重新发明轮子,并不像大神们说的那么难和神秘,甚至并不比我们平常项目中所用到的技术复杂,当然这里可能会涉及到线程同步、锁、连接池等因素,但是了解基本原理之后,这并不是什么高不可攀的技术。其次,要想用好现有的轮子,就是完全理解一个如SSH这样的框架,需要了解的内容比重新发明轮子要多得多,通常技术栈的内容是一个倒金字塔,底层技术虽然有些难度,但是都是指导性的,内容比较精简,但是我们通常采用的框架,经过层层抽象和大量应用设计模式之后,其实质上是一个异常复杂的系统,我们通常仅仅使用其中很小很小一部分功能,而对其大部分内容,我们是根本不了解的,这就导致了我们只能做一些简单的工作,一旦需求改变或出现系统级的BUG,我们就无能为力了,而长此以往之后,我们的编码能力就下降了,成为所谓的码农;最后,重新发明轮子,可以采用最适合的技术,如果使用得当,可以提高系统的质量。因为主流成熟的框架,主体都是采用几代之前的技术,虽然也会加入新技术支持,但是由于向后兼容性,支持程度通常不太理想。而我们重新发明轮子,我们就可以采用例如Java8的最新技术,减少代码量(采用Lambda表达式),提高并发性(采用Stream API),提高健壮性(采用Optional避免空指针异常)等。大神给我们的建议虽然有合理性,但是同时也有很大的原因是他们想把框架开发神秘化,提高自身的身价,在这点上,我们要有自己独立的思考。
门户Facade首先启动NIO服务器,代码如下所示:
public static void main( String[] args ) { System.out.println( "微服务工业云平台..." ); ImsaServer imsaServer = new ImsaServer(); try { imsaServer.start(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }
NIO服务器启动代码如下所示:
private short port = 8088; // 服务器监听端口 /** * 程序总入口,启动Imsa服务器 * @throws Exception */ public void start() throws Exception { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.socket().bind(new InetSocketAddress(port)); while(true){ while (selector.select() > 0) { Iterator<SelectionKey> selectedKeys = selector.selectedKeys() .iterator(); while (selectedKeys.hasNext()) { SelectionKey key = selectedKeys.next(); if (key.isAcceptable()) { acceptConnection(key, selector); } else if (key.isReadable()) { readRequest(key, selector); } else if (key.isWritable()) { sendResponse(key, prepareTestResponse()); } } } } }
程序首先打开一个Socket选择器,然后打开服务器的Channel,将其设置为非阻塞模式,并且向系统注册想要接收客户端连接事件,然后设置是否允许重用端口,这里设置为可以,这主要用于多个应用对同一端口进行监听,最后将其绑定到port指定的端口上。
然后程序就进入了一个无限循环,每次循环中,如果有需要操作的Socket,则取出这些Socket进行循环处理。对于每个Socket,取出其需要进行的操作key,我们在这里只是简单的处理接受连接、接收请求和发送响应三种操作。下面我们分别来看这三种情况的处理代码。
我们首先来看接收客户端连接的代码,如下所示:
/** * 接受客户端的连接请求 * @param key * @param selector */ private void acceptConnection(SelectionKey key, Selector selector) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel channel = null; try { channel = ssc.accept(); if(channel != null){ channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ);// 客户socket通道注册读操作 } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }我们首先得到服务器Channel,然后接受客户端连接并保存到channel中,将其设置为非阻塞方式,并向系统注册,我们需要监听读入操作。因为我们接受客户端连接之后,我们需要做的是读入客户端的请求。
接下来我们来看接收客户端请求的具体内容,我们在这里先以最简单的HTTP文本形式的GET和POST请求为例,向大家讲解具体的处理方法,在我们需要处理文件上传等需求时,我们再来讲解怎样处理二进制数据。这也是我们自己发明的轮子的优势所在,我们不需为不需要的功能开发代码。
接收文本HTTP请求的代码如下所示:
/** * 读取消息内容,并向消息总线plato发送消息 * @param key * @param selector */ private void readRequest(SelectionKey key, Selector selector) { SocketChannel channel = (SocketChannel) key.channel(); try { channel.configureBlocking(false); String receive = receive(channel); // 如果没有接收到内容,就直接返回 if (receive.equals("")) { return ; } BufferedReader b = new BufferedReader(new StringReader(receive)); String s = b.readLine(); StringBuilder req = new StringBuilder(); while (s != null) { req.append(s + "\r\n"); s = b.readLine(); } b.close(); String[] urls = null; String msgStr = ImsaMsgEngine.createMsg(AppConsts.MT_HTTP_GET_REQ, AppConsts.MT_MSG_V1, req.toString(), null); System.out.println("v0.0.1 msg:" + msgStr + "!"); channel.register(selector, SelectionKey.OP_WRITE); // 发送消息 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }由于我们只读取文本请求,所以我们用BufferedReader读入所有文本,当读入整个请求之后,我们会调用ImsaMsgEngine产生Json字符串格式的系统消息,我们将在下一节中详细讨论这个方法的实现,在这里我们就知道其会根据消息类型、消息版本、消息内容生成一个Json字符串就可以了。当生成系统消息之后,我们就将系统消息发送到消息总线Plato上,这里我们先将这部分代码留到下一篇博文中来介绍。同时我们向系统不册,我们将要发送Socket数据。
具体接收数据的方法如下所示:
/** * 接收请求数据 * @param socketChannel * @return */ private String receive(SocketChannel socketChannel) { ByteBuffer buffer = ByteBuffer.allocate(1024); byte[] bytes = null; int size = 0; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { while ((size = socketChannel.read(buffer)) > 0) { buffer.flip(); bytes = new byte[size]; buffer.get(bytes); baos.write(bytes); buffer.clear(); } bytes = baos.toByteArray(); } catch (IOException ex) { return ""; } return new String(bytes); }
在真实系统中,我们的系统消息将触发一系列微服务,来完成复杂的业务逻辑,处理的结果以系统消息的形式,将处理结果发送到消息总线Plato上,门户Facade通过监听消息总线,取回处理结果,将处理结果发送给客户端,从而完成一个完整的请求响应流程。我们在这里,先不考虑在系统内部的消息异步处理机制,我们先来看怎样将处理结果发送给客户端,代码如下所示:
/** * 从消息总线接收到需要发送的HTTP响应,将响应发送给客户端 * @param key * @param resp */ private void sendResponse(SelectionKey key, String resp) { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); byte[] bytes = resp.toString().getBytes(); buffer.put(bytes); buffer.flip(); try { channel.write(buffer); channel.shutdownInput(); channel.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }如上所示,程序按照字节形式,将响应数据发送给客户端。为了使程序正常运行,我们需要一个生成简单HTTP响应的辅助测试方法,代码如下所示:
/** * 临时方法,产生向客户端发送的响应 * @return */ private String prepareTestResponse() { String hello = "<html><head><meta charset=\"utf-8\" /></head><body>IMSA v0.0.1...微服务工业云(测试版本)<br />测试读入内容是否正确<br />Hello World!</body></html>"; StringBuilder resp = new StringBuilder(); resp.append("HTTP/1.1 200 OK" + "\r\n"); resp.append("Server: Microsoft-IIS/5.0 " + "\r\n"); resp.append("Date: Thu,08 Mar 200707:17:51 GMT" + "\r\n"); resp.append("Connection: Keep-Alive" + "\r\n"); resp.append("Content-Length: " + hello.getBytes().length + "\r\n"); resp.append("Content-Type: text/html\r\n"); resp.append("\r\n" + hello); return resp.toString(); }我们将在下一篇博文向大家讲解消息的生成函数,读者可以将ImsaMsgEngine.createMsg方法调用换为一个字符串。
启动程序,打开浏览器,在地址栏中输入:http://ip_addr:8088,如果一切顺利的话,就会在页面中显示如下内容:
如果读者朋友对代码有疑问,可以参考Github上的开源项目:https://github.com/yt7589/imsa,如果大家觉得项目对大家有帮助,肯请大家为我点赞,谢谢大家!