虽然微服务工业云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,如果大家觉得项目对大家有帮助,肯请大家为我点赞,谢谢大家!