Netty内置的http报文解码流程
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 作者:尼恩