netty解码

netty通过内置处理器HttpRequestDecoder和HttpObjectAggregator对Http请求报文进行解码之后,Netty会将Http请求封装成一个FullHttpRequest实例,然后发送给下一站。

Netty内置的与Http请求报文相对应的类大致有如下几个:

(1)FullHttpRequest:包含整个Http请求的信息,包含对HttpRequest首部和HttpContent请求体的结合。

(2)HttpRequest:请求首部,主要包含对Http请求行和请求头的组合。

(3)HttpContent:对Http请求体进行封装,本质上就是一个ByteBuf缓冲区实例。如果ByteBuf的长度是固定的,则请求体过大,可能包含多个HttpContent。解码的时候,最后一个解码返回对象为LastHttpContent(空的HttpContent),表示对请求体的解码已经结束。

(4)HttpMethod:主要是对Http报文请求头的封装及相关操作。

(5)HttpVersion:对Http版本的封装。

(6)HttpHeaders:包含对http报文请求头的封装及相关操作。



Netty的HttpRequest首部类中有一个String uri成员,主要是对请求uri的封装,该成员包含了Http请求的Path路径与跟随在其后的请求参数。

有关请求参数的解析,不同的Web服务器所使用的解析策略有所不同。在tomcat中,如果客户端提交的是application/x-www-form-urlencoded类型的表单post请求,则java请求参数实例除了包含跟随在uri后面的键-值对之外,请求参数还包含Http请求体body中的键-值对。在netty中,java中请求参数实例仅仅包含跟在uri后面的键-值对。

接下来介绍本文的重点:Netty的Http报文拆包方案。

一般来说,服务端收到的Http字节流可能被分成多个ByteBuf包。Netty服务端如何处理Http报文的分包问题呢?大致有以下几种策略:

(1)定长分包策略:接收端按照固定长度进行分割,发送端按照固定长度发送数据包。

(2)长度域分包策略:比如使用LengthFieldBasedFrameDecoder长度域解码器在接收端分包,而在发送端先发送4个字节表示信息的长度,紧接着发送消息的内容。

(3)分割符分割:比如说使用LineBasedFrameDecoder解码器通过换行符进行分包,或者使用DelimiterBasedFrameDecoder通过特定的分隔符进行分包。

netty结合使用上面第(2)种和第(3)种的策略完成http报文的拆包:对于请求头,应用了分隔符分包的策略,以特定分隔符("\r\n")进行拆包;对于Http请求体,应用长度字段中的分包策略,按照请求头中的内容长度进行内容拆包。

Netty的Http响应编码流程

Netty的Http响应的处理流程只需在流水线装配HttpResponseEncoder编码器即可。该编码器是一个出站处理器,有以下特点:

(1)该编码器输入的是FullHttpResponse响应实例,输出的是ByteBuf字节缓冲器。后面的处理器会将ByteBuf数据写入Channel,最终被发送到Http客户端。

(2)该编码器按照Http对入站FullHttpResponse实例的请求行,请求头,请求体进行序列化,通过请求头去判断是否含有Content-Length头或者Trunked头,然后将请求体按照对应长度规则对内容进行序列化。



如果只是发送简单的Http响应,就可以通过DefaultFullHttpResponse默认响应实现类完成。通过该默认响应类既可以设置响应的内容,又可以进行响应头的设置。

public class HttpProtocolHelper
{
public static final int HTTP_CACHE_SECONDS = 60; public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); public static final AttributeKey<HttpVersion> PROTOCOL_VERSION_KEY =
AttributeKey.valueOf("PROTOCOL_VERSION");
public static final AttributeKey<Boolean> KEEP_ALIVE_KEY =
AttributeKey.valueOf("KEEP_ALIVE_KEY"); /**
* 通过channel 缓存 Http 的协议版本,以及是否为长连接
*
* @param ctx 上下文
* @param request 报文
*/
public static void cacheHttpProtocol(ChannelHandlerContext ctx, final FullHttpRequest request)
{
//每一个连接设置一次即可,不需要重复设置
if (ctx.channel().attr(KEEP_ALIVE_KEY).get() == null)
{
ctx.channel().attr(PROTOCOL_VERSION_KEY).set(request.protocolVersion());
final boolean keepAlive = HttpUtil.isKeepAlive(request);
ctx.channel().attr(KEEP_ALIVE_KEY).set(keepAlive);
}
} public static void setKeepAlive(ChannelHandlerContext ctx, boolean val)
{
ctx.channel().attr(KEEP_ALIVE_KEY).set(val);
} public static String sanitizeUri(String uri, String dir)
{
// Decode the path.
try
{
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e)
{
throw new Error(e);
} if (uri.isEmpty() || uri.charAt(0) != '/')
{
return null;
} // Convert file separators.
uri = uri.replace('/', File.separatorChar); // Simplistic dumb security check.
// You will have to do something serious in the production environment.
if (uri.contains(File.separator + '.') ||
uri.contains('.' + File.separator) ||
uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' ||
INSECURE_URI.matcher(uri).matches())
{
return null;
} // Convert to absolute path.
return dir + File.separator + uri;
} private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*"); public static void sendListing(ChannelHandlerContext ctx, final FullHttpRequest request,
File dir, String dirPath)
{
StringBuilder buf = new StringBuilder()
.append("<!DOCTYPE html>\r\n")
.append("<html><head><meta charset='utf-8' /><title>")
.append("Listing of: ")
.append(dirPath)
.append("</title></head><body>\r\n") .append("<h3>Listing of: ")
.append(dirPath)
.append("</h3>\r\n") .append("<ul>")
.append("<li><a href=\"../\">..</a></li>\r\n"); File[] files = dir.listFiles();
if (files != null)
{
for (File f : files)
{
if (f.isHidden() || !f.canRead())
{
continue;
} String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches())
{
continue;
} buf.append("<li><a href=\"")
.append(name)
.append("\">")
.append(name)
.append("</a></li>\r\n");
}
} buf.append("</ul></body></html>\r\n"); ByteBuf buffer = ctx.alloc().buffer(buf.length());
buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer);
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); sendAndCleanupConnection(ctx, response);
} public static void sendRedirect(ChannelHandlerContext ctx, final FullHttpRequest request, String newUri)
{
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER);
response.headers().set(LOCATION, newUri); sendAndCleanupConnection(ctx, response);
} public static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status)
{
HttpVersion version = getHttpVersion(ctx);
FullHttpResponse response = new DefaultFullHttpResponse(
version, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); sendAndCleanupConnection(ctx, response);
} /**
* 发送普通文本响应
*
* @param ctx 上下文
* @param content 响应内容
*/
public static void sendContent(ChannelHandlerContext ctx, String content)
{
HttpVersion version = getHttpVersion(ctx);
FullHttpResponse response = new DefaultFullHttpResponse(
version, OK, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); sendAndCleanupConnection(ctx, response);
} /**
* 发送html页面响应
*
* @param ctx 上下文
* @param content 响应内容
*/
public static void sendWebPage(ChannelHandlerContext ctx, String content)
{
HttpVersion version = getHttpVersion(ctx);
FullHttpResponse response = new DefaultFullHttpResponse(
version, OK, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); sendAndCleanupConnection(ctx, response);
} /**
* 发送Json格式的响应
*
* @param ctx 上下文
* @param content 响应内容
*/
public static void sendJsonContent(ChannelHandlerContext ctx, String content)
{
HttpVersion version = getHttpVersion(ctx);
/**
* 构造一个默认的FullHttpResponse实例
*/
FullHttpResponse response = new DefaultFullHttpResponse(
version, OK, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8));
/**
* 设置响应头
*/
response.headers().set(CONTENT_TYPE, "application/json; charset=UTF-8");
/**
* 发送响应内容
*/
sendAndCleanupConnection(ctx, response);
} /**
* 发送响应
*/
public static void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response)
{
final boolean keepAlive = ctx.channel().attr(KEEP_ALIVE_KEY).get();
HttpUtil.setContentLength(response, response.content().readableBytes());
if (!keepAlive)
{
// 如果不是长连接,设置 connection:close 头部
response.headers().set(CONNECTION, CLOSE);
} else if (isHTTP_1_0(ctx))
{
// 如果是1.0版本的长连接,设置 connection:keep-alive 头部
response.headers().set(CONNECTION, KEEP_ALIVE);
} //发送内容
ChannelFuture writePromise = ctx.channel().writeAndFlush(response); if (!keepAlive)
{
// 如果不是长连接,发送完成之后,关闭连接
writePromise.addListener(ChannelFutureListener.CLOSE);
}
} private static HttpVersion getHttpVersion(ChannelHandlerContext ctx)
{
HttpVersion version;
if (isHTTP_1_0(ctx))
{
version = HTTP_1_0;
} else
{
version = HTTP_1_1;
}
return version;
} /**
* When file timestamp is the same as what the browser is sending up, send a "304 Not Modified"
*
* @param ctx Context
*/
public static void sendNotModified(ChannelHandlerContext ctx)
{
HttpVersion version = getHttpVersion(ctx);
FullHttpResponse response = new DefaultFullHttpResponse(version, NOT_MODIFIED, Unpooled.EMPTY_BUFFER);
setDateHeader(response); sendAndCleanupConnection(ctx, response);
} public static boolean isHTTP_1_0(ChannelHandlerContext ctx)
{ HttpVersion protocol_version =
ctx.channel().attr(PROTOCOL_VERSION_KEY).get();
if (null == protocol_version)
{
return false;
}
if (protocol_version.equals(HTTP_1_0))
{
return true;
}
return false;
} /**
* Sets the Date header for the HTTP response
*
* @param response HTTP response
*/
public static void setDateHeader(FullHttpResponse response)
{
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); Calendar time = new GregorianCalendar();
response.headers().set(DATE, dateFormatter.format(time.getTime()));
} /**
* Sets the Date and Cache headers for the HTTP Response
*
* @param response HTTP response
* @param fileToCache file to extract content type
*/
public static void setDateAndCacheHeaders(HttpResponse response, File fileToCache)
{
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); // Date header
Calendar time = new GregorianCalendar();
response.headers().set(DATE, dateFormatter.format(time.getTime())); //设置缓存过期时间
time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
response.headers().set(EXPIRES, dateFormatter.format(time.getTime()));
response.headers().set(CACHE_CONTROL, "private, max-platform=" + HTTP_CACHE_SECONDS); //最近修改时间
String lastModified = dateFormatter.format(new Date(fileToCache.lastModified()));
response.headers().set(LAST_MODIFIED, lastModified);
} /**
* Sets the content type header for the HTTP Response
*
* @param response HTTP response
* @param file file to extract content type
*/
public static void setContentTypeHeader(HttpResponse response, File file)
{
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,
mimeTypesMap.getContentType(file.getPath()));
} public static void setKeepAlive(ChannelHandlerContext ctx, HttpResponse response)
{
final boolean keepAlive = ctx.channel().attr(KEEP_ALIVE_KEY).get(); if (!keepAlive)
{
response.headers().set(CONNECTION, CLOSE); } else if (isHTTP_1_0(ctx))
{
response.headers().set(CONNECTION, KEEP_ALIVE);
} } public static boolean isKeepAlive(ChannelHandlerContext ctx)
{
boolean keepAlive = ctx.channel().attr(KEEP_ALIVE_KEY).get();
return keepAlive;
} /**
* 发送目录或者错误信息,如果是文件,则返回
*
* @param ctx 上下文
* @param request 请求
* @return 文件对象
*/
public static File sendErrorOrDirectory(ChannelHandlerContext ctx, FullHttpRequest request)
{
/**
* 路径不对
*/
final String uri = request.uri();
final String path = HttpProtocolHelper.sanitizeUri(uri, SystemConfig.getFileServerDir());
if (path == null)
{
HttpProtocolHelper.sendError(ctx, FORBIDDEN);
return null;
}
File file = new File(path); /**
* 文件不存在
*/
if (!file.exists())
{
HttpProtocolHelper.sendError(ctx, NOT_FOUND);
return null;
} /**
* 发送文件目录
*/
if (file.isDirectory())
{
if (uri.endsWith("/"))
{
HttpProtocolHelper.sendListing(ctx, request, file, uri);
} else
{
HttpProtocolHelper.sendRedirect(ctx, request, uri + '/');
}
return null;
}
/**
* 文件不可用访问
*/
if (!file.isFile())
{
HttpProtocolHelper.sendError(ctx, FORBIDDEN);
return null;
} return file;
} /**
* 根据文件,获取只读的随机访问文件实例
*
* @param ctx 上下文
* @param file 文件
* @return 随机访问文件实例
*/
public static RandomAccessFile openFile(ChannelHandlerContext ctx, File file)
{
RandomAccessFile raf = null;
try
{
raf = new RandomAccessFile(file, "r");
} catch (FileNotFoundException ignore)
{
HttpProtocolHelper.sendError(ctx, NOT_FOUND);
return null;
}
return raf;
}
}

参考文献:java高并发核心编程Nio、Netty、Redis、ZooKeeper 作者:尼恩

Netty内置的http报文解码流程的更多相关文章

  1. python 之 前端开发( JavaScript变量、数据类型、内置对象、运算符、流程控制、函数)

    11.4 JavaScript 11.41 变量 1.声明变量的语法 // 1. 先声明后定义 var name; // 声明变量时无需指定类型,变量name可以接受任意类型 name= " ...

  2. 前端开发:4、JavaScript简介、变量与常量、数据类型及内置方法、运算符、流程控制、循环结构、内置方法

    前端开发之JavaScript 目录 前端开发之JavaScript 一.JavaScript简介 二.JS基础 三.变量与常量 四.基本数据类型 1.数值类型 2.字符类型 3.布尔类型 五.特殊数 ...

  3. Error js内置错误 js处理错误流程 Throw语句

    Exceptional Exception Handling in JavaScript       MDN资料 Anything that can go wrong, will go wrong. ...

  4. Netty内置的编解码器和ChannelHandler

    Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相当繁琐的事务上本来会花费的时间与精力. 通过SSL/TLS 保护Netty 应用程序 SSL和TLS这样的安全协议 ...

  5. Mysql内置功能《六》流程控制

    一 流程控制 delimiter // CREATE PROCEDURE proc_if () BEGIN declare i int default 0; if i = 1 THEN SELECT ...

  6. Mysql内置功能《一》流程控制

    delimiter // CREATE PROCEDURE proc_if () BEGIN declare i int default 0; if i = 1 THEN SELECT 1; ELSE ...

  7. 5、前端--js常量、变量、5种基本数据类型(number string boolean undefined object)、运算符、流程控制、三元运算符、函数、自定义对象、内置对象、BOM操作

    变量与常量 在JS中声明变量需要使用关键字 老版本 var(全部都是全局变量) 新版本 let(可以声明局部变量) # 推荐使用let(其实问题不大) 在JS中声明常量也需要使用关键字 const # ...

  8. awk(流程控制、内置变量、内置函数、数组)

    摘自:http://bbs.51cto.com/thread-883948-1-1.html awk(流程控制.内置变量.内置函数.数组) ... 参考其他的资料,给大家看看.一.awk流程控制语句 ...

  9. 第一百零八节,JavaScript,内置对象,Global对象字符串编码解码,Math对象数学公式

    JavaScript,内置对象,Global对象字符串编码解码,Math对象数学公式 学习要点: 1.Global对象 2.Math对象 ECMA-262对内置对象的定义是:"由ECMASc ...

  10. Javascript初识之流程控制、函数和内置对象

    一.JS流程控制 1. 1.if else var age = 19; if (age > 18){ console.log("成年了"); }else { console. ...

随机推荐

  1. AI绘画StableDiffusion实操教程:月光下的美人(含高清图片)

    本教程收集于:AIGC从入门到精通教程汇总 今天继续分享AI绘画实操教程,如何用lora包生成超真实好看质感超强的人物图片. 放大高清图已放到教程包内,需要的可以自取. 出图效果: 更多图片资源访问查 ...

  2. CodeForces 1388C Uncle Bogdan and Country Happiness

    题意 给一棵\(n\)节点的树,每个节点有\(a[i]\)个人住,他们从\(1\)号节点回家,回家路上可能从开心的状态变成不开心的状态(但不可以由不开心变为开心),每个节点有个探测器,会探测经过该节点 ...

  3. HDU 1171 0-1背包

    最近感觉DP已经完全忘了..各种爆炸,打算好好复习一发,0-1背包开始 Big Event in HDU Problem Description Nowadays, we all know that ...

  4. KRpano项目微信出现"关于潜在的违法或违规内容"

    最近,部分小伙伴反应某些KRPano项目在微信中,出现"关于潜在的未发或违规内容"而无法播放的问题,会看到下图中的提示: 出现原因 这个问题是由于KRPano项目中的webvr.j ...

  5. LeetCode 周赛上分之旅 #46 经典二分答案与质因数分解

    ️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问. 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越 ...

  6. 微信Native支付(扫码支付)商户配置

    0.需要从商户平台获取/设置的配置 公众号appId 商户号 APIv3密钥 证书序列号 证书密钥 1.扫码登录商户平台 网址:https://pay.weixin.qq.com/ 2.确认已开通Na ...

  7. day02 数据类型转换 运算符 方法

    数据类型转换 自动类型转换 强制类型转换 1. 自动类型转换:就是范围小的向范围大的转换  将取值范围小刀的类型自动提升为取值范围大的类型. 转换规则  byte.short.char  int--- ...

  8. CH59X/CH58X/CH57X PWM使用

    以CH582M为例: CH582M有4+8组PWM这里的4路26位PWM(定时器提供),8路系统PWM(8位) 先看系统提供的PWM: 下列截图根据例程进行测试的 注:如需要使用PWM11则需要通过i ...

  9. bash解释器特性、目录结构、命令种类及优先级、常用命令

    bash解释器的交互式环境特性 命令和文件自动补全 注意:Tab只能补全命令和文件及其文件路径 [root@localhost ~]# ls /etc/sysconfig/network-script ...

  10. MySQL8.0默认加密连接方式

    Mysql8.0开始默认采用新的caching_sha2_password的身份验证方式,常规老接口会因此无法连接数据库. 为继续使用老的身份验证方式,需显式指定身份验证方式为 mysql_nativ ...