如何解析multipart/form-data格式的HTTP请求?
目前我正在开发一个使用Socket通信的Java简易文件服务器。在项目过程中,我对HTTP请求的格式产生了兴趣,希望在项目中复现相关解析逻辑,并且仅使用底层API(Socket)来了解其底层工作原理。
问题十分明确,位于本文最后一部分,其余内容为问题说明及我的理解。
以下示例中我会使用简化的Socket代码来展示我的理解,同时假设已存在以下变量:
Socket socket = server.accept(); DataInputStream input = new DataInputStream(socket.getInputStream()); DataOutputStream output = new DataOutputStream(socket.getOutputStream());
我认为解析application/x-www-form-urlencoded格式的HTTP请求(或类似格式)的逻辑是清晰的,如果我的理解有误,欢迎指正。示例请求如下:
POST / HTTP/1.1 Content-Length: 64 Content-Type: application/x-www-form-urlencoded name=John%20User&request=Send%20me%20one%20of%20your%20catalogue
服务器可按如下方式解析该请求:
// 读取请求起始行 String startLine = input.readline(); ... // 读取所有请求头,直到遇到空行 String header; while (!(header = input.readLine()).equals("")) { ... } // 读取请求体 int len = <Content-Length请求头的值>; byte[] body = new byte[len]; input.read(body, 0, len); ...
multipart/form-data格式的HTTP请求 这是我的核心问题。以下是一个multipart请求示例:
POST / HTTP/1.1 Content-Type: multipart/form-data; boundary=boundary Content-Length: 465 --boundary Content-Disposition: form-data; name="name" John --boundary Content-Disposition: form-data; name="avatar"; filename="avatar.jpg" Content-Type: image/jpeg <some binary data> --boundary--
我不确定应如何解析此类请求。请求起始行和请求头的解析方式与之前的示例类似,但请求体(尤其是包含二进制数据时)的处理方式让我困惑。我有一些思路,但认为它们并不正确或不够完善。
我尝试将请求体读取为字符串,再通过boundary值将其拆分多个部分,之后服务器可以对这些拆分后的部分进行处理(例如提取头信息、处理内容等)。代码大致如下:
int len = <Content-Length请求头的值>; byte[] byteBody = new byte[len]; input.read(byteBody, 0, len); String boundary = <从请求头中提取的值>; String body = new String(byteBody); String bodyParts = body.split(boundary) ...
但随即遇到了问题:这种方式无法处理二进制数据。将byte[]转换为String再转换回byte[](用于在服务器上写入文件)的操作对文件无效,原因是默认编码为ASCII,不支持负值。我做了一个小测试,结果如下:
byte[] arr1 = new byte[] { -1, -2, -3 }; String str1 = new String(arr1); byte[] arr2 = str1.getBytes(); // arr1 = [-1, -2, -3] // arr2 = [-17, -65, -67, -17, -65, -67, -17, -65, -67]
了解到这一点后,我寻找解决方案。我认为base64编码可以解决该问题,但这似乎是一种变通方案,存在以下缺点:
- 会增大文件体积;
- 需要服务器和客户端都实现
base64的编解码逻辑。
我也查阅了许多示例,并搭建了一个简单的Node.js服务器,证实multipart/form-data请求体中的文件确实可以以二进制格式发送,而非base64格式。
如何在不将请求体转换为字符串的前提下,使用boundary值将其拆分为多个部分?我考虑过逐字节读取并检测边界,但这似乎不是一个高效或合适的方法。我非常想知道完成这项任务的正确方式以及此类请求体的解析标准是什么。
内容的提问来源于stack exchange,提问作者ushka1

