最近一直在用Netty做一个数据服务,此服务的主要功能是接收客户端的Http请求并解析,经过业务逻辑处理后访问HBase获取数据流,然后返回给客户端。在服务的初始开发阶段,重心全部在如何设计Netty服务端的线程模型和缓存方案从而能更快的响应客户端请求,提高服务端的吞吐量,经过两个月的开发调试,性能基本达到要求。在核心功能基本可用后,一直在思考如何提供一个服务端的状态展现和管理动态页面。虽然说之前用过jsp和php开发过很简单的网站,当时仅限自己玩玩,没有工程应用,因此没有深入理解,后续的学习重心一直在服务端,并没有对服务端容器和网站这整个技术流程梳理清楚,所以一直不知道如何才能实现在基于Netty开发的服务器提供展现和管理动态页面。
经过两个星期的上班时完善并压测核心功能,下班有空后了解Netty如何整合页面显示,慢慢感觉有了眉目。当使用浏览器访问一个URL时,服务端接收到请求,将此请求映射的html文件发送给浏览器,浏览器解析html文件,发现里面引用了一些静态文件,例如图片、javascript和css文件等,会继续向服务端请求这些静态文件,然后在浏览器端就显示出来了这个页面。之前一直纠结于tomcat下网站开发使用的jsp技术,始终没想明白当在浏览器中输入URL后,到底是如何显示出页面的。大体来说,浏览器发送Url后,tomcat会负责将jsp转换为html,然后将这个html发送给浏览器,我关心的重点应该是怎么渲染出html文件。经过了解,针对HTML生成前后端的不同,主要分为这么两种:
后端渲染(SSR、服务端渲染)
后端渲染HTML的情况下,浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,这里的计算就是服务器经过解析存放在服务器端的模板文件来完成的,在这种情况下,浏览器只进行了HTML的解析,以及通过操作系统提供的操纵显示器显示内容的系统调用在显示器上把HTML所代表的图像显示给用户。
前端渲染(SPA、单页面应用)
前端渲染就是指浏览器会从后端得到一些信息,这些信息或许是适用于题主所说的angularjs的模板文件,亦或是JSON等各种数据交换格式所包装的数据,甚至是直接的合法的HTML字符串。这些形式都不重要,重要的是,将这些信息组织排列形成最终可读的HTML字符串是由浏览器来完成的,在形成了HTML字符串之后,再进行显示。
所以,如果我想让Netty能提供展现和管理动态页面,只需要选择是前端渲染还是后端渲染,然后响应浏览器的静态文件请求,将html、javascript、css、图片等静态文件发送给浏览器即可,浏览器会负责显示出页面。通过咨询同事,发现vue这种支持前端渲染,随即进行了尝试。相关代码如下:
VUE前端页面代码,index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vue.js 导航菜单</title>
<script src="js/vue.min.js"></script>
<style>
*{
margin:0;
padding:0;
}
body{
font:15px/1.3 'Open Sans', sans-serif;
color: #5e5b64;
text-align:center;
}
a, a:visited {
outline:none;
color:#389dc1;
}
a:hover{
text-decoration:none;
}
section, footer, header, aside, nav{
display: block;
}
/*-------------------------
菜鸟
--------------------------*/
nav{
display:inline-block;
margin:60px auto 45px;
background-color:#5597b4;
box-shadow:0 1px 1px #ccc;
border-radius:2px;
}
nav a{
display:inline-block;
padding: 18px 30px;
color:#fff !important;
font-weight:bold;
font-size:16px;
text-decoration:none !important;
line-height:1;
text-transform: uppercase;
background-color:transparent;
-webkit-transition:background-color 0.25s;
-moz-transition:background-color 0.25s;
transition:background-color 0.25s;
}
nav a:first-child{
border-radius:2px 0 0 2px;
}
nav a:last-child{
border-radius:0 2px 2px 0;
}
nav.home .home,
nav.projects .projects,
nav.services .services,
nav.contact .contact{
background-color:#e35885;
}
p{
font-size:22px;
font-weight:bold;
color:#7d9098;
}
p b{
color:#ffffff;
display:inline-block;
padding:5px 10px;
background-color:#c4d7e0;
border-radius:2px;
text-transform:uppercase;
font-size:18px;
}
</style>
</head>
<body>
<div id="main">
<!-- 激活的菜单样式为 active 类 -->
<!-- 为了阻止链接在点击时跳转,我们使用了 "prevent" 修饰符 (preventDefault 的简称)。 -->
<nav v-bind:class="active" v-on:click.prevent>
<!-- 当菜单上的链接被点击时,我们调用了 makeActive 方法, 该方法在 Vue 实例中创建。 -->
<a href="#" class="home" v-on:click="makeActive('home')">Home</a>
<a href="#" class="projects" v-on:click="makeActive('projects')">Projects</a>
<a href="#" class="services" v-on:click="makeActive('services')">Services</a>
<a href="#" class="contact" v-on:click="makeActive('contact')">Contact</a>
</nav>
<!-- 以下 "active" 变量会根据当前选中的值来自动变换 -->
<p>您选择了 <b>{{active}} 菜单</b></p>
</div>
<script>
// 创建一个新的 Vue 实例
var demo = new Vue({
// DOM 元素,挂载视图模型
el: '#main',
// 定义属性,并设置初始值
data: {
active: 'home'
},
// 点击菜单使用的函数
methods: {
makeActive: function(item){
// 模型改变,视图会自动更新
this.active = item;
}
}
});
</script>
</body>
</html>
Netty后台静态服务器代码:
1、NettyStaticFileServerMain.java
package com.me.io;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
*
* @author linjx
*
*/
public class NettyStaticFileServerMain {
private static int httpListenPort = 6070;
public static void main(String[] args) throws InterruptedException{
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup work = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, work)
.channel(NioServerSocketChannel.class)
.childHandler(new NettyHttpInitializer())
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = serverBootstrap.bind(httpListenPort).sync();
future.channel().closeFuture().sync();
}
finally {
work.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
2、Netty的管道初始化器 NettyHttpInitializer.java:
package com.me.io;
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.stream.ChunkedWriteHandler;
public class NettyHttpInitializer extends ChannelInitializer<SocketChannel> {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//将请求和应答消息编码或解码为HTTP消息
pipeline.addLast(new HttpServerCodec());
//将HTTP消息的多个部分组合成一条完整的HTTP消息
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new NettyHttpStaticFileHandler());
}
}
3、静态文件请求处理器,NettyHttpStaticFileHandler.java
package com.me.io;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import java.io.File;
import java.io.RandomAccessFile;
import javax.activation.MimetypesFileTypeMap;
/**
*
* @author linjx
*
*/
public class NettyHttpStaticFileHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
// 资源所在路径
private static final String STATIC_LOCATION = "C:/Users/linjx/Desktop/code";
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 获取URI
String uri = request.uri();
// 设置不支持favicon.ico文件
if ("/favicon.ico".equals(uri)) {
return;
}
// 根据路径地址构建文件
String path = STATIC_LOCATION + uri;
File html = new File(path);
// 状态为1xx的话,继续请求
if (HttpUtil.is100ContinueExpected(request)) {
send100Continue(ctx);
}
// 当文件不存在的时候,将资源指向NOT_FOUND
if (!html.exists()) {
sendNotFound(ctx);
return;
}
final RandomAccessFile randomAccessFile = new RandomAccessFile(html, "r");
HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
// 设置文件格式内容
if (path.endsWith(".html")){
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
}else if(path.endsWith(".js")){
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/x-javascript");
}else if(path.endsWith(".css")){
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/css; charset=UTF-8");
}else{
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(path));
}
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, randomAccessFile.length());
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
ChannelFuture lastContentFuture;
if (ctx.pipeline().get(SslHandler.class) == null) {
sendFileFuture =
ctx.write(new DefaultFileRegion(randomAccessFile.getChannel(), 0, randomAccessFile.length()), ctx.newProgressivePromise());
// Write the end marker.
lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
} else {
sendFileFuture =
ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(randomAccessFile, 0, randomAccessFile.length(), 10 * 1024 * 1024)),
ctx.newProgressivePromise());
// HttpChunkedInput will write the end marker (LastHttpContent) for us.
lastContentFuture = sendFileFuture;
}
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
if (total < 0) { // total unknown
System.err.println(future.channel() + " Transfer progress: " + progress);
} else {
System.out.println(future.channel() + " Transfer progress: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) {
System.out.println(future.channel() + " Transfer complete.");
}
});
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
private static void sendNotFound(ChannelHandlerContext ctx){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
ctx.writeAndFlush(response);
}
}
运行结果如下:
启动Netty服务器后,在浏览器中输入url后,便可以显示出经过vue前端渲染出比较好看的页面了。页面与netty服务端的数据交互可以使用ajax。