本文主要是介绍Netty的HTTP协议开发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Netty的HTTP协议开发
由于netty天生是异步事件驱动的架构,因此基于NIO TCP协议栈开发的HTTP协议栈也是异步非阻塞的。
Netty的HTTP协议栈无论在性能还是可靠性上,都表现优异,非常适合在非web容器的场景下应用,相比于传统的tomcat,jetty等web容器,它更轻量和小巧。
一.HTTP服务端开发
1.1 HttpFileServer实现
package http;
import marshalling.SubReqClient;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
importio.netty.channel.nio.NioEventLoopGroup;
importio.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
importio.netty.handler.codec.http.HttpObjectAggregator;
importio.netty.handler.codec.http.HttpRequestDecoder;
importio.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
/*
*HTTP服务端
*/
public class HttpFileServer {
privatestatic final String DEFAULT_URL="/src/com/phei/netty/";
publicvoid run(final int port,final Stinr url) throws Exception {
EventLoopGroupbossGroup =new NioEventLoopGroup();
EventLoopGroupworkerGroup =new NioEventLoopGroup();
try{
ServerBootstrapb=new ServerBootstrap();
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(newChannelInitializer<SocketChannel>() {
@Override
protectedvoid initChannel(SocketChannel ch) throws Exception{
//添加HTTP请求消息解码器
ch.pipeline().addLast("http-decoder",newHttpRequestDecoder());
//添加HttpObjectAggregator解码器,作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse
//原因是HTTP解码器在每个HTTP消息中会生成多个消息对象
ch.pipeline().addLast("http-aggregator",newHttpObjectAggregator(65536));
//新增HTTP响应编码器,对HTTP响应消息进行编码
ch.pipeline().addLast("http-encoder",newHttpRequestEncoder());
// 新增Chunked handler,主要作用是支持异步发送大的码流(例如大的文件传输),
//但不占用过多的内存,防止发生java内存溢出错误
ch.pipeline().addLast("http-chunked",newChunkedWriteHandler());
//添加HttpFileServerHandler,用于文件服务器的业务逻辑处理
ch.pipeline().addLast("fileServerHandler",newHttpFileServerHandler(url));
}
});
ChannelFuturefuture=b.bind("192.168.1.102",port).sync();
System.out.println("http文件目录服务器启动,网址是:"+http://192.168.1.102:+"+port+url);
future.channel().closeFuture().sync();
}catch (Exception e) {
//TODO: handle exception
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
publicstatic void main(String[] args) throws Exception{
intport=8080;
if(args!=null&&args.length>0){
try{
port=Integer.valueOf(args[0]);
}catch (NumberFormatException e) {
//TODO: handle exception
e.printStackTrace();
}
}
Stringurl=DEFAULT_URL;
if(args.length>1)
url=args[1];
newHttpFileServer().run(port,url);
}
}
首先我们看main函数,它有2个参数:第一个端口,第一个是HTTP服务端的URL路径。如果启动的时候没有配置,则使用默认值,默认端口是8080,默认的URL“/src/com/phei/netty”。
向ChannelPipeline中添加HTTP消息解码器,随后又添加了HttpObjectAggregator解码器,它的作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,原因是HTTP解码器在每个HTTP消息中会生成多个消息对象:HttpRequest/HttpResponse,HttpContent,
LastHttpContent
新增HTTP响应解码器,对HTTP响应消息进行解码,新增Chunked handler,它的主要作用是支持异步发送大的码流(例如大的文件传输),但不占用过多内存,防止发送java内存溢出错误。
最后添加HttpFileServerHandler,用于文件服务器的业务逻辑处理。
1.2 HttpFileServerHandler实现
package http;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
importio.netty.channel.ChannelFutureListener;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.ChannelProgressiveFuture;
importio.netty.channel.ChannelProgressiveFutureListener;
importio.netty.handler.codec.http.FullHttpResponse;
importio.netty.handler.codec.http.HttpHeaders;
importio.netty.handler.codec.http.HttpResponse;
importio.netty.handler.codec.http.LastHttpContent;
import io.netty.util.CharsetUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
importjava.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;
public class HttpFileServerHandler extends
SimpleChannelInboundHandler<FullHttpRequest>{
private final String url;
public HttpFileServerHandler(String url){
this.url=url;
}
@Override
public void messageReceived(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception{
//对HTTP请求消息的解码结果进行判断,如果解码失败,直接构造HTTP400错误返回
if(!request.getDecoderResult().isSucccess()){
sendError(ctx,BAD_REQUEST);
return;
}
// 判断请求行中的方法,如果不是GET,则构造HTTP405错误返回
if(request.getMethod()!=GET){
sendError(ctx,METHOD_NOT_ALLOWED);
return;
}
final String url=request.getUri();
final String path=sanitizeUri(uri);
// 如果构造的URI不合法,则返回HTTP403错误
if(path==null){
sendError(ctx,FORBIDDEN);
return;
}
// 使用新组装的URI路径构造File对象
File file=new File(path);
// 如果文件不存在或者是系统隐藏文件,则构造HTTP404异常返回
if(file.isHidden()||!file.exists()){
sendError(ctx,NOT_FOUND);
return;
}
// 如果文件是目录,则发送目录的链接给客户端浏览器
if(file.isDirectory()){
if(uri.endsWith("/")){
sendListing(ctx,file);
} else{
sendRedirect(ctx,uri+'/');
}
return;
}
if(!file.isFile()){
sendError(ctx,FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile=null;
try {
randomAccessFile=newRandomAccessFile(file,"r"); // 以只读的方式打开文件
}catch (FileNotFoundException e) {
//TODO: handle exception
sendError(ctx,NOT_FOUND);
return;
}
// 获取文件的长度,构造成功的HTTP应答消息
long fileLength=randomAccessFile.length();
HttpResponse response=newDefaultHttpResponse(HTTP_1_1,OK);
setContentLength(response,fileLength);
// 在消息头设置contentlength和content type
setContentTypeHeader(reponse,file);
// 判断是否是keep-alive 如果是,则在应答消息头设置connection为keep-alive
if(isKeepAlive(request)) {
response.headers().set(CONNECTION,HttpHeaders.values.KEEP_ALIVE);
}
// 发送响应消息
ctx.write(response);
// 通过netty的chunkedfile对象直接将文件写入到发送缓冲区中
ChannelFuture sendFileFuture;
sendFileFuture=ctx.write(newChunkedFile(randomAccessFile,0,fileLength,8192,
ctx.newProgressivePromise()));
// 为sendFileFuture增加GenericFutureListner
sendFileFuture.addListener(newChannelProgressiveFutureListener() {
@Override
public voidoperationprogressed(ChannelProgressiveFuture future,
long progress,long total) {
if(total<0) {
System.err.println("Transferprogress:"+progress);
} else{
System.err.println("Transferprogress:"+progress+"/"+total);
}
}
// 如果发送完成,打印"Transfer complete"
@Override
public voidoperationComplete(ChannelProgressiveFuture future) throws Exception{
System.out.println("Transfercomplete:");
}
});
// 如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttp的EMPLY_LAST_CONTENT发送到缓冲区
// 标识所有的消息体已经发送完成,同时调用flush方法将之前在发送缓冲区的消息刷新到SocketChannel中发送给对方
ChannelFuturelastContentFuture=ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
// 如果是非keey-alive,最后一包消息发送完成之后,服务端要主动关闭连接
if(!isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause)throw Exception{
cause.printStackTrace();
if(ctx.channel().isActive()){
sendError(ctx,INTERNAL_SERVER_ERROR);
}
}
private static final PatternINSECURE_URI=Pattern.compile("."[<>&\]."");
private String sanitizeUri(String uri){
try{
// 解码
uri=URLDecoder.decode(uri,"UTF-8");
}catch(UnsupportedEncodingException e){
try {
uri=URLDecoder.decode(uri,"ISO-8859-1");
}catch (Exception e2) {
//TODO: handle exception
thrownew Error();
}
}
// 解码成功后对URI进行合法性判断,如果URI与允许访问的URI一致或者是其子目录(文件)则校验通过,
// 否则返回空
if(!uri.startsWith(url)){
return null;
}
if(!url.startsWith("/")) {
return null;
}
// 将硬编码的文件路径分隔符替换为本地操作系统的文件路径分隔符
uri=uri.replace('/', File.separatorChar);
// 对新的URI作二次合法性校验,如果校验失败则返回空.
if(uri.contains(File.separator+'.')||uri.contains('.'+File.separator)
||uri.startsWith(".")||uri.endsWith(".")||INSECURE_URI.matches(uri,matches)){
return null;
}
// 对文件进行拼接,使用当前运行程序所在的工程目录+URI构造绝对路径返回
returnSystem.getProperty("user.dir")+File.separator+uri;
}
private static final PatternALLOWED_FILE_NAME=Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
private static void sendListing(ChannelHandlerContext ctx,File dir){
// 创建成功的HTTP响应消息
FullHttpResponse response=newDefaultFullHttpResponse(HTTP_1_1,OK);
// 设置消息头类型为"text/html;charset=UTF-8"
response.headers().set(CONTENT_TYPE,"text/html;charset-UTF-8");
StringBuilder buf=new StringBuilder();
String dirPath=dir.getPath();
buf.append("<!DOCTYPEhtml>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append("目录:");
buf.append("</title></head><body>\r\n");
buf.append("<h3>");
buf.append(dirPath).append("目录:");
buf.append("</h3>\r\n");
buf.append("<url>");
buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
// 用于展示根目录下的所有文件和文件夹,同时使用超链接来标识
for(File f:dir.listFiles()){
if(f.isHidden()||!f.canRead()){
continue;
}
}
String name=f.getName();
if(!ALLOWED_FILE_NAME.matcher(name).matches()){
continue;
}
buf.append("<li>链接:<a href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.append("</a></li>\r\n");
}
buf.append("</ul></body></html>\r\n");
// 分配消息的缓冲对象,
ByteBuf buffer=Unpooled.copleBuffer(buf,CharsetUtil.UTF-8);
response.content().writeBytes(buffer);
// 释放缓冲区
buffer.release();
// 将缓冲区的响应消息发送到缓冲区并刷新到SocketChannel中
ctx.writeAndFlush(reponse).addLisener(ChannelFutureLisener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx,StringnewUri) {
FullHttpResponse response=newDefaultFullHttpResponse(HTTP_1_1,FOUND);
response.headers().set(LOCATION,newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContextctx,HttpResponseStatus status){
FullHttpResponse response=newDefaultFullHttpResponse
(HTTP_1_1,status,Unpooled.copiedBuffer("Failure:"+status.toString()+"\r\n",CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE,"text/plain;charset-UTF-8");
ctx.writeAndFlush(response).addLisener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response,Filefile){
MimetypeFileTypeMap mimeTypeMap=newMimetypeFileTypeMap();
response.headers().
set(CONTENT_TYPE,mimeTypeMap.getContentType(file.getPaht));
}
}
首先对HTTP请求消息的解码结果进行判断,如果解码失败,直接构造HTTP 400错误返回。对请求行中的方法进行判断,如果不是从浏览器或者表单设置为Get发起的请求(例如post),则构造http 405错误返回。
对请求URL进行包装,然后对sanitizeUtil方法展开分析。首先使用JDK的URLDecoder进行编码,使用UTF-8字符集,解码成功之后对URI进行合法性判断。如果URI与允许访问的URI一致或者其子目录,则校验通过,否则返回空。
如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttpContent的EMPTY_LAST_CONTENT发送到缓冲区中,标识所有的消息体已经发送完成,同时调用flush方法将之前在发送缓冲区的消息刷新到SocketChannel中发送给对方。
如果是非Keep-Alive的,最后一包消息发送完成之后,服务端要主动关闭连接。
这篇关于Netty的HTTP协议开发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!