Netty的HTTP协议开发

2024-06-23 12:32
文章标签 协议 开发 http netty

本文主要是介绍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协议开发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1087168

相关文章

如何在Spring Boot项目中集成MQTT协议

《如何在SpringBoot项目中集成MQTT协议》本文介绍在SpringBoot中集成MQTT的步骤,包括安装Broker、添加EclipsePaho依赖、配置连接参数、实现消息发布订阅、测试接口... 目录1. 准备工作2. 引入依赖3. 配置MQTT连接4. 创建MQTT配置类5. 实现消息发布与订阅

springboot如何通过http动态操作xxl-job任务

《springboot如何通过http动态操作xxl-job任务》:本文主要介绍springboot如何通过http动态操作xxl-job任务的问题,具有很好的参考价值,希望对大家有所帮助,如有错... 目录springboot通过http动态操作xxl-job任务一、maven依赖二、配置文件三、xxl-

SpringBoot开发中十大常见陷阱深度解析与避坑指南

《SpringBoot开发中十大常见陷阱深度解析与避坑指南》在SpringBoot的开发过程中,即使是经验丰富的开发者也难免会遇到各种棘手的问题,本文将针对SpringBoot开发中十大常见的“坑... 目录引言一、配置总出错?是不是同时用了.properties和.yml?二、换个位置配置就失效?搞清楚加

使用Python进行GRPC和Dubbo协议的高级测试

《使用Python进行GRPC和Dubbo协议的高级测试》GRPC(GoogleRemoteProcedureCall)是一种高性能、开源的远程过程调用(RPC)框架,Dubbo是一种高性能的分布式服... 目录01 GRPC测试安装gRPC编写.proto文件实现服务02 Dubbo测试1. 安装Dubb

Python中对FFmpeg封装开发库FFmpy详解

《Python中对FFmpeg封装开发库FFmpy详解》:本文主要介绍Python中对FFmpeg封装开发库FFmpy,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录一、FFmpy简介与安装1.1 FFmpy概述1.2 安装方法二、FFmpy核心类与方法2.1 FF

基于Python开发Windows屏幕控制工具

《基于Python开发Windows屏幕控制工具》在数字化办公时代,屏幕管理已成为提升工作效率和保护眼睛健康的重要环节,本文将分享一个基于Python和PySide6开发的Windows屏幕控制工具,... 目录概述功能亮点界面展示实现步骤详解1. 环境准备2. 亮度控制模块3. 息屏功能实现4. 息屏时间

Python实例题之pygame开发打飞机游戏实例代码

《Python实例题之pygame开发打飞机游戏实例代码》对于python的学习者,能够写出一个飞机大战的程序代码,是不是感觉到非常的开心,:本文主要介绍Python实例题之pygame开发打飞机... 目录题目pygame-aircraft-game使用 Pygame 开发的打飞机游戏脚本代码解释初始化部

使用Python开发一个现代化屏幕取色器

《使用Python开发一个现代化屏幕取色器》在UI设计、网页开发等场景中,颜色拾取是高频需求,:本文主要介绍如何使用Python开发一个现代化屏幕取色器,有需要的小伙伴可以参考一下... 目录一、项目概述二、核心功能解析2.1 实时颜色追踪2.2 智能颜色显示三、效果展示四、实现步骤详解4.1 环境配置4.

Maven 配置中的 <mirror>绕过 HTTP 阻断机制的方法

《Maven配置中的<mirror>绕过HTTP阻断机制的方法》:本文主要介绍Maven配置中的<mirror>绕过HTTP阻断机制的方法,本文给大家分享问题原因及解决方案,感兴趣的朋友一... 目录一、问题场景:升级 Maven 后构建失败二、解决方案:通过 <mirror> 配置覆盖默认行为1. 配置示

Python使用smtplib库开发一个邮件自动发送工具

《Python使用smtplib库开发一个邮件自动发送工具》在现代软件开发中,自动化邮件发送是一个非常实用的功能,无论是系统通知、营销邮件、还是日常工作报告,Python的smtplib库都能帮助我们... 目录代码实现与知识点解析1. 导入必要的库2. 配置邮件服务器参数3. 创建邮件发送类4. 实现邮件