文章目录
- HttpPostRequestEncoder
-
- EncoderMode
- 属性
- 构造方法
-
- cleanFiles()
- isMultipart()
- initMixedMultipart()
- getBodyListAttributes()
- setBodyHttpDatas(List\<InterfaceHttpData>)
- [addBodyAttribute(String name, String value)](#addBodyAttribute(String name, String value))
- [addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText)](#addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText))
- [addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)](#addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText))
- *addBodyHttpData(InterfaceHttpData)
-
- [encodeAttribute(String s, Charset charset)](#encodeAttribute(String s, Charset charset))
- *finalizeRequest()
- isChunked()
- readChunk(ByteBufAllocator)
-
- HttpPostRequestEncoder示例
-
HttpPostRequestEncoder
java
复制代码
// 实现ChunkedInput接口, 将使用 ChunkedWriteHandler 逐块读取,每次读取1个HttpContent分块
public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
// ...
}
EncoderMode
java
复制代码
// 编码模式
public enum EncoderMode {
// 传统模式适用于多数情况。已知其不兼容OAUTH认证,OAUTH场景请使用EncoderMode#RFC3986。
// W3C表单规范建议在处理POST表单数据提交时采用此编码模式。
RFC1738,
// 此模式为较新的编码方案,专用于OAUTH认证。
RFC3986,
// HTML5规范禁止在multipart/form-data请求中使用混合模式。
// 更具体地说,这意味着同名提交的多个文件将不会使用混合模式编码,而是会被视为独立字段处理。
HTML5
}
属性
java
复制代码
@SuppressWarnings("rawtypes")
private static final Map.Entry[] percentEncodings;
static {
percentEncodings = new Map.Entry[] {
// * 被替换为 %2A
new SimpleImmutableEntry<Pattern, String>(Pattern.compile("\\*"), "%2A"),
// + 被替换为 %20
new SimpleImmutableEntry<Pattern, String>(Pattern.compile("\\+"), "%20"),
// ~ 被替换为 %7E
new SimpleImmutableEntry<Pattern, String>(Pattern.compile("~"), "%7E")
};
}
// 用于手动添加数据时,将对应的对象转为HttpData
private final HttpDataFactory factory;
// 必须指定1个请求对象
private final HttpRequest request;
// 默认使用的编码
private final Charset charset;
// 是否使用分块传输,默认不使用分块传输
private boolean isChunked;
// 待编码的HttpData数据
private final List<InterfaceHttpData> bodyListDatas;
// 已编码的HttpData数据
final List<InterfaceHttpData> multipartHttpDatas;
// request是否是multipart文件上传请求
private final boolean isMultipart;
// 如果request是文件上传请求,这个就是分隔线
String multipartDataBoundary;
// 若为multipart格式,全局multipart内部可能存在嵌套的multipart(mixed)结构,此为嵌套multipart的分隔线。
// 对此仅允许单层嵌套。
String multipartMixedBoundary;
// 用于检查标头是否已最终确定
private boolean headerFinalized;
// 编码模式
private final EncoderMode encoderMode;
// 已经是最后数据块,下一数据块将为空
private boolean isLastChunk;
// 最后数据块已发送
private boolean isLastChunkSent;
// 当前正在编码处理中的文件上传对象
private FileUpload currentFileUpload;
// 添加文件上传时,当前是否处于混合模式(Mixed Mode)
private boolean duringMixedMode;
// 整体内容主体大小
private long globalBodySize;
// 整体传输进度
private long globalProgress;
// 已编码的HttpData集合 的 迭代器
private ListIterator<InterfaceHttpData> iterator;
// 当前读取块时,正在处理的 currentBuffer
private ByteBuf currentBuffer;
// 当前读取块时,正在处理的 httpData对象
private InterfaceHttpData currentData;
// 在不作 文件上传时,标记这个currentBuffer是key还是value
private boolean isKey = true;
构造方法
java
复制代码
public HttpPostRequestEncoder(HttpRequest request,
boolean multipart) {
// 创建HttpData的工厂
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), // 16384
// 请求对象
request,
// 是否需要作multipart文件上传
multipart,
// UTF-8
HttpConstants.DEFAULT_CHARSET,
// RFC1738 传统模式
EncoderMode.RFC1738);
}
public HttpPostRequestEncoder(HttpDataFactory factory,
HttpRequest request,
boolean multipart) {
this(factory,
request,
multipart,
HttpConstants.DEFAULT_CHARSET,
EncoderMode.RFC1738);
}
public HttpPostRequestEncoder(
HttpDataFactory factory,
HttpRequest request,
boolean multipart,
Charset charset,
EncoderMode encoderMode) {
this.request = checkNotNull(request, "request");
this.charset = checkNotNull(charset, "charset");
this.factory = checkNotNull(factory, "factory");
// trace请求不允许使用该编码器
if (HttpMethod.TRACE.equals(request.method())) {
throw new ErrorDataEncoderException();
}
// 待编码的HttpData列表初始化
bodyListDatas = new ArrayList<InterfaceHttpData>();
// 默认初始化为 当前不是最后的分块
isLastChunk = false;
// 默认初始化为 最后的分块尚未发送完
isLastChunkSent = false;
// 默认初始化 是否作文件上传
isMultipart = multipart;
// 已编码的HttpData列表初始化
multipartHttpDatas = new ArrayList<InterfaceHttpData>();
// 编码模式
this.encoderMode = encoderMode;
// 如果要作文件上传请求
if (isMultipart) {
// 生成1个分割线字符串
initDataMultipart();
}
}
initDataMultipart
java
复制代码
private void initDataMultipart() {
// 生成分隔线字符串
multipartDataBoundary = getNewMultipartDelimiter();
}
getNewMultipartDelimiter
java
复制代码
private static String getNewMultipartDelimiter() {
return Long.toHexString(PlatformDependent.threadLocalRandom().nextLong());
}
cleanFiles()
java
复制代码
public void cleanFiles() {
// 将HttpDataFactory实例的requestFileDeleteMap属性中,
// 该request作为key对应的HttpData集合移除掉,并且遍历该HttpData集合,逐个释放HttpData
factory.cleanRequestHttpData(request);
}
isMultipart()
java
复制代码
public boolean isMultipart() {
return isMultipart;
}
initMixedMultipart()
java
复制代码
private void initMixedMultipart() {
// 生成嵌套部分的分隔线
multipartMixedBoundary = getNewMultipartDelimiter();
}
getBodyListAttributes()
java
复制代码
public List<InterfaceHttpData> getBodyListAttributes() {
// 返回 待编码的HttpData列表
return bodyListDatas;
}
setBodyHttpDatas(List<InterfaceHttpData>)
java
复制代码
public void setBodyHttpDatas(List<InterfaceHttpData> datas) {
// 传入的 datas 不能为null
ObjectUtil.checkNotNull(datas, "datas");
// 整体内容主体大小 置为 0
globalBodySize = 0;
// 清空 待编码的HttpData列表
bodyListDatas.clear();
// 当前正在编码处理中的文件上传对象
currentFileUpload = null;
// 添加文件上传时,当前是否处于混合模式(Mixed Mode) 置为false
duringMixedMode = false;
// 清空 已编码的HttpData列表
multipartHttpDatas.clear();
// 逐个添加 传入的HttpData列表
for (InterfaceHttpData data : datas) {
addBodyHttpData(data);
}
}
addBodyAttribute(String name, String value)
java
复制代码
public void addBodyAttribute(String name, String value) {
String svalue = value != null? value : "";
// 使用HttpDataFactory创建Attribute属性对象
Attribute data = factory.createAttribute(request,
checkNotNull(name, "name"),
svalue);
// 添加 将刚刚创建的Attribute属性对象
addBodyHttpData(data);
}
addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText)
java
复制代码
public void addBodyFileUpload(String name,
String filename,
File file,
String contentType,
boolean isText) {
// name不能为null
checkNotNull(name, "name");
// file不能为null
checkNotNull(file, "file");
// 文件名如果为null,则默认为空字符串
if (filename == null) {
filename = "";
}
String scontentType = contentType;
String contentTransferEncoding = null;
// 如果contentType为空
if (contentType == null) {
// 如果是文本,则contentType设置为text/plain
if (isText) {
scontentType = HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE;
} else {
// 如果不是文本,则contentType设置为application/octet-stream
scontentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
}
}
// 如果不是文本
if (!isText) {
// HTTP multipart传输时允许的三种编码机制:7bit、8bit和binary
// - 7bit (默认):只能包含ASCII字符(0-127)。数据中的每一行必须以CRLF (\r\n)结束。这是最通用、兼容性最好的格式。
// - 8bit:可以包含非ASCII字符(扩展字节,128-255),但每一行仍然有长度限制(通常不超过998个字符),且必须以CRLF结束。它不在字节层面进行编码转换。
// - BINARY:可以包含任何数据,没有行长度限制,也不需要遵守CRLF换行规则。它是最自由的格式。
// contentTransferEncoding 设置为 binary
contentTransferEncoding = HttpPostBodyUtil.TransferEncodingMechanism
.BINARY.value();
}
// 创建 FileUpload 文件上传对象
FileUpload fileUpload = factory.createFileUpload(
request,
name,
filename,
scontentType,
contentTransferEncoding,
null, // charset参数
file.length()
);
try {
// 将 file 文件设置到 FileUpload对象 中
fileUpload.setContent(file);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
// 将此 fileUpload 添加
addBodyHttpData(fileUpload);
}
addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)
java
复制代码
public void addBodyFileUploads(String name,
File[] file,
String[] contentType,
boolean[] isText) {
if (file.length != contentType.length && file.length != isText.length) {
throw new IllegalArgumentException("Different array length");
}
// 逐个添加 文件上传
for (int i = 0; i < file.length; i++) {
addBodyFileUpload(name, file[i], contentType[i], isText[i]);
}
}
*addBodyHttpData(InterfaceHttpData)
java
复制代码
public void addBodyHttpData(InterfaceHttpData data) {
// 检查标头是否已最终确定, 如果已经确定,则不允许添加HttpData
if (headerFinalized) {
throw new ErrorDataEncoderException("Cannot add value once finalized");
}
// 将 待添加的 HttpData 添加到 bodyListDatas 待编码的HttpData数据集合中
bodyListDatas.add(checkNotNull(data, "data"));
// 如果不作 文件上传
if (!isMultipart) {
// 如果 data 是 Attribute
if (data instanceof Attribute) {
Attribute attribute = (Attribute) data;
try {
// name=value& with encoded name and attribute
// 对key和value重新编码
String key = encodeAttribute(attribute.getName(), charset);
String value = encodeAttribute(attribute.getValue(), charset);
// 重新创建Attribute对象
Attribute newattribute = factory.createAttribute(request, key, value);
// 将重新创建的Attribute对象添加到 已编码的HttpData数据 集合中
multipartHttpDatas.add(newattribute);
// 整体内容大小 添加 (按name=value&计算)的长度
globalBodySize += newattribute.getName().length()
+ 1
+ newattribute.length()
+ 1;
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
}
else if (data instanceof FileUpload) {
// 因为不作 文件上传,所以即便添加FileUpload,也只会把它当作Attribute处理
FileUpload fileUpload = (FileUpload) data;
// name=filename& with encoded name and filename
// 同样 对key和value重新编码
String key = encodeAttribute(fileUpload.getName(), charset);
String value = encodeAttribute(fileUpload.getFilename(), charset);
// 创建Attribute对象
Attribute newattribute = factory.createAttribute(request, key, value);
// 将重新创建的Attribute对象添加到 已编码的HttpData数据 集合中
multipartHttpDatas.add(newattribute);
// 整体内容大小 添加 (按name=value&计算)的长度
globalBodySize += newattribute.getName().length()
+ 1
+ newattribute.length()
+ 1;
}
// 结束
return;
}
// 此时,就表示需要作 文件上传 处理
// 如果data是 Attribute对象
if (data instanceof Attribute) {
// 如果 当前是否处于混合模式
if (duringMixedMode) {
// 创建1个 InternalAttribute
InternalAttribute internal = new InternalAttribute(charset);
// 添加分隔线
internal.addValue("\r\n--" + multipartMixedBoundary + "--");
// 将 刚创建的 InternalAttribute 添加到 已编码的HttpData数据(其实该if里面只干了这一件实事)
multipartHttpDatas.add(internal);
// 嵌套部分的分隔线 置为 null
multipartMixedBoundary = null;
// 当前正在编码处理中的文件上传对象 置为 null
currentFileUpload = null;
// 将 当前是否处于混合模式 置为 false
duringMixedMode = false;
}
// 创建1个 InternalAttribute
InternalAttribute internal = new InternalAttribute(charset);
// 如果前面已经添加过了 HttpData 到 已编码的HttpData数据 集合中
if (!multipartHttpDatas.isEmpty()) {
// 此时,就需要添加 回车 + 换行
internal.addValue("\r\n");
}
// 添加1个分隔线 + 回车 + 换行
internal.addValue("--" + multipartDataBoundary + "\r\n");
// 原本要添加的 HttpData 强转为 attribute
Attribute attribute = (Attribute) data;
// 添加 content-disposition: form-data; name="属性名" 加 \r\n
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION
+ ": "
+ HttpHeaderValues.FORM_DATA
+ "; "
+ HttpHeaderValues.NAME
+ "=\""
+ attribute.getName()
+ "\"\r\n");
// 添加 content-length: xxx 加 \r\n
internal.addValue(HttpHeaderNames.CONTENT_LENGTH
+ ": "
+ attribute.length()
+ "\r\n");
// 如果 attribute还指定了编码集
Charset localcharset = attribute.getCharset();
if (localcharset != null) {
// 添加content-type: text/plain; charset=指定的编码集 加 \r\n
internal.addValue(HttpHeaderNames.CONTENT_TYPE
+ ": "
+ HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE
+ "; "
+ HttpHeaderValues.CHARSET
+ '='
+ localcharset.name()
+ "\r\n");
}
// 在 头 和 内容之间 添加1个空行
internal.addValue("\r\n");
// 将 internal 添加到 已编码的HttpData数据 集合中
multipartHttpDatas.add(internal);
// 将 真正的数据 data 添加到 已编码的HttpData数据 集合中
multipartHttpDatas.add(data);
// 整体内容大小 添加 真正的数据data的长度 + internal头部的长度
globalBodySize += attribute.length() + internal.size();
}
else if (data instanceof FileUpload) {
// 如果 data 是 FileUpload文件上传 对象,
// 则强转为fileUpload
FileUpload fileUpload = (FileUpload) data;
// 创建1个 InternalAttribute
InternalAttribute internal = new InternalAttribute(charset);
// 如果前面已经添加过了 HttpData 到 已编码的HttpData数据 集合中
if (!multipartHttpDatas.isEmpty()) {
// 此时,就需要添加 回车 + 换行
internal.addValue("\r\n");
}
boolean localMixed;
// 如果 当前是否处于混合模式
if (duringMixedMode) {
// 当前添加的fileUpload的name与currentFileUpload又相同
if (currentFileUpload != null
&& currentFileUpload.getName().equals(fileUpload.getName())) {
// 指示下面的代码需要处理混合分隔符
localMixed = true;
}
else {
// 当前添加的fileUpload的name与currentField不相同了
// internal 添加 --混合分隔线字符串-- 表示混合处理结束了
internal.addValue("--" + multipartMixedBoundary + "--");
// 将 internal 添加到 已编码的HttpData数据 集合中
multipartHttpDatas.add(internal);
// 将 multipartMixedBoundary混合分隔符 置为null
multipartMixedBoundary = null;
// 构建1个新的internal,开始添加1个新的文件上传对象
internal = new InternalAttribute(charset);
// 因为multipartHttpDatas已经有数据了,所以这里需要加上 \r\n
internal.addValue("\r\n");
// 指示下面的代码不需要处理混合分隔符
localMixed = false;
// 记录当前处理的fileUpload
currentFileUpload = fileUpload;
// 混合模式已经处理完毕,将此标记置为false
duringMixedMode = false;
}
}
else {
// 在 multipart/form-data 编码中,如果一个字段名对应多个文件(比如同一个 name 有多个文件),那么可以使用 multipart/mixed 嵌套。
// 如果当前的编码模式 不为 HTML5(即编码模式为: RFC1738 或 RFC3986)
// 并且 当前currentFileUpload 不为null(之前 已添加过 文件上传对象)
// 并且 当前currentFileUpload的name属性 与 当前要添加的fileUpload的name属性相同
// (第一次添加FileUpload文件上传对象不可能走这个if,因为currentFileUpload初始的时候就是null;
// 这个显然是处理当前要添加的FileUpload文件上传对象与上1次添加的FileUpload文件上传对象的name属性名相同时,所要执行的逻辑;)
// 注意,只有在这个if中(也就是只有在满足当前if中的条件),才会将duringMixed置为true,此处同时也会将localMixed置为true
if (encoderMode != EncoderMode.HTML5
&& currentFileUpload != null
&& currentFileUpload.getName().equals(fileUpload.getName())) {
// 重新生成1个分隔符字符串,赋值到 multipartMixedBoundary
initMixedMultipart();
// 这里就是拿到 上次 已添加过的 文件上传对象 前面添加的internal
// 因为 multipartHttpDatas 列表在添加每个数据部分时,通常先添加一个 InternalAttribute(作为头部),然后添加实际的数据(Attribute 或 FileUpload)
// 现在,我们需要修改前一个文件的头部,将其从普通的 form-data 部分改为 multipart/mixed 部分的开始。所以,我们需要获取前一个文件的头部(即 InternalAttribute)并进行修改。
InternalAttribute pastAttribute =
(InternalAttribute) multipartHttpDatas.get(multipartHttpDatas.size() - 2);
// 整体内容大小 先减去 上次 已添加过的 文件上传对象 前面添加的internal的长度
// (因为我们将要修改前面添加的这个头部,修改后的大小会变化,所以先减去旧的大小。)
globalBodySize -= pastAttribute.size();
// 下面所有已经确定的字符串的长度就是139
// 还需要加上 分隔符的长度
// 混合分隔符的长度的2倍
// 文件上传对象的fileName的长度
// 文件上传对象的name的长度
/*
--分隔符 + "\r\n"
content-disposition: form-data; name="属性名" + "\r\n"
content-type: multipart/mixed; boundary=混合分隔符 + "\r\n\r\n"
这里会空1行
--混合分隔符 + "\r\n"
content-disposition: attachment
*/
StringBuilder replacement = new StringBuilder(
139
+ multipartDataBoundary.length()
+ multipartMixedBoundary.length() * 2
+ fileUpload.getFilename().length()
+ fileUpload.getName().length()
)
.append("--")
.append(multipartDataBoundary)
.append("\r\n")
.append(HttpHeaderNames.CONTENT_DISPOSITION)
.append(": ")
.append(HttpHeaderValues.FORM_DATA)
.append("; ")
.append(HttpHeaderValues.NAME)
.append("=\"")
.append(fileUpload.getName())
.append("\"\r\n")
.append(HttpHeaderNames.CONTENT_TYPE)
.append(": ")
.append(HttpHeaderValues.MULTIPART_MIXED)
.append("; ")
.append(HttpHeaderValues.BOUNDARY)
.append('=')
.append(multipartMixedBoundary)
.append("\r\n\r\n")
.append("--")
.append(multipartMixedBoundary)
.append("\r\n")
.append(HttpHeaderNames.CONTENT_DISPOSITION)
.append(": ")
.append(HttpHeaderValues.ATTACHMENT);
// 如果当前添加的fileUpload文件上传对象的filename不能为空
if (!fileUpload.getFilename().isEmpty()) {
// 添加 ; filename="文件名"
replacement.append("; ")
.append(HttpHeaderValues.FILENAME)
.append("=\"")
.append(currentFileUpload.getFilename())
.append('"');
}
// 再加上换行符
replacement.append("\r\n");
// 将前面的添加的文件上传的前的attribute的 第2个添加的内容 替换为 上面构造的replacement
pastAttribute.setValue(replacement.toString(), 1);
// 将前面的添加的文件上传的前的attribute的 第3个添加的内容 清空
pastAttribute.setValue("", 2);
// 重新更新 整体的内容大小
globalBodySize += pastAttribute.size();
// 指示下面的代码需要处理混合分隔符
localMixed = true;
// 标记 当前处于mixed混合模式
duringMixedMode = true;
} else {
// 指示下面的代码不需要处理混合分隔符
localMixed = false;
// 记录当前正在操作的fileUpload文件上传对象
currentFileUpload = fileUpload;
// 标记 当前不处于mixed混合模式
duringMixedMode = false;
}
}
// 需要处理混合分隔符的情况
if (localMixed) {
// internal 添加 --混合分隔符 + "\r\n"
internal.addValue("--" + multipartMixedBoundary + "\r\n");
// 如果 fileUpload对象的filename为空
if (fileUpload.getFilename().isEmpty()) {
// internal 添加 content-disposition: attachment + "\r\n"
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION
+ ": "
+ HttpHeaderValues.ATTACHMENT
+ "\r\n");
} else {
// 如果 fileUpload对象的filename不为空
// internal 添加 content-disposition: attachment; filename="file1.txt" + "\r\n"
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION
+ ": "
+ HttpHeaderValues.ATTACHMENT
+ "; "
+ HttpHeaderValues.FILENAME
+ "=\""
+ fileUpload.getFilename()
+ "\"\r\n");
}
} else {
// internal 先添加 --分隔符 + "\r\n"
internal.addValue("--" + multipartDataBoundary + "\r\n");
// 如果fileUpload文件上传对象的filename是空的话,
if (fileUpload.getFilename().isEmpty()) {
// internal 添加 Content-Disposition: form-data; name="files"; + "\r\n"
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION
+ ": "
+ HttpHeaderValues.FORM_DATA
+ "; "
+ HttpHeaderValues.NAME
+ "=\""
+ fileUpload.getName()
+ "\"\r\n");
} else {
// 如果fileUpload文件上传对象的filename不是空的话
// internal 添加 Content-Disposition: form-data; name="files"; filename="file1.txt" + "\r\n"
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION
+ ": "
+ HttpHeaderValues.FORM_DATA
+ "; "
+ HttpHeaderValues.NAME
+ "=\"" + fileUpload.getName()
+ "\"; "
+ HttpHeaderValues.FILENAME
+ "=\"" + fileUpload.getFilename()
+ "\"\r\n");
}
}
// 以下部分是需要作 文件上传 时,公共处理逻辑
// 添加 content-length: 长度 + "\r\n"
internal.addValue(HttpHeaderNames.CONTENT_LENGTH
+ ": "
+ fileUpload.length()
+ "\r\n");
// 添加 content-type: 内容类型
internal.addValue(HttpHeaderNames.CONTENT_TYPE
+ ": "
+ fileUpload.getContentType());
// 获取fileUpload文件上传对象的contenTransferEncoding属性
String contentTransferEncoding = fileUpload.getContentTransferEncoding();
// 如果fileUpload文件上传对象的contenTransferEncoding属性值为 binary
if (contentTransferEncoding != null
&& contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism
.BINARY.value()
)
) {
// internal 继续添加 "\r\n" + content-transfer-encoding: binary + "\r\n\r\n"
// 注意, 这里最后面换行了2次,相当于在最下面空了1行,这些头部内容和真正内容有1空行
internal.addValue("\r\n"
+ HttpHeaderNames.CONTENT_TRANSFER_ENCODING
+ ": "
+ HttpPostBodyUtil.TransferEncodingMechanism
.BINARY.value()
+ "\r\n\r\n");
}
// 如果fileUpload文件上传对象的contenTransferEncoding属性值不是binary, 并且设置了编码集
else if (fileUpload.getCharset() != null) {
// internal 继续添加 ; charset=字符集 + "\r\n\r\n"
// 注意, 这里最后面换行了2次,相当于在最下面空了1行,这些头部内容和真正内容有1空行
internal.addValue("; "
+ HttpHeaderValues.CHARSET
+ '='
+ fileUpload.getCharset().name()
+ "\r\n\r\n");
}
else {
// 注意, 这里最后面换行了2次,相当于在最下面空了1行,这些头部内容和真正内容有1空行
internal.addValue("\r\n\r\n");
}
// 先将internal 添加到 已编码的HttpData集合 中
multipartHttpDatas.add(internal);
// 再把 真正的内容data 添加到 已编码的HttpData集合 中
multipartHttpDatas.add(data);
// 更新 整体内容大小 为加 真正的数据data的长度 + internal头部的长度
globalBodySize += fileUpload.length() + internal.size();
}
}
encodeAttribute(String s, Charset charset)
java
复制代码
private String encodeAttribute(String s, Charset charset) {
// s 为 null时的处理
if (s == null) {
// 返回 空字符串
return "";
}
try {
// 对 s 进行URL编码
String encoded = URLEncoder.encode(s, charset.name());
// 如果编码模式 为 RFC3986
if (encoderMode == EncoderMode.RFC3986) {
// URL编码之后, 还需要作相应的 字符串替换
// * 被替换为 %2A
// + 被替换为 %20
// ~ 被替换为 %7E
for (Map.Entry<Pattern, String> entry : percentEncodings) {
String replacement = entry.getValue();
encoded = entry.getKey().matcher(encoded).replaceAll(replacement);
}
}
// 返回编码处理后的字符串
return encoded;
} catch (UnsupportedEncodingException e) {
throw new ErrorDataEncoderException(charset.name(), e);
}
}
*finalizeRequest()
java
复制代码
public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
// 如果 头部编码 还未完成
if (!headerFinalized) {
// 如果需要作 文件上传
if (isMultipart) {
// 构建1个 InternalAttribute
InternalAttribute internal = new InternalAttribute(charset);
// 如果当前还在处于 混合模式
if (duringMixedMode) {
// internal 添加 "\r\n" + --混合分隔符--
internal.addValue("\r\n--" + multipartMixedBoundary + "--");
}
// internal 添加 "\r\n--" + 分隔符-- +"\r\n"
internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
// 将internal 添加到 已编码的HttpData集合 中
multipartHttpDatas.add(internal);
// 混合分隔符 置为null
multipartMixedBoundary = null;
// 当前currentFileUpload置为null
currentFileUpload = null;
// 当前还在处于 混合模式 置为false
duringMixedMode = false;
// 更新 整体内容长度
globalBodySize += internal.size();
}
// 标记 头部编码 已完成
headerFinalized = true;
} else {
// 如果 头部编码 已完成, 则当前方法不允许再调用
throw new ErrorDataEncoderException("Header already encoded");
}
// 获取请求头信息
HttpHeaders headers = request.headers();
// 获取content-type头
List<String> contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE);
// 获取transfer-encoding头
List<String> transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING);
if (contentTypes != null) {
// 移除全部的 content-type头
headers.remove(HttpHeaderNames.CONTENT_TYPE);
// 遍历之前的 content-type头
for (String contentType : contentTypes) {
String lowercased = contentType.toLowerCase();
// 忽略 multipart/form-data、application/x-www-form-urlencoded 这些值
// 只保留其它值
if (lowercased.startsWith(HttpHeaderValues
.MULTIPART_FORM_DATA.toString())
|| lowercased.startsWith(HttpHeaderValues
.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
// ignore
} else {
headers.add(HttpHeaderNames.CONTENT_TYPE, contentType);
}
}
}
// 如果要作 文件上传
if (isMultipart) {
// 请求头 添加 content-type: multipart/form-data; boundary=分隔符
String value = HttpHeaderValues.MULTIPART_FORM_DATA
+ "; "
+ HttpHeaderValues.BOUNDARY
+ '='
+ multipartDataBoundary;
headers.add(HttpHeaderNames.CONTENT_TYPE, value);
} else {
// 请求头 添加 content-type: application/x-www-form-urlencoded
headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues
.APPLICATION_X_WWW_FORM_URLENCODED);
}
// 根据大小判断是否需要分块传输
long realSize = globalBodySize;
// 如果不作 文件上传, 大小 - 1(移除最后面的&)
if (!isMultipart) {
realSize -= 1;
}
// 获取 已编码的HttpData集合 的 迭代器
iterator = multipartHttpDatas.listIterator();
// 设置 content-length 请求头
headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize));
// 如果大小超过了 8096(默认)或者 明确需要文件上传
if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) {
// 标记要使用分块传输(此时,只会返回1个HttpRequest,后续需要将当前encoder通过ChunkedWriterHandler写出去)
isChunked = true;
// 移除 transfer-encoding中的chunked值,其他值保留
if (transferEncoding != null) {
headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
for (CharSequence v : transferEncoding) {
if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) {
// ignore
} else {
headers.add(HttpHeaderNames.TRANSFER_ENCODING, v);
}
}
}
// 移除 content-length 请求头,transfer-encoding添加chunked值
HttpUtil.setTransferEncodingChunked(request, true);
// 返回包装后的 HttpRequest
return new WrappedHttpRequest(request);
} else {
// 说明大小 小于 8096(默认)并且 明确不需要文件上传
// 则获取1个分块就可以处理完成
HttpContent chunk = nextChunk();
// 如果request本身就是FullHttpRequest
if (request instanceof FullHttpRequest) {
FullHttpRequest fullRequest = (FullHttpRequest) request;
// 拿到chunk的内容
ByteBuf chunkContent = chunk.content();
// 将chunk的内容覆盖掉原FullHttpRequest中的内容
if (fullRequest.content() != chunkContent) {
fullRequest.content().clear().writeBytes(chunkContent);
// 释放chunkContent
chunkContent.release();
}
return fullRequest;
} else {
// 返回1个WrappedFullHttpRequest(直接将reques和chunk组合成FullHttpRequest)
return new WrappedFullHttpRequest(request, chunk);
}
}
}
isChunked()
java
复制代码
public boolean isChunked() {
// 返回是否需要 分块传输
return isChunked;
}
readChunk(ByteBufAllocator)
java
复制代码
@Override
public HttpContent readChunk(ByteBufAllocator allocator) {
// 是否 最后数据块已发送
if (isLastChunkSent) {
// 如果是,则返回null
return null;
} else {
// 读取下1个chunk(HttpContent类型)
HttpContent nextChunk = nextChunk();
// 将读取到的chunk的可读字节数添加到 整体传输进度 中(但其实还未真正传输完成)
globalProgress += nextChunk.content().readableBytes();
// 返回读取到的chunk
return nextChunk;
}
}
nextChunk()
java
复制代码
private HttpContent nextChunk() {
// 如果 已经是最后数据块(下一数据块将为空)
if (isLastChunk) {
// 标记 最后的数据块已发送 为true
isLastChunkSent = true;
// 紧接着,添加最后的 空内容(这个就是 最后的数据块,它必定是空内容,但不为null哦)
return LastHttpContent.EMPTY_LAST_CONTENT;
}
// 计算size,即 默认大小(8096) 减去 currentBuffer的大小
//(如果currentBuffer为null,则 减去 0,即直接返回 默认大小8096)
int size = calculateRemainingSize();
// 如果currentBuffer 大于等于 默认大小(8096)
if (size <= 0) {
// 将 currentBuffer中的数据 按默认大小(8096)处理后,返回
ByteBuf buffer = fillByteBuf();
// 直接将buffer构建为 HttpContent返回
// (也就是,如果够了8096个字节,则直接从currentBuffer中读取8096个字节)
return new DefaultHttpContent(buffer);
}
// 走到这里,此时 currentBuffer的可读字节数 小于 默认大小(8096),或者 currentBuffer为null
// 如果 currentData 不为 null
// (既然currentBuffer中的可读字节数不够,那么如果还有currentData,
// 就继续从currentData中获取数据添加到curretnBuffer中)
if (currentData != null) {
HttpContent chunk;
// 如果要作 文件上传
if (isMultipart) {
// 从 currentData中获取size大小的数据 添加到 currentBuffer 中
chunk = encodeNextChunkMultipart(size);
} else {
chunk = encodeNextChunkUrlEncoded(size);
}
// 如果chunk不为null(currentBuffer肯定是够了8096,才会返回不为null的chunk)
if (chunk != null) {
// 直接返回
return chunk;
}
// 计算currentBuffer还差多少到 8096
size = calculateRemainingSize();
}
// 使用 已编码的HttpData集合 的 迭代器 判断 是否已无下1个要处理的 已编码的HttpData 了
if (!iterator.hasNext()) {
// 作 最后1个分块的处理
return lastChunk();
}
// size表示当前currentBuffer中还差多少个字节到 默认大小(8096)
// 当还需要读取size字节数据才能将currentBuffer凑够8096,并且当前还有下一个httpData时,继续循环
while (size > 0 && iterator.hasNext()) {
// currentData 拿到 已编码的HttpData集合 中下1个 HttpData
// (currentData记录的 就是 当前需要处理的已编码HttpData)
currentData = iterator.next();
HttpContent chunk;
// 如果要作 文件上传
if (isMultipart) {
// 从 currentData中获取size大小的数据 添加到 currentBuffer 中
chunk = encodeNextChunkMultipart(size);
} else {
// 如果不作文件上传
chunk = encodeNextChunkUrlEncoded(size);
}
// 如果 读取的currentBuffer的可读字节数不够 默认大小(8096)
if (chunk == null) {
// 计算 默认大小(8096) - currentBuffer的可读字节数
size = calculateRemainingSize();
// 继续循环(不够8096,就继续循环)
continue;
}
// 此时读取的 currentBuffer的可读字节数 肯定够8096(够了就直接返回)
return chunk;
}
// 如果size够了,那么上面循环就返回了chunk;
// 如果size不够,那么上面循环就会获取下一个已编码的httpData,继续去读取数据到currentBuffer中;
// 所以走到这里一定是没有下一个已编码的httpData,所以这里就处理最后的chunk了
return lastChunk();
}
calculateRemainingSize()
java
复制代码
private int calculateRemainingSize() {
// 默认大小 是 8096
int size = HttpPostBodyUtil.chunkSize;
// 如果currentBuffer不为null,则将 (默认大小) 减去 (currentBuffer的可读字节数大小)并返回
if (currentBuffer != null) {
size -= currentBuffer.readableBytes();
}
return size;
}
lastChunk()
java
复制代码
private HttpContent lastChunk() {
// 标记 已经没有下1个要处理的 已编码的HttpData 了
isLastChunk = true;
// 如果 当前currentBuffer 为null
if (currentBuffer == null) {
// 标记 最后的数据块已发送 为true
isLastChunkSent = true;
// 紧接着,就把 最后的空内容(这个就是 最后的数据块,它必定是空内容,但不为null哦)
return LastHttpContent.EMPTY_LAST_CONTENT;
}
// 此时 当前currentBuffer 不为null
// 将 currentBuffer 赋值给 buffer
ByteBuf buffer = currentBuffer;
// 将 当前currentBuffer 置为 null
currentBuffer = null;
// 将buffer封装到 HttpContent 返回
// (这种情况,就是 还未发送最后的数据块。
// 此时,就由nextChunk()代码中第一个if判断后,发送最后的数据块)
return new DefaultHttpContent(buffer);
}
encodeNextChunkMultipart(int sizeleft)
java
复制代码
private HttpContent encodeNextChunkMultipart(int sizeleft) {
// 如果 当前处理的HttpData(即currentData) 是null,
if (currentData == null) {
// 则直接返回null
return null;
}
// buffer引用(表示最终要封装到HttpContent中的内容)
ByteBuf buffer;
// 如果 当前处理的HttpData 是 InternalAttribute(表示处理的是编码的头部信息)
if (currentData instanceof InternalAttribute) {
// 创建1个 compositeBuffer,并添加 InternalAttribute 的 value(List<ByteBuf> )
buffer = ((InternalAttribute) currentData).toByteBuf();
// 将 当前处理的HttpData(即 currentData) 重置为 null(因为此时就相当于这个currentData已经赋值给了buffer,就不需要管currentData了)
currentData = null;
} else {
// 如果 当前处理的HttpData 不是 InternalAttribute(表示处理的是实际的内容)
try {
// 从 currentData 中 最多 获取 sizeLeft个字节(注意,不是读取哦),
// (如果currentData中的数据不够sizeLeft个字节,则全部获取)
buffer = ((HttpData) currentData).getChunk(sizeleft);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
// 如果 从currentData中 获取的buffer的容量是0,
// 说明这个currentData已经获取完了(处理完了)
if (buffer.capacity() == 0) {
// currentData重置为null
currentData = null;
// 返回null
return null;
}
}
// 如果 currentBuffer 是null
if (currentBuffer == null) {
// 就将currentBuffer赋值为 buffer
currentBuffer = buffer;
}
else {
// 如果 currentBuffer 不是null,则将 currentBuffer与 buffer 组合起来,
// 然后 赋值给currentBuffer
currentBuffer = wrappedBuffer(currentBuffer, buffer);
}
// 如果 currentBuffer 的可读字节数 小于 默认大小(8096)
if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
// 将currentData 置为 null(注意此时还未将currentBuffer置为null)
currentData = null;
// 返回null(会继续循环,等到大小凑够8096,就返回HttpContent了,而不是null了)
return null;
}
// 此时 currentBuffer 的可读字节数 必定 大于等于 默认大小(8096)
// 也就是说,只有 currentBuffer 的可读字节数 大于等于 默认大小(8096),当前方法才不会返回null
// 将 currentBuffer中的数据 按默认大小(8096)处理后,返回
buffer = fillByteBuf();
// 将 currentBuffer处理后的数据 封装为 HttpContent
return new DefaultHttpContent(buffer);
}
encodeNextChunkUrlEncoded(int sizeleft)
java
复制代码
private HttpContent encodeNextChunkUrlEncoded(int sizeleft) {
// 如果currentData是null,直接返回。因为没有此时没有需要处理的处理
if (currentData == null) {
return null;
}
// 还需要读取的剩余字节数
int size = sizeleft;
ByteBuf buffer;
// 果isKey为true,表示当前需要处理键(key)的部分
if (isKey) {
// 获取当前数据的名称(key),并将其转换为字节序列,存入buffer
String key = currentData.getName();
// 就是 currentData的name
buffer = wrappedBuffer(key.getBytes(charset));
// 将isKey设置为false,表示接下来处理值(value)。
isKey = false;
// 如果currentBuffer为null, 则不需要加上currentBuffer
// 如果currentBuffer不为null,则需要最前面加上currentBuffer,
// 后面再 添加上 =
if (currentBuffer == null) {
currentBuffer = wrappedBuffer(buffer,
wrappedBuffer("=".getBytes(charset)));
} else {
currentBuffer = wrappedBuffer(currentBuffer,
buffer,
wrappedBuffer("=".getBytes(charset)));
}
// 还需要读取的字节数 减去 currentData的name的字节数和"="的字节数
size -= buffer.readableBytes() + 1;
// 如果currentBuffer此时已经够了8096
if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
// 将currentBuffer的8096读取,并返回
//(如果currentBuffer刚好满8096,则currentBuffer置为null)
buffer = fillByteBuf();
// 返回数据
return new DefaultHttpContent(buffer);
}
// 走到这里表示currentBuffer此时不够8096
}
// 接下来,处理值(value)部分
try {
// 从httpData中 (最多)获取size大小的数据
buffer = ((HttpData) currentData).getChunk(size);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
ByteBuf delimiter = null;
// 如果 从httpData中获取的数据 的可读字节数 不够size(说明当前httpData已经读完)
if (buffer.readableBytes() < size) {
// 重置isKey为true
isKey = true;
// 如果还有下1个httpData,则将&作为分隔符;
// 如果没有下1个httpData,则分隔符为null;
delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes(charset)) : null;
}
// 如果buffer的容量为0,表示当前数据currenData已经结束(进入该方法后,就会结束当前方法)
if (buffer.capacity() == 0) {
// 将currentData设置为null(它已经处理完毕了)
currentData = null;
// 如果currentBuffer为null
if (currentBuffer == null) {
// 如果delimiter也为null,则返回null
if (delimiter == null) {
return null;
}
// 如果delimiter不为null,currentBuffer设置为delimiter
else {
currentBuffer = delimiter;
}
} else {
// 如果currentBuffer不为null
// 且如果 delimiter不为null
if (delimiter != null) {
// 将 currentBuffer 与 delimiter 合并
currentBuffer = wrappedBuffer(currentBuffer, delimiter);
}
}
// 如果合并后的currentBuffer的可读字节数达到块大小,则调用fillByteBuf()并返回DefaultHttpContent。
if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
buffer = fillByteBuf();
return new DefaultHttpContent(buffer);
}
// 否则返回null
return null;
}
// 此时,buffer的容量不为0,说明还有数据
// 如果currentBuffer为null
if (currentBuffer == null) {
// 如果 delimeter分隔符不为null
if (delimiter != null) {
// 将 buffer和delimiter合并,赋值给currentBuffer
currentBuffer = wrappedBuffer(buffer, delimiter);
} else {
// delimeter分隔符为null
// 将buffer赋值给currentBuffer
currentBuffer = buffer;
}
} else {
// currentBuffer不为null
// 如果 delimeter分隔符不为null
if (delimiter != null) {
// 将 currentBuffer和buffer和delimiter合并,赋值给currentBuffer
currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
} else {
// 将 currentBuffer和buffer合并,赋值给currentBuffer
currentBuffer = wrappedBuffer(currentBuffer, buffer);
}
}
// 如果currentBuffer的可读字节数小于块大小,那么将currentData设置为null,isKey设置为true,并返回null。
// 这意味着当前数据已经处理完,但是当前块还没有达到块大小,所以等待下一个数据来填充。
if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
// currentData设置为null
currentData = null;
// isKey设置为true
isKey = true;
// 返回null
return null;
}
// 此时说明 currentBuffer 的可读字节数 必定 大于等于 默认大小(8096)
// 将 currentBuffer中的数据 按默认大小(8096)处理后,返回
buffer = fillByteBuf();
// 将 currentBuffer处理后的数据 封装为 HttpContent
return new DefaultHttpContent(buffer);
}
fillByteBuf()
java
复制代码
// 这个方法就是 将 currentBuffer 按 默认大小(8096)去读取,
// 如果不能读完,就读取8096个字节返回,
// 如果能读完,就全部返回,并将currentBuffer置为null
private ByteBuf fillByteBuf() {
// 拿到 currentBuffer 的 可读字节数
int length = currentBuffer.readableBytes();
// 如果 currentBuffer 的 可读字节数 大于 默认大小(8096)
if (length > HttpPostBodyUtil.chunkSize) {
// 从 currentBuffer 中,读取1个 大小固定为 8096 的 分片,并返回
// (此时currentBuffer中还有数据)
return currentBuffer.readRetainedSlice(HttpPostBodyUtil.chunkSize);
} else {
// 如果 currentBuffer 的 可读字节数 小于等于 默认大小(8096)
// currentBuffer 赋值给slice
ByteBuf slice = currentBuffer;
// currentBuffer 置为null
// (此时currentBuffer中所有数据已经交给了slice了,因此,将currentBuffer直接置为null)
currentBuffer = null;
// 返回slice
return slice;
}
}
HttpPostRequestEncoder示例
HttpServer
java
复制代码
public class HttpServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpRequestDecoder());
ch.pipeline().addLast(new HttpResponseEncoder());
ch.pipeline().addLast(new HttpServerHandler());
}
});
ChannelFuture bindFuture = serverBootstrap.bind(8080);
Channel channel = bindFuture.sync().channel();
System.out.println("---启动http服务成功---");
channel.closeFuture().sync();
} catch (Exception e) {
System.out.println("---启动http服务失败---");
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
System.out.println("---http服务已关闭---");
}
}
HttpServerHandler
java
复制代码
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
HttpPostRequestDecoder decoder;
HttpDataFactory factory;
HttpRequest request;
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject httpObject) throws Exception {
System.out.println("[============> channelRead0]: " + httpObject);
if (httpObject instanceof HttpRequest) {
if (decoder != null) {
decoder.destroy();
}
request = (HttpRequest) httpObject;
factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
decoder = new HttpPostRequestDecoder(factory, request);
} else if (httpObject instanceof HttpContent) {
decoder.offer(((HttpContent) httpObject));
if (httpObject instanceof LastHttpContent) {
for (InterfaceHttpData bodyHttpData : decoder.getBodyHttpDatas()) {
if (bodyHttpData.refCnt() > 0) {
handleHttpData(((HttpData) bodyHttpData));
}
}
decoder.destroy();
decoder = null;
sendResponse(ctx, request);
} else {
InterfaceHttpData next = null;
while ((next = decoder.next()) != null) {
try {
handleHttpData(((HttpData) next));
} finally {
next.release();
decoder.removeHttpDataFromClean(next);
}
}
}
} else {
System.out.println("never reach here");
ctx.fireChannelRead(httpObject);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常了...");
cause.printStackTrace();
ctx.close();
}
private void sendResponse(ChannelHandlerContext ctx, HttpRequest request) {
DefaultFullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.wrappedBuffer("请求处理完成".getBytes(CharsetUtil.UTF_8))
);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ChannelFuture channelFuture = ctx.writeAndFlush(response);
// 客户端让我关, 我才关
if (request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true)) {
System.out.println("添加关闭");
channelFuture.addListener(future -> {
ctx.channel().close();
});
}
}
private void handleHttpData(HttpData httpData) {
if (httpData instanceof Attribute) {
Attribute attribute = (Attribute) httpData;
System.out.println("received attribute: " + attribute);
} else if (httpData instanceof FileUpload) {
FileUpload fileUpload = (FileUpload) httpData;
System.out.println("received fileUpload: " + fileUpload);
String filename = fileUpload.getFilename();
String userDir = System.getProperty("user.dir");
File file = new File(userDir + File.separator + "file" + File.separator + filename);
if (file.exists() && file.isFile()) {
file.delete();
}
try {
fileUpload.renameTo(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
HttpClient
java
复制代码
public class HttpClient {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup(1);
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpResponseDecoder());
ch.pipeline().addLast(new HttpRequestEncoder());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpClientHandler());
}
});
ChannelFuture connectFuture = bootstrap.connect("127.0.0.1", 8080);
Channel channel = connectFuture.sync().channel();
System.out.println("---客户端连接http服务成功---");
// 发送单个请求
// sendSingleRequest(channel);
// 发送多次请求
// sendManyRequests(channel);
// 发送文件上传请求
sendPostData(channel);
channel.closeFuture().sync();
} catch (Exception e) {
System.out.println("---客户端发生错误---");
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
private static void sendPostData(Channel channel) throws HttpPostRequestEncoder.ErrorDataEncoderException {
DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/testEncoder");
HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(request, true);
encoder.addBodyAttribute("name", "zzhua");
encoder.addBodyAttribute("age", "18");
encoder.addBodyFileUpload("mfile", new File("D:\\Projects\\practice\\demo-netty\\logs\\app.log"), null, false);
HttpRequest httpRequest = encoder.finalizeRequest();
channel.writeAndFlush(httpRequest);
if (encoder.isChunked()) {
channel.writeAndFlush(encoder);
}
}
private static void sendManyRequests(Channel channel) {
System.out.println("发第1个请求");
sendSimplePost(channel);
// try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}
System.out.println("发第2个请求");
sendSimplePost(channel, true);
}
private static void sendSingleRequest(Channel channel, boolean... needClose) {
System.out.println("发1个请求");
sendSimplePost(channel, needClose);
}
private static ChannelFuture sendSimplePost(Channel channel, boolean... needClose) {
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/test");
if (needClose != null && needClose.length > 0 && needClose[0]) {
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
return channel.writeAndFlush(request);
}
}
HttpClientHandler
java
复制代码
public class HttpClientHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
System.out.println("[客户端received]: " + msg);
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
System.out.println("响应状态码: " + response.status());
System.out.println("响应头: " + response.headers());
} else if (msg instanceof HttpContent) {
HttpContent httpContent = (HttpContent) msg;
System.out.println("响应内容: " + httpContent.content().toString(CharsetUtil.UTF_8));
if (httpContent instanceof LastHttpContent) {
System.out.println("响应结束");
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("客户端异常");
cause.printStackTrace();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端inactive");
}
}
SimpleHttpClient(值得看)
java
复制代码
public class SimpleHttpClient {
private final EventLoopGroup workerGroup;
private final Bootstrap bootstrap;
public SimpleHttpClient() {
workerGroup = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new HttpClientCodec());
ch.pipeline().addLast(new HttpObjectAggregator(65536));
}
});
}
/**
* 发送HTTP请求
* @param url 请求URL
* @param method 请求方法
* @param body 请求体内容
* @param timeout 超时时间(秒)
* @return 响应内容
*/
public CompletableFuture<String> sendRequest(String url, HttpMethod method,
String body, int timeout) {
CompletableFuture<String> future = new CompletableFuture<>();
try {
URI uri = new URI(url);
String host = uri.getHost();
int port = uri.getPort() != -1 ? uri.getPort() :
(uri.getScheme().equals("https") ? 443 : 80);
String path = uri.getRawPath() + (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : "");
// 创建HTTP请求
FullHttpRequest request;
if (body != null && !body.isEmpty()) {
request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
method,
path,
Unpooled.copiedBuffer(body, CharsetUtil.UTF_8)
);
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, body.length());
} else {
request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
method,
path
);
}
// 设置请求头
request.headers()
.set(HttpHeaderNames.HOST, host)
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
.set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP)
.set(HttpHeaderNames.USER_AGENT, "SimpleNettyClient/1.0");
if (body != null && !body.isEmpty()) {
request.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
}
// 连接服务器
ChannelFuture channelFuture = bootstrap.connect(host, port);
channelFuture.addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) {
Channel channel = f.channel();
// 添加响应处理器
channel.pipeline().addLast(new SimpleChannelInboundHandler<FullHttpResponse>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) {
try {
String content = response.content().toString(CharsetUtil.UTF_8);
future.complete("Status: " + response.status() +
"\nHeaders: " + response.headers() +
"\nBody: " + content);
} finally {
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
future.completeExceptionally(cause);
ctx.close();
}
});
// 发送请求
channel.writeAndFlush(request);
} else {
future.completeExceptionally(f.cause());
}
});
} catch (URISyntaxException e) {
future.completeExceptionally(e);
}
// 设置超时
workerGroup.schedule(() -> {
if (!future.isDone()) {
future.completeExceptionally(new RuntimeException("Request timeout"));
}
}, timeout, TimeUnit.SECONDS);
return future;
}
/**
* 发送GET请求(简化方法)
*/
public CompletableFuture<String> get(String url) {
return sendRequest(url, HttpMethod.GET, null, 10);
}
/**
* 发送POST请求(简化方法)
*/
public CompletableFuture<String> post(String url, String body) {
return sendRequest(url, HttpMethod.POST, body, 10);
}
/**
* 关闭客户端
*/
public void shutdown() {
workerGroup.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
SimpleHttpClient client = new SimpleHttpClient();
try {
// 示例1:发送GET请求
System.out.println("=== Sending GET request ===");
String getResponse = client.get("http://127.0.0.1:8080/abc").get(15, TimeUnit.SECONDS);
System.out.println(getResponse);
Thread.sleep(1000); // 等待一下
// 示例2:发送POST请求
System.out.println("\n=== Sending POST request ===");
String postResponse = client.post("http://127.0.0.1:8080/abc",
"Hello from Netty!").get(15, TimeUnit.SECONDS);
System.out.println(postResponse);
} finally {
client.shutdown();
}
}
}