很多人应该对 HTTP 的 multipart/form-data 不陌生,一般常用于上传文件,虽然大部分人可以很熟练的使用,但是对于 multipart/form-data 如何对请求编码的却知之甚少,这篇博客就来简单介绍下其中的编码过程以及 Netty 的实现。
首先我们使用 Postman 发送一个 HTTP multipart/form-data 类型的请求,通过抓包发现请求内容。
Request Header:
Request Body :
Request Body 完成内容:
从第二个图中可以看出,Reqeust Body 内容是由 Content-Type 中的 boundary 所分隔,内容中的 Content-Disposition 指定 form 的 key 和上传的文件名,Content-Type 指定文件类型,接着一个换行,后面就是文件的内容,同样,服务器在接受到请求后,也会按照这种格式来进行解码。首先来看看 Netty 是如何构造 multipart 请求内容的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "http://127.0.0.1:1028/test-file" ); HttpPostRequestEncoder bodyRequestEncoder = new HttpPostRequestEncoder(factory, request, true , HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738)); File file = new File("test.txt" ); bodyRequestEncoder.addBodyFileUpload("file" , file, "text/plain" , true ); bodyRequestEncoder.finalizeRequest(); channel.write(request); if (bodyRequestEncoder.isChunked()) { channel.write(bodyRequestEncoder); } channel.flush(); bodyRequestEncoder.cleanFiles();
其中最关键的地方是第 7 行 和 10 行代码。
HttpPostRequestEncoder 在初始化时会根据传入的 multipart 是否为 true 来构造对应的 RequestEncoder:
1 2 3 4 5 6 7 8 9 10 public HttpPostRequestEncoder (HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset, EncoderMode encoderMode) throws ErrorDataEncoderException { isMultipart = multipart; multipartHttpDatas = new ArrayList<InterfaceHttpData>(); this .encoderMode = encoderMode; if (isMultipart) { initDataMultipart(); } }
当我们传入 multipart 为 true 时,HttpPostRequestEncoder 就会生成一个唯一的 multipart boundary,而这个 boundary 就是之前图中的分割界线。
接着来看 HttpPostRequestEncoder.addBodyFileUpload()
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void addBodyFileUpload (String name, File file, String contentType, boolean isText) throws ErrorDataEncoderException { FileUpload fileUpload = factory.createFileUpload(request, name, file.getName(), scontentType, contentTransferEncoding, null , file.length()); try { fileUpload.setContent(file); } catch (IOException e) { throw new ErrorDataEncoderException(e); } addBodyHttpData(fileUpload); }
通过 HttpDataFactory 构造 FileUpload 后,添加至 bodyListDatas 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public void addBodyHttpData (InterfaceHttpData data) throws ErrorDataEncoderException { bodyListDatas.add(data); if (data instanceof FileUpload) { FileUpload fileUpload = (FileUpload) data; InternalAttribute internal = new InternalAttribute(charset); if (!multipartHttpDatas.isEmpty()) { internal.addValue("\r\n" ); } internal.addValue("--" + multipartDataBoundary + "\r\n" ); internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; " + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n" ); internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " + fileUpload.length() + "\r\n" ); internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType()); String contentTransferEncoding = fileUpload.getContentTransferEncoding(); if (contentTransferEncoding != null && contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) { internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": " + HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() + "\r\n\r\n" ); } else if (fileUpload.getCharset() != null ) { internal.addValue("; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n" ); } else { internal.addValue("\r\n\r\n" ); } multipartHttpDatas.add(internal); multipartHttpDatas.add(data); globalBodySize += fileUpload.length() + internal.size(); } }
只保留了一些关键代码,删除了 attribute 和 mix 相关代码。
根据 Netty 原有的注释以及我后面补充的注释,已经有了非常完整的 FileUpload 添加过程,接下来看 finalizeRequest() 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public HttpRequest finalizeRequest () throws ErrorDataEncoderException { if (isMultipart) { String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '=' + multipartDataBoundary; headers.add(HttpHeaderNames.CONTENT_TYPE, value); } if (isMultipart) { iterator = multipartHttpDatas.listIterator(); } }
finalizeRequest 除了设置 Content-Type 外还构造了 ListIterator\ iterator ,而实际的数据内容则是遍历 iterator 来读取的。
1 2 3 4 5 6 7 8 9 10 11 private HttpContent nextChunk () throws ErrorDataEncoderException { while (size > 0 && iterator.hasNext()) { currentData = iterator.next(); HttpContent chunk; if (isMultipart) { chunk = encodeNextChunkMultipart(size); } return chunk; } }
在数据传输时,通过 nextChunk 中遍历 iterator 获取每一个 InterfaceHttpData 的内容并传输到对应的 server 中。
至此 HTTP multipart/form-data 及 Netty 中的实现基本分析完毕,错误之处欢迎指出。