Netty的HTTP协议开发

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

本文主要是介绍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

相关文章

SpringBoot集成Netty,Handler中@Autowired注解为空

最近建了个技术交流群,然后好多小伙伴都问关于Netty的问题,尤其今天的问题最特殊,功能大概是要在Netty接收消息时把数据写入数据库,那个小伙伴用的是 Spring Boot + MyBatis + Netty,所以就碰到了Handler中@Autowired注解为空的问题 参考了一些大神的博文,Spring Boot非controller使用@Autowired注解注入为null的问题,得到

Eclipse+ADT与Android Studio开发的区别

下文的EA指Eclipse+ADT,AS就是指Android Studio。 就编写界面布局来说AS可以边开发边预览(所见即所得,以及多个屏幕预览),这个优势比较大。AS运行时占的内存比EA的要小。AS创建项目时要创建gradle项目框架,so,创建项目时AS比较慢。android studio基于gradle构建项目,你无法同时集中管理和维护多个项目的源码,而eclipse ADT可以同时打开

探索蓝牙协议的奥秘:用ESP32实现高质量蓝牙音频传输

蓝牙(Bluetooth)是一种短距离无线通信技术,广泛应用于各种电子设备之间的数据传输。自1994年由爱立信公司首次提出以来,蓝牙技术已经经历了多个版本的更新和改进。本文将详细介绍蓝牙协议,并通过一个具体的项目——使用ESP32实现蓝牙音频传输,来展示蓝牙协议的实际应用及其优点。 蓝牙协议概述 蓝牙协议栈 蓝牙协议栈是蓝牙技术的核心,定义了蓝牙设备之间如何进行通信。蓝牙协议

Python应用开发——30天学习Streamlit Python包进行APP的构建(9)

st.area_chart 显示区域图。 这是围绕 st.altair_chart 的语法糖。主要区别在于该命令使用数据自身的列和指数来计算图表的 Altair 规格。因此,在许多 "只需绘制此图 "的情况下,该命令更易于使用,但可定制性较差。 如果 st.area_chart 无法正确猜测数据规格,请尝试使用 st.altair_chart 指定所需的图表。 Function signa

【杂记-浅谈DHCP动态主机配置协议】

DHCP动态主机配置协议 一、DHCP概述1、定义2、作用3、报文类型 二、DHCP的工作原理三、DHCP服务器的配置和管理 一、DHCP概述 1、定义 DHCP,Dynamic Host Configuration Protocol,动态主机配置协议,是一种网络协议,主要用于在IP网络中自动分配和管理IP地址以及其他网络配置参数。 2、作用 DHCP允许计算机和其他设备通

WDF驱动开发-WDF总线枚举(一)

支持在总线驱动程序中进行 PnP 和电源管理 某些设备永久插入系统,而其他设备可以在系统运行时插入和拔出电源。 总线驱动 必须识别并报告连接到其总线的设备,并且他们必须发现并报告系统中设备的到达和离开情况。 总线驱动程序标识和报告的设备称为总线的 子设备。 标识和报告子设备的过程称为 总线枚举。 在总线枚举期间,总线驱动程序会为其子 设备创建设备对象 。  总线驱动程序本质上是同时处理总线枚

JavaWeb系列六: 动态WEB开发核心(Servlet) 上

韩老师学生 官网文档为什么会出现Servlet什么是ServletServlet在JavaWeb项目位置Servlet基本使用Servlet开发方式说明快速入门- 手动开发 servlet浏览器请求Servlet UML分析Servlet生命周期GET和POST请求分发处理通过继承HttpServlet开发ServletIDEA配置ServletServlet注意事项和细节 Servlet注

微服务中RPC的强类型检查与HTTP的弱类型对比

在微服务架构中,服务间的通信是一个至关重要的环节。其中,远程过程调用(RPC)和HTTP是两种最常见的通信方式。虽然它们都能实现服务间的数据交换,但在类型检查方面,RPC的强类型检查和HTTP的弱类型之间有着显著的差异。本文将深入探讨这两种通信方式在类型检查方面的优缺点,以及它们对微服务架构的影响。 一、RPC的强类型检查 RPC的强类型检查是其核心优势之一。在RPC通信中,客户端和服务端都使

手把手教你入门vue+springboot开发(五)--docker部署

文章目录 前言一、前端打包二、后端打包三、docker运行总结 前言 前面我们重点介绍了vue+springboot前后端分离开发的过程,本篇我们结合docker容器来研究一下打包部署过程。 一、前端打包 在VSCode的命令行中输入npm run build可以打包前端代码,出现下图提示表示打包完成。 打包成功后会在前端工程目录生成dist目录,如下图所示: 把

Sapphire开发日志 (十) 关于页面

关于页面 任务介绍 关于页面用户对我组工作量的展示。 实现效果 代码解释 首先封装一个子组件用于展示用户头像和名称。 const UserGrid = ({src,name,size,link,}: {src: any;name: any;size?: any;link?: any;}) => (<Box sx={{ display: "flex", flexDirecti