前言
为了实现轻量级的HttpServer,可以使用JDK自带的HttpServer API,那么如何实现文件上传与下载,其实要实现这些需要理解Http协议的输入与输出标记。
1. HttpServer文件下载
show me the code:😋
package com.feng.server.http;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.*;
import java.net.InetSocketAddress;
public class InnerHttpServer {
public static void main(String[] args) throws IOException {
// 创建 http 服务器, 绑定本地 8383 端口
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8383), 0);
httpServer.createContext("/download", new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
System.out.println("---url: " + httpExchange.getRequestURI().getQuery());
File file = new File("/Users/huahua/Downloads/closeSmps.jpg");
OutputStream out = httpExchange.getResponseBody();
try (FileInputStream in = new FileInputStream(file)){
httpExchange.getResponseHeaders().add("Content-Disposition", "attachment;filename="+file.getName());
httpExchange.sendResponseHeaders(200, file.length());
byte[] fileBytes = new byte[(int) file.length()];
in.read(fileBytes);
out.write(fileBytes);
} finally {
out.flush();
out.close();
}
}
});
httpServer.start();
// httpServer.stop(0);
}
}
使用postman模拟下载

然后


可以确认下载成功了,注意
1.1 header设置
需要设置header,否则下载的文件名会是默认的response.xxx;header必须在返回码前设置生效,否则设置没用。
"Content-Disposition", "attachment;filename="+file.getName()
2. 文件上传
文件上传默认使用form表单上传,那么上传的文件就在requestbody里面,里面存储了form的表单值,需要解析2进制,还原真实的数据。
2.1 form表单,其实是一种协议
form表单传输时,有text与file。file专门用于传输文件。

通过抓包,form表单传输的body如下:

结尾如下

而在Request的Header里面存储了分隔符,那么如何解决拆包的问题:固定分隔符+空行

其实也可以通过分隔符+(header(存放body length)+body)的方式分割,只是这里并没有存放body的length,而是通过空行分割。
2.2 常见的协议处理方式
估计设计认为form表单不会存储很多东西吧。数据拆分一直是数据传输的一个关键点,包括netty,这就是典型的TCP粘包和拆包处理
粘包、拆包的解决方案:自定义通信协议
目前业界主流的协议(protocol)方案主要有3种:
| 定长协议 | 比如约定:每4096个字节,表示一个有效报文。 |
| 特殊字符分隔符协议 | 在数据包尾部增加 \n 或者 \r 等特殊字符进行分割 ,比如这里的form,使用了“\r\n”分割数据key与数据value。 |
| 长度编码协议 | 将报文分为头(header)和体(body),头中用一个int型数据(4字节),表示体的长度。在解析时,优先读取体长度Length,其值为实际消息体内容长度,然后按照长度读取的内容,认为是有效报 |
2.3 form协议解析
form协议的解析,笔者自己写了一种简单的方式。
定义解析器HttpRequestFormResolver
package com.feng.server.http;
import com.sun.net.httpserver.Headers;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class HttpRequestFormResolver {
private static final int PARAM_INDEX = "Content-Disposition: form-data; ".length();
public static List<ParamItem> resolveForm(Headers headers, byte[] body) throws IOException {
String contentType = headers.getFirst("Content-type");
String boundary = contentType.substring(contentType.indexOf("=") + 1);
boundary = "--" + boundary; //size 52
byte[] boundaryBytes = boundary.getBytes(StandardCharsets.UTF_8);
List<ParamItem> paramItems = new LinkedList<>();
List<Integer> boundaryIndex = boundaryIndex(body, boundaryBytes);
int paramSize = boundaryIndex.size() - 1;
int boundaryLen = boundaryBytes.length;
byte[] sep = "\r\n".getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < paramSize; i++) {
ParamItem paramItem = resolveParam(body, boundaryIndex.get(i)+boundaryLen+2, boundaryIndex.get(i+1), sep);
paramItems.add(paramItem);
}
return paramItems;
}
/**
* 单个参数解析
*/
private static ParamItem resolveParam(byte[] body, int start, int end, byte[] sep) {
int count = 0;
int cursor = start;
ParamItem paramItem = null;
for (int i = start; i < end; i++) {
for (int j = 0; j < 2; j++) {
if (body[i + j] == sep[j]) {
count++;
} else {
break;
}
}
if (count == 2) {
byte[] line = new byte[i-cursor];
System.arraycopy(body, cursor, line, 0,i-cursor);
//参数的key value分隔符 \r\n
if (isLineBlank(line)) {
cursor = i+2;
if (paramItem == null) {
return null;
}
if (paramItem.getType().equals("text")) {
byte[] val = new byte[end - cursor-2];
System.arraycopy(body, cursor, val, 0, end-cursor-2);
paramItem.setVal(new String(val));
} else {
paramItem.setStartIndex(cursor);
paramItem.setEndIndex(end);
}
break;
} else {
String lineStr = new String(line);
if (lineStr.startsWith("Content-Disposition: form-data; ")) {
paramItem = resolveParam(lineStr);
}
cursor = i;
}
i++;
}
count = 0;
}
return paramItem;
}
private static ParamItem resolveParam(String lineStr){
lineStr = lineStr.substring(PARAM_INDEX);
String[] kVs = lineStr.split(";");
ParamItem paramItem = new ParamItem();
paramItem.setType("text");
for (String kV : kVs) {
String[] k_v = kV.trim().split("=");
if ("name".equals(k_v[0])) {
paramItem.setName(k_v[1].replace("\"", ""));
} else if ("filename".equals(k_v[0])) {
paramItem.setFilename(k_v[1].replace("\"", ""));
paramItem.setType("file");
}
}
return paramItem;
}
private static boolean isLineBlank(byte[] line){
if (line.length == 0) {
return true;
}
byte[] sep = "\r\n".getBytes(StandardCharsets.UTF_8);
if (line.length == 2) {
if (line[0] == sep[0] && line[1] == sep[1]) {
return true;
}
}
return false;
}
/**
* 索引参数的index
*/
private static List<Integer> boundaryIndex(byte[] body, byte[] boundary){
int count = 0;
List<Integer> list = new ArrayList<>();
int length = body.length;
int boundaryLen = boundary.length;
for (int i = 0; i < length; i++) {
for (int j = 0; j < boundaryLen; j++) {
if (i + j == length) {
return list;
}
if (body[i + j] == boundary[j]) {
count++;
} else {
break;
}
}
if (count == boundaryLen) {
list.add(i);
i += boundaryLen - 1;
}
count = 0;
}
return list;
}
public static class ParamItem {
private String type;//text file
private String name;
private String filename;
private String val;
private int startIndex;
private int endIndex;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
public int getStartIndex() {
return startIndex;
}
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
public int getEndIndex() {
return endIndex;
}
public void setEndIndex(int endIndex) {
this.endIndex = endIndex;
}
@Override
public String toString() {
return "ParamItem{" +
"type='" + type + '\'' +
", name='" + name + '\'' +
", filename='" + filename + '\'' +
", val='" + val + '\'' +
", startIndex=" + startIndex +
", endIndex=" + endIndex +
'}';
}
}
}
http server
package com.feng.server.http;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.net.InetSocketAddress;
import java.util.List;
public class InnerHttpServer {
public static void main(String[] args) throws IOException {
// 创建 http 服务器, 绑定本地 8383 端口
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8383), 0);
httpServer.createContext("/upload", new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
// System.out.println(IOUtils.toString(httpExchange.getRequestBody()));
System.out.println("url: " + httpExchange.getRequestURI().getQuery());
Headers headers = httpExchange.getRequestHeaders();
int length = Integer.parseInt(headers.getFirst("Content-length"));
InputStream in = httpExchange.getRequestBody();
byte[] body = IOUtils.toByteArray(in, length);
List<HttpRequestFormResolver.ParamItem> params = HttpRequestFormResolver.resolveForm(headers, body);
for (HttpRequestFormResolver.ParamItem paramItem : params) {
if (paramItem.getType().equals("text")) {
System.out.println(paramItem);
} else {
//write file
File file = new File("/Users/huahua/Desktop/upload/"+paramItem.getFilename());
if (file.exists()) {
file.delete();
}
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(body, paramItem.getStartIndex(), paramItem.getEndIndex()- paramItem.getStartIndex());
fileOutputStream.close();
}
}
httpExchange.sendResponseHeaders(200, "hello".length());
OutputStream out = httpExchange.getResponseBody();
out.write("hello".getBytes());
in.close();
out.flush();
out.close();
}
});
httpServer.start();
// httpServer.stop(0);
}
}
解析思路,获取字节码,然后获取分隔符,逐段解析,这里其实还可以优化:
循环合并解析
package com.feng.server.http;
import com.sun.net.httpserver.Headers;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class HttpRequestFormOtherResolver {
private static final int PARAM_INDEX = "Content-Disposition: form-data; ".length();
public static List<ParamItem> resolveForm(Headers headers, byte[] body) throws IOException {
String contentType = headers.getFirst("Content-type");
String boundary = contentType.substring(contentType.indexOf("=") + 1);
boundary = "--" + boundary; //size 52
byte[] boundaryBytes = boundary.getBytes(StandardCharsets.UTF_8);
byte[] sep = "\r\n".getBytes(StandardCharsets.UTF_8);
List<ParamItem> paramItems = boundaryIndex(body, boundaryBytes, sep);
return paramItems;
}
private static ParamItem resolveParam(String lineStr) {
lineStr = lineStr.substring(PARAM_INDEX);
String[] kVs = lineStr.split(";");
ParamItem paramItem = new ParamItem();
paramItem.setType("text");
for (String kV : kVs) {
String[] k_v = kV.trim().split("=");
if ("name".equals(k_v[0])) {
paramItem.setName(k_v[1].replace("\"", ""));
} else if ("filename".equals(k_v[0])) {
paramItem.setFilename(k_v[1].replace("\"", ""));
paramItem.setType("file");
}
}
return paramItem;
}
private static boolean isLineBlank(byte[] line) {
if (line.length == 0) {
return true;
}
byte[] sep = "\r\n".getBytes(StandardCharsets.UTF_8);
if (line.length == 2) {
if (line[0] == sep[0] && line[1] == sep[1]) {
return true;
}
}
return false;
}
/**
* 解析索引的同时解析参数
* 还可以进一步优化,一边读取字节码一边解析,进一步优化性能,降低算法复杂度
*/
private static List<ParamItem> boundaryIndex(byte[] body, byte[] boundary, byte[] sep) {
int count = 0;
int sep_count = 0;
List<ParamItem> list = new ArrayList<>();
int length = body.length;
int boundaryLen = boundary.length;
int cursor = boundaryLen + 2;
ParamItem paramItem = null;
boolean paramStart = false;
boolean paramEnd = false;
for (int i = 0; i < length; i++) {
for (int j = 0; j < boundaryLen; j++) {
if (i + j == length) {
return list;
}
if (body[i + j] == boundary[j]) {
count++;
} else {
break;
}
}
//边界索引
if (count == boundaryLen) {
if (i > 0) //参数结束标记,同时也是下一个参数的开始解析
paramEnd = true;
//开始参数解析标记
paramStart = true;
i += boundaryLen + 2 - 1;
}
//参数解析
if (paramStart) {
for (int j = 0; j < 2; j++) {
if (body[i + j] == sep[j]) {
sep_count++;
} else {
break;
}
}
if (sep_count == 2) {
byte[] line = new byte[i - cursor];
System.arraycopy(body, cursor, line, 0, i - cursor);
if (isLineBlank(line)) {
//解析参数值,此时既不是参数开始,也不是参数结束
paramStart = false;
//参数结束才能为true,由边界决定
paramEnd = false;
cursor = i + 2;
} else {
String lineStr = new String(line);
if (lineStr.startsWith("Content-Disposition: form-data; ")) {
paramItem = resolveParam(lineStr);
}
cursor = i;
}
i += 1;
}
sep_count = 0;
}
//参数结束,处理参数的值
if (paramEnd) {
if (paramItem == null) {
return null;
}
if (paramItem.getType().equals("text")) {
byte[] val = new byte[i - cursor - boundaryLen - 1 - 2];
System.arraycopy(body, cursor, val, 0, i - cursor - boundaryLen - 1 - 2);
paramItem.setVal(new String(val));
} else {
paramItem.setStartIndex(cursor);
paramItem.setEndIndex(i - boundaryLen - 1 - 2);
}
list.add(paramItem);
cursor = i+1;
//处理完成需要更新标记
paramEnd = false;
}
count = 0;
}
return list;
}
public static class ParamItem {
private String type;//text file
private String name;
private String filename;
private String val;
private int startIndex;
private int endIndex;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
public int getStartIndex() {
return startIndex;
}
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
public int getEndIndex() {
return endIndex;
}
public void setEndIndex(int endIndex) {
this.endIndex = endIndex;
}
@Override
public String toString() {
return "ParamItem{" +
"type='" + type + '\'' +
", name='" + name + '\'' +
", filename='" + filename + '\'' +
", val='" + val + '\'' +
", startIndex=" + startIndex +
", endIndex=" + endIndex +
'}';
}
}
}
http server,其实还可以优化,一边读取一边解析,读取多少字节,解析多少字节,在流的读取完成循环,进一步降低复杂度。
package com.feng.server.http;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.net.InetSocketAddress;
import java.util.List;
public class InnerHttpServer {
public static void main(String[] args) throws IOException {
// 创建 http 服务器, 绑定本地 8383 端口
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8383), 0);
httpServer.createContext("/upload", new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
// System.out.println(IOUtils.toString(httpExchange.getRequestBody()));
System.out.println("url: " + httpExchange.getRequestURI().getQuery());
Headers headers = httpExchange.getRequestHeaders();
int length = Integer.parseInt(headers.getFirst("Content-length"));
InputStream in = httpExchange.getRequestBody();
byte[] body = IOUtils.toByteArray(in, length);
List<HttpRequestFormOtherResolver.ParamItem> params = HttpRequestFormOtherResolver.resolveForm(headers, body);
for (HttpRequestFormOtherResolver.ParamItem paramItem : params) {
if (paramItem.getType().equals("text")) {
System.out.println(paramItem);
} else {
//write file
File file = new File("/Users/huahua/Desktop/upload/"+paramItem.getFilename());
if (file.exists()) {
file.delete();
}
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(body, paramItem.getStartIndex(), paramItem.getEndIndex()- paramItem.getStartIndex());
fileOutputStream.close();
}
}
httpExchange.sendResponseHeaders(200, "hello".length());
OutputStream out = httpExchange.getResponseBody();
out.write("hello".getBytes());
in.close();
out.flush();
out.close();
}
});
httpServer.start();
// httpServer.stop(0);
}
}
参数分隔符,参数信息同时解析,可以节约一定的时间,不过参数一般比较小,所以对性能没有严格要求可以不管。
经实践,文件上传成功,且参数传递正确


总结
自己实现http的文件下载与上传,自定义了http的form表单字节码解析,可以非常清晰的知道传输协议。其实在Tomcat或者Spring boot都是封装了解析协议的,可以直接拿到数据,解析算法还更高效,只是需要明白参数是怎么得来的。

179

被折叠的 条评论
为什么被折叠?



