欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-12 20:31:50
...

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

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

  前端使用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实现Restful搭建服务架构思路

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

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

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

  为什么考虑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 简单实现和部署》
谈谈基于Netty实现Restful搭建服务架构思路

  新建一个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 的服务不需要放到任何容器中,可以单独运行。
谈谈基于Netty实现Restful搭建服务架构思路
参考:
《基于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月