Java 基于 socket 处理 Http 文件上传

毕竟熬夜写的,记录一下。
最近更新:2019-12-19

【参考 木叶之荣 的博客】Java中用Socket实现HTTP文件上传实例

HTTP POST 请求报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /uploadFile HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 14752
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://localhost:8080
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryOINSWz8sKneAKSpY
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Referer: http://localhost:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

------WebKitFormBoundaryOINSWz8sKneAKSpY
Content-Disposition: form-data; name="file"; filename="%E7%AC%9B%E5%8D%A1%E5%B0%94%E5%9E%8B%E6%9C%BA%E6%A2%B0%E8%87%82.png"
Content-Type: application/octet-stream

âPNG
...

------WebKitFormBoundaryOINSWz8sKneAKSpY--
  • âPNG 开始是文件内容
  • --boundary-- 表示文件的结尾


处理 HTTP HEADER

因为我对 socket 工作原理不是很了解,所以这里从拿到 socket 开始。

创建处理 HTTP 请求的类

1
2
3
4
5
6
7
8
9
10
11
public class HttpProcess implements Runnable{
// ...
@Override
public void run() {
try {
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
// ...
} catch (Exception e) { e.printStackTrace(); }
}
}
  • 这里拿到输入流 input 和输出流 output
  • HTTP 请求的数据从 input 中获得
  • 服务器端返回的数据用 output 写入

获得 HTTP 请求的 HEADER

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
StringBuilder sb = new StringBuilder();
int cache;
while (input.available() > 0 && (cache = input.read()) > -1){
if ((char)cache == '\r'){
sb.append((char)cache);
if (input.available() > 0 && (cache = input.read()) > -1){
sb.append((char)cache);
if ((char)cache == '\n'){
if (input.available() > 0 && (cache = input.read()) > -1){
sb.append((char)cache);
if ((char)cache == '\r'){
if (input.available() > 0 && (cache = input.read()) > -1{
sb.append((char)cache);
if ((char)cache == '\n'){
sb.append((char)cache);
break;
}
}
}
}
}
}
} else {
sb.append((char)cache);
}
}

Queue<String> headers = Arrays.stream(sb.toString().split("\r\n")).collect(Collectors.toCollection(LinkedList::new));
Map<String, String> headers = new HashMap<>();
while (!headers.isEmpty()){
String headerString = headers.poll();
String[] keyValue = headerString.split(": ");
headers.put(keyValue[0].toLowerCase(), keyValue[1]);
}
  • \r\n\r\n 表示头部的结束
  • headers 用于保存头部 key 和 value


处理 HTTP body

1
2
String contentType = headers.get("Content-Type");
if (request.getMethod() == HttpMethod.POST && contentType != null && contentType.startsWith("multipart/form-data")){...}
  • 如果 contentType 中包含 multipart/form-data 表示后面是文件的二进制流。

保存结束符号

由上面介绍可知,文件结束符号为 --boundary--

1
String boundary = "\r\n--" + contentType.substring(contentType.indexOf("boundary") + "boundary=".length()) + "--";

读取文件名称

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
String filename = "";
while (input.available() > 0 && (cache = input.read()) > -1){
// 读取文件名称
if ((char)cache == 'f'){
if (input.available() > 0 && (cache = input.read()) == 'i'){
if (input.available() > 0 && (cache = input.read()) == 'l'){
if (input.available() > 0 && (cache = input.read()) == 'e'){
if (input.available() > 0 && (cache = input.read()) == 'n'){
if (input.available() > 0 && (cache = input.read()) == 'a'){
if (input.available() > 0 && (cache = input.read()) == 'm'){
if (input.available() > 0 && (cache = input.read()) == 'e'){
if (input.available() > 0 && (cache = input.read()) == '='){
if (input.available() > 0 && (cache = input.read()) == '\"'){
StringBuilder sbfilename = new StringBuilder();
while (input.available() > 0 && (cache = input.read()) != '\"'){
sbfilename.append((char)cache);
}
filename = URLDecoder.decode(sbfilename.toString(), "UTF-8");
input.read();
break;
}
}
}
}
}
}
}
}
}
}
}

这里我们读取的是

1
2
------WebKitFormBoundaryOINSWz8sKneAKSpY
Content-Disposition: form-data; name="file"; filename="%E7%AC%9B%E5%8D%A1%E5%B0%94%E5%9E%8B%E6%9C%BA%E6%A2%B0%E8%87%82.png"

因此为了读取文件名称,当读到 f 时,判断剩下的是否为 ilename

读取无用行

1
2
3
4
// 读取无用行
while (input.available() > 0 && (cache = input.read()) != '\n'){}
while (input.available() > 0 && (cache = input.read()) != '\n'){}
while (input.available() > 0 && (cache = input.read()) != '\n'){}

读取的内容为

1
2
3
\r\n
Content-Type: application/octet-stream
\r\n

读取文件二进制流

下面是文件的二进制流和结束符号

1
2
3
4
âPNG
...

------WebKitFormBoundaryOINSWz8sKneAKSpY--

读取文件二进制流并写入到临时文件 file 中。

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
DataOutputStream output = new DataOutputStream(new FileOutputStream(file));
boolean isOver = false;
while (input.available() > 0 && (cache = input.read()) > -1 && !isOver){
// 判断是否到达文件末尾
if ((char)cache == '\r'){
byte[] bytes = new byte[boundary.length()];
bytes[0] = (byte)cache;
int i = 1;
while (input.available() > 0 && (cache = input.read()) > -1 && i < boundary.length()){
if ((char)cache == boundary.charAt(i)){
bytes[i] = (byte)cache;
if (i == boundary.length() - 1){
isOver = true;
break;
}
i++;
} else {
output.write(bytes, 0, i);
output.write(cache);
break;
}
}
} else {
output.write(cache);
}
}
output.close();
  • 上面 filename 的查找可效仿 --boundary--,可我不想改了。·