10.2.1 HTTP 服务端例程场景描述
我 们 以 文 件 服 务 器 为 例 学 习 Netty 的 HTTP 服 务 端 入 门 开 发 , 例 程 场 景 如 下 : 文 件 服务 器 使 用 HTTP 协 议 对 外 提 供 服 务 , 当 客 户 端 通 过 浏 览 器 访 问 文 件 服 务 器 时 , 对 访 问 路 径行 检 查 , 检 查 失 败 时 返 回 HTTP 403 错 误 , 该 页 无 法 访 问 ; 如 果 校 验 通 过 , 以 链 接 的 方弋 打 开 当 前 文 件 目 录 , 每 个 目 录 或 者 文 件 都 是 个 超 链 接 , 可 以 递 归 访 问 。
10.2.2 HTTP服务器端开发
本文主要是写一个入门程序让大家了解netty的强大。本文主要编写一个基于http协议的文件服务器,可以使用浏览器来访问服务器的目录,下载文件等。如果不熟悉http协议可以先了解http相应相关的内容。
HttpFileServer.java
package com.viagra.chapter10.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @Auther: viagra
* @Date: 2019/8/2 10:25
* @Description:
*/
public class HttpFileServer {
private final int port=80;
private final String localDir="d:/";
public void run() throws Exception{
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
EventLoopGroup clientGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBookstrap = new ServerBootstrap();
serverBookstrap.group(acceptorGroup, clientGroup).channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childHandler(new ChannelInitializer<SocketChannel>(){
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast("http-decoder",new HttpRequestDecoder());
sc.pipeline().addLast("http-aggregator",new HttpObjectAggregator(64*1024));
sc.pipeline().addLast("http-encoder",new HttpResponseEncoder());
sc.pipeline().addLast("http-handler",new HttpFileServerHandler(localDir));
}
});
ChannelFuture channelFuture = serverBookstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
acceptorGroup.shutdownGracefully();
clientGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new HttpFileServer().run();
}
}
Netty提类似linux管道机制的使用方法,可以向pipeline添加多个ChannelHandler,Netty会依次调用添加的handler来处理,这个很像java web中的servlet和filter的概念。
上述代码的调用顺序是:
1.调用HttpRequestDecoder将ByteBuf转成HttpRequest和HttpContent对象。即将字节流缓存对象解码成pojo对象
2.调用HttpObjectAggregator将HttpRequest和它跟着的HttpContent对象聚合成一个对象FullHttpRequest
3.调用HttpRequestHandler来处理真正的业务逻辑,在HttpRequestHandler里面就可以获取到FullHttpRequest对象。
4.调用HttpResponseEncoder将HttpRequestHandler写出的FullHttpResponse对象编码成字节流。
我们在声明ServerBootstrap服务器引导对象的时候传入了两个EventLoopGroup对象这两个对象有什么作用呢?
groupA为acceptorGroup组,groupB为clientGroup组。groupA的作用是接受连接过来的客户端,并把他们交给groupB,groupB负责具体的请求处理。如果一个group既负责接受连接也负责处理连接后的会话,那么在请求很大的时候一个group会成为瓶颈,会导致一些连接的超时,如果将两个功能分离开来,就不会有这个问题。
HttpFileServerHandler.java
package com.viagra.chapter10.http;
import java.io.File;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import javax.activation.MimetypesFileTypeMap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
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.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;
/**
* @Auther: viagra
* @Date: 2019/8/2 10:38
* @Description:
*/
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
private static final String CRLF = "\r\n";
private String localDir;
private static SimpleDateFormat sdf=new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
public HttpFileServerHandler(String localDir){
this.localDir=localDir;
}
public static final HttpVersion HTTP_1_1 = new HttpVersion("HTTP", 1, 1, true);
@Override
public void messageReceived(ChannelHandlerContext ctx, FullHttpRequest req)throws Exception {
//解码不成功
if(!req.decoderResult().isSuccess())
{
sendErrorToClient(ctx,HttpResponseStatus.BAD_REQUEST);
return ;
}
if(req.method().compareTo(HttpMethod.GET)!=0){
sendErrorToClient(ctx,HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
}
String uri=req.uri();
uri=URLDecoder.decode(uri, "utf-8");
String filePath=getFilePath(uri);
File file=new File(filePath);
//如果文件不存在
if(!file.exists()){
sendErrorToClient(ctx,HttpResponseStatus.NOT_FOUND);
return;
}
//如果是目录,则显示子目录
if(file.isDirectory()){
sendDirListToClient(ctx,file,uri);
return;
}
//如果是文件,则将文件流写到客户端
if(file.isFile()){
sendFileToClient(ctx,file,uri);
return;
}
ctx.close();
}
public String getFilePath(String uri) throws Exception{
return localDir+uri;
}
private void sendErrorToClient(ChannelHandlerContext ctx,HttpResponseStatus status) throws Exception{
ByteBuf buffer=Unpooled.copiedBuffer(("系统服务出错:"+status.toString()+CRLF).getBytes("utf-8"));
FullHttpResponse resp=new DefaultFullHttpResponse(HTTP_1_1,status,buffer);
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
}
private void sendDirListToClient(ChannelHandlerContext ctx, File dir,String uri) throws Exception {
StringBuffer sb=new StringBuffer("");
String dirpath=dir.getPath();
sb.append("<!DOCTYPE HTML>"+CRLF);
sb.append("<html><head><title>");
sb.append(dirpath);
sb.append("目录:");
sb.append("</title></head><body>"+CRLF);
sb.append("<h3>");
sb.append("当前目录:"+dirpath);
sb.append("</h3>");
sb.append("<table>");
sb.append("<tr><td colspan='3'>上一级:<a href=\"../\">..</a> </td></tr>");
if(uri.equals("/")){
uri="";
}else
{
if(uri.charAt(0)=='/'){
uri=uri.substring(0);
}
uri+="/";
}
String fnameShow;
for (File f:dir.listFiles()) {
if(f.isHidden()||!f.canRead()){
continue;
}
String fname=f.getName();
Calendar cal=Calendar.getInstance();
cal.setTimeInMillis(f.lastModified());
String lastModified=sdf.format(cal.getTime());
sb.append("<tr>");
if(f.isFile()){
fnameShow="<font color='green'>"+fname+"</font>";
}else
{
fnameShow="<font color='red'>"+fname+"</font>";
}
sb.append("<td style='width:200px'> "+lastModified+"</td><td style='width:100px'>"+Files.size(f.toPath())+"</td><td><a href=\""+uri+fname+"\">"+fnameShow+"</a></td>");
sb.append("</tr>");
}
sb.append("</table>");
ByteBuf buffer=Unpooled.copiedBuffer(sb.toString(),CharsetUtil.UTF_8);
FullHttpResponse resp=new DefaultFullHttpResponse(HTTP_1_1,HttpResponseStatus.OK,buffer);
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
}
private void sendFileToClient(ChannelHandlerContext ctx, File file, String uri) throws Exception {
ByteBuf buffer=Unpooled.copiedBuffer(Files.readAllBytes(file.toPath()));
FullHttpResponse resp=new DefaultFullHttpResponse(HTTP_1_1,HttpResponseStatus.OK,buffer);
MimetypesFileTypeMap mimeTypeMap=new MimetypesFileTypeMap();
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypeMap.getContentType(file));
ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception {
cause.printStackTrace();
ctx.close();
}
}
HttpRequestHandler
继承SimpleChannelInboundHandler
类,当然也可以继承ChannelHandlerAdapter
类,其实SimpleChannelInboundHandler
就是ChannelHandlerAdapter
的子类。
对于SimpleChannelInboundHandler有以下需要注意的 :
1.它可以指定只处理某一种类型的消息
2.根据SimpleChannelInboundHandler的构造函数
protected SimpleChannelInboundHandler(boolean autoRelease)
它会自动释放已经处理过的消息。
3.根据实现messageReceived抽象方法。
在messageReceived方法中我们要实现我们具体的业务逻辑:
我们的http文件服务器的需求如下:
1.显示当前的路径下的目录和文件,点击文件就下载该文件,点击目录则切换到改目录下面。
2.提供返回上一级功能。
3.简单的异常处理。
运行结果:
注意:
sendFileToClient方法中直接将文件的字节流写到FullHttpResponse中,在文件不大的情况下可以这样做,在文件比较大的情况下这样做很容易出现内存溢出之类的问题。需要考虑使用http trunked协议机制来传输大文件。这个后面会继续说。
运行的时候把HttpFileServer.java中localDir改成你自己的目录即可。