工业互联网平台IMSA---1.2.启动过程详解1

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






猜你喜欢

转载自blog.csdn.net/yt7589/article/details/79150468