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

相关文章

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

HTTP 与 SpringBoot 参数提交与接收协议方式

《HTTP与SpringBoot参数提交与接收协议方式》HTTP参数提交方式包括URL查询、表单、JSON/XML、路径变量、头部、Cookie、GraphQL、WebSocket和SSE,依据... 目录HTTP 协议支持多种参数提交方式,主要取决于请求方法(Method)和内容类型(Content-Ty

Python 基于http.server模块实现简单http服务的代码举例

《Python基于http.server模块实现简单http服务的代码举例》Pythonhttp.server模块通过继承BaseHTTPRequestHandler处理HTTP请求,使用Threa... 目录测试环境代码实现相关介绍模块简介类及相关函数简介参考链接测试环境win11专业版python

Python实战之SEO优化自动化工具开发指南

《Python实战之SEO优化自动化工具开发指南》在数字化营销时代,搜索引擎优化(SEO)已成为网站获取流量的重要手段,本文将带您使用Python开发一套完整的SEO自动化工具,需要的可以了解下... 目录前言项目概述技术栈选择核心模块实现1. 关键词研究模块2. 网站技术seo检测模块3. 内容优化分析模

使用Python的requests库来发送HTTP请求的操作指南

《使用Python的requests库来发送HTTP请求的操作指南》使用Python的requests库发送HTTP请求是非常简单和直观的,requests库提供了丰富的API,可以发送各种类型的HT... 目录前言1. 安装 requests 库2. 发送 GET 请求3. 发送 POST 请求4. 发送

基于Java开发一个极简版敏感词检测工具

《基于Java开发一个极简版敏感词检测工具》这篇文章主要为大家详细介绍了如何基于Java开发一个极简版敏感词检测工具,文中的示例代码简洁易懂,感兴趣的小伙伴可以跟随小编一起学习一下... 目录你是否还在为敏感词检测头疼一、极简版Java敏感词检测工具的3大核心优势1.1 优势1:DFA算法驱动,效率提升10

Python开发简易网络服务器的示例详解(新手入门)

《Python开发简易网络服务器的示例详解(新手入门)》网络服务器是互联网基础设施的核心组件,它本质上是一个持续运行的程序,负责监听特定端口,本文将使用Python开发一个简单的网络服务器,感兴趣的小... 目录网络服务器基础概念python内置服务器模块1. HTTP服务器模块2. Socket服务器模块