谈谈基于Netty实现Restful搭建服务架构思路
自从使用商用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月
上一篇: matlab基础