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的,最后一包消息发送完成之后,服务端要主动关闭连接。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值