谈谈基于Netty实现Restful搭建服务架构思路

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiaoyw/article/details/80868575

  自从使用商用Opentext Cordys BOP搭建了符合Gartner多租户模型的云应用服务后,一直思考使用开源框架再搭建一个云服务架构,例如使用当前流行的Spring Cloud,以及,更底层的Java HttpServer。目标是实现自主知识产权、轻量级的云服务平台或架构,发挥集成NoSQL(例如Mongo DB)、大数据(AI)优势,通过前、后端分离,软件功能服务化,能为产品研发提供快速开发平台和可复用服务。

  为什么不用开源Spring系列框架呢,这要从2009年开始讲起,那时,本人也是铁杆SSH框架的粉丝,自从使用Cordys平台后,发现除了J2EE架构、微软.Net架构以外,还有很多面向服务的架构,先看当时前后端分离的服务架构(SOA),如下图所示。
这里写图片描述

  前端使用HTML+JQuery,部署在Apache Http服务上,后端基于Cordys开发Soap Webservice。
  
  在2014年,又成功引入NoSQL数据库Mongo DB,在OpenText上海国际交流会议上,经友人提示,定义为非数值敏感型表单解决方案,详见《HTML(JS)+SOA+MongoDB简易架构实践经验》,概括的说,软件设计没有表结构,修改前端表单界面,不必修改后端代码,无表结构限制,与SSH、SSI等Spring框架冲突,与Cordys对象模型也冲突,如此这样,需要依赖更多的原生、底层的设计与开发。

  参照Node.JS和PHP等实现微服务模式,研究过使用Java自带的HTTP Server实现Restfull服务,详见《基于Java内置的HttpServer实现轻量级Restful》。最近,在友人的提醒下,发现采用Netty网络应用框架将会效果更好。

  设计思路是把基于Java内置的HttpServer实现轻量级Restful,替换为Netty实现HTTP Server。总体架构如下图所示。

这里写图片描述

  Netty是基于Java NIO client-server的网络应用框架,使用Netty可以快速开发网络应用,例如服务器和客户端协议。Netty提供了一种新的方式来开发网络应用程序,这种新的方式使它很容易使用和具有很强的扩展性。Netty的内部实现是很复杂的,但是Netty提供了简单易用的API从网络处理代码中解耦业务逻辑。Netty是完全基于NIO实现的,所以整个Netty都是异步的。

  网络应用程序通常需要有较高的可扩展性,无论是Netty还是其他的基于Java Nio的框架,都会提供可扩展性的解决方案。Netty中一个关键组成部分是它的异步特性,总体结构如下图所示。

这里写图片描述

  为什么考虑Netty解决方案,请先了解其他人“《Nesty 高性能轻量级Http Restful Server》 ”怎么说的:

(1)Jetty + Jersay

  最开始我们想到了Jetty框架,他可以用非容器的方式,只是一个library级别。再配合Jersay完成类似SpringMVC的HTTP注解,使用方式也很简单。

  也满足非容器的需求,调试方便,启动时间1~2s,足够轻量。但简单做了性能测试后,发现QPS依旧不高。机器配置24核,48G内存,千兆多队列网卡。约7k~8k QPS(Http短连接,4个ab并发128)。将涉及的各个Jetty参数设置了多遍,提升依旧不大。

(2)NginxLua(OpenResty) or Golang or Node

  出于性能考虑,我们参考了一下NginxLua和原生的Nginx,这种基于多进程epoll在非阻塞IO上性能完全可以满足,加之Lua脚本开发成本低。还参考了一下Golang和Node,Golang基于netpoll组件,底层也是基于epoll非阻塞加之协程的并发模式,满足需求。

  但NginxLua和Golang是非Java系的,只能满足一些非核心的新应用可以从头开始编码,老应用里又有很多的HSF和Java库的调用,并且没有SpringMVC那种方便的HTTP注解,所以只能部分应用使用。老应用肯定不会迁移上去。

(3)Nesty

  出于上述的调研想法,我们试图寻找一个能像Nginx或Golang一样性能比较高,并且基于Java的实现。所以我们决定自己实现一个轻量级的Http服务server。JavaNIO方面最成熟的就算Netty了,基于他来做底层的网络IO肯定OK,所以我们的方案中NIO方面就选择它了。网络协议方面正好Netty也提供了相应的decoder和encoder,真是方便。但目前有公司在生产环境使用,但无大型系统的case。所以我们还是仔细的扣了一遍代码,并计划之后fork出来自己优化这个decoder。

  Nesty就这么诞生了,从名字可以看出它是Netty + HttpRest。

  https://github.com/gugemichael/nesty/

  下面开始使用Netty开发实践:

(1)启动 ServerBootstrap(建立连接)

  Netty官方主页为:http://netty.io/index.html
  下载最新版地址:https://dl.bintray.com/netty/downloads/netty-4.1.25.Final.tar.bz2
  涉及到JSON处理的下载地址为:json-20180130.jar

  代码示例引自《Netty的restful API 简单实现和部署》
这里写图片描述

  新建一个Java类MainServer,加入 ServerBootstrap的启动代码。这部分代码源自Netty 的Http Example,所有的Netty 服务启动代码和这类似。

package com.yw.restserver;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;

public final class MainServer {

    /*是否使用https协议*/
    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "6789"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.option(ChannelOption.SO_BACKLOG, 1024);
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ServerInitializer(sslCtx));

            Channel ch = b.bind(PORT).sync().channel();

            System.err.println("Open your web browser and navigate to " +
                    (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');

            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

(2)ChannelInitializer(初始化连接),在工程中新建一个class ServerInitializer,用于连接的初始化。

package com.yw.restserver;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.ssl.SslContext;

public class ServerInitializer extends ChannelInitializer<SocketChannel> {

    private final SslContext sslCtx;

    public ServerInitializer(SslContext sslCtx) {
        this.sslCtx = sslCtx;
    }

    @Override
    public void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        if (sslCtx != null) {
            p.addLast(sslCtx.newHandler(ch.alloc()));
        }
        p.addLast(new HttpServerCodec());/*HTTP 服务的解码器*/
        p.addLast(new HttpObjectAggregator(2048));/*HTTP 消息的合并处理*/
        p.addLast(new HealthServerHandler()); /*自己写的服务器逻辑处理*/
    }
}

(3)ChannelHandler(业务控制器),以上两份代码是固定功能的框架代码,业务控制器Handler才是自有发挥的部分。

  需要获取客户端的请求uri做路由分发,不同的请求做不同的响应。
  把客户端的请求数据解析成Json对象,方便做运算。
  把计算好的结果生成一个Json 数据发回客户端。

package com.yw.restserver;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;

import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.*;



import org.json.JSONObject;

public class HealthServerHandler extends ChannelInboundHandlerAdapter {

    private static final AsciiString CONTENT_TYPE = new AsciiString("Content-Type");
    private static final AsciiString CONTENT_LENGTH = new AsciiString("Content-Length");
    private static final AsciiString CONNECTION = new AsciiString("Connection");
    private static final AsciiString KEEP_ALIVE = new AsciiString("keep-alive");

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {

        if (msg instanceof FullHttpRequest) {
            FullHttpRequest req = (FullHttpRequest) msg;//客户端的请求对象
            JSONObject responseJson = new JSONObject();//新建一个返回消息的Json对象

            //把客户端的请求数据格式化为Json对象
            JSONObject requestJson = null;
            try{
               requestJson = new JSONObject(parseJosnRequest(req));
            }catch(Exception e)
            {
                ResponseJson(ctx,req,new String("error json"));
                return;
            }

            String uri = req.uri();//获取客户端的URL

            //根据不同的请求API做不同的处理(路由分发),只处理POST方法
            if (req.method() == HttpMethod.POST) {
                if(req.uri().equals("/bmi"))
                { 
                    //计算体重质量指数
                    double height =0.01* requestJson.getDouble("height");
                    double weight =requestJson.getDouble("weight");
                    double bmi =weight/(height*height);
                    bmi =((int)(bmi*100))/100.0;
                    responseJson.put("bmi", bmi +"");

                }else if(req.uri().equals("/bmr"))
                {
                    //计算基础代谢率
                    boolean isBoy = requestJson.getBoolean("isBoy");
                    double height = requestJson.getDouble("height");
                    double weight = requestJson.getDouble("weight");
                    int age = requestJson.getInt("age");
                    double bmr=0;
                    if(isBoy)
                    {
                        //66 + ( 13.7 x 体重kg ) + ( 5 x 身高cm ) - ( 6.8 x 年龄years )
                        bmr = 66+(13.7*weight) +(5*height) -(6.8*age);

                    }else
                    {
                        //655 + ( 9.6 x 体重kg ) + ( 1.8 x 身高cm ) - ( 4.7 x 年龄years )
                        bmr =655 +(9.6*weight) +1.8*height -4.7*age;
                    }

                    bmr =((int)(bmr*100))/100.0;
                    responseJson.put("bmr", bmr+"");
                }else {
                    //错误处理
                    responseJson.put("error", "404 Not Find");
                }

            } else {
                //错误处理
                responseJson.put("error", "404 Not Find");
            }

            //向客户端发送结果
            ResponseJson(ctx,req,responseJson.toString());
        }
    }

    /**
     * 响应HTTP的请求
     * @param ctx
     * @param req
     * @param jsonStr
     */
    private void ResponseJson(ChannelHandlerContext ctx, FullHttpRequest req ,String jsonStr)
    {

        boolean keepAlive = HttpUtil.isKeepAlive(req);
        byte[] jsonByteByte = jsonStr.getBytes();
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(jsonByteByte));
        response.headers().set(CONTENT_TYPE, "text/json");
        response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());

        if (!keepAlive) {
            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
        } else {
            response.headers().set(CONNECTION, KEEP_ALIVE);
            ctx.write(response);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 获取请求的内容
     * @param request
     * @return
     */
    private String parseJosnRequest(FullHttpRequest request) {
        ByteBuf jsonBuf = request.content();
        String jsonStr = jsonBuf.toString(CharsetUtil.UTF_8);
        return jsonStr;
    }
}

  关于服务的运行与测试,与Java EE容器没有关系,也就是说不需要Tomcat、JBoss等容器服务支持,是完全独立的JVM应用。

  在Eclipse 中直接Run as Java Application,就可以开启Netty的服务。Netty 的服务不需要放到任何容器中,可以单独运行。
这里写图片描述
参考:
《基于Java内置的HttpServer实现轻量级Restful》 CSDN博客 肖永威 2018年3月
《HTML(JS)+SOA+MongoDB简易架构实践经验》 CSDN博客 肖永威 2016年6月
《使用云技术升级改造现有应用系统的思考》 CSDN博客 肖永威 2013年11月
《Netty——基本使用介绍》 CSDN博客 隔壁老王的专栏 2016年11月
《Netty的restful API 简单实现和部署》 CSDN博客 super_zhan 2016年11月
《Nesty 高性能轻量级Http Restful Server》 CSDN博客 曦轩 2016年4月

猜你喜欢

转载自blog.csdn.net/xiaoyw/article/details/80868575