在工作中碰到需要在服务端中转Http请求的需求,一般的请求其实问题不大,用apache的HttpClient 提供的jar包即可解决问题,但是带文件上传的请求中转,遇到了一些小麻烦,在此记录一下。
业务场景是这样的,由于是做系统升级,升级的同时不影响老版本的使用,同时又不和新版本的代码耦合,所以做了一个适配模块,这个模块是个单独的项目。在前端请求新版本系统时会先进入新版本系统的拦截器中,在拦截器中判断该请求是调用新版本的系统接口还是老版本的系统接口,如果调用老版本系统接口,就会先走适配模块,在适配模块中调用老系统的接口。整个流程大致如下图:
在适配器中使用了apache的HttpClient包,而在拦截器中使用的是jdk自带的java.net.URL类。因此两种方式我都会提供,此外还可以通过java.net.socket类实现Http请求,这几种种方式大同小异,只是HttpClient做了一下封装。最重要的还是要对Http请求协议有比较全面的认识,然后模拟出一个符合协议标准的请求报文就可以像浏览器那样模拟Http请求了。
1 . Http请求报文
通过Fiddler抓包工具,可以抓取网络请求,查看请求信息,下面是我抓取的Get和Post请求的请求报文
(1) . Get请求
GET http://localhost:8081/test?name=111 HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
(2) . Post请求
POST http://localhost:8081/upload HTTP/1.1
cache-control: no-cache
Postman-Token: 9316956c-b2a9-43a0-9a5c-87a4f5eead7d
User-Agent: PostmanRuntime/6.3.2
Accept: */*
Host: localhost:8081
accept-encoding: gzip, deflate
content-type: multipart/form-data; boundary=--------------------------535635233106673687621220
content-length: 355
Connection: keep-alive
----------------------------535635233106673687621220
Content-Disposition: form-data; name="name"
lsj
----------------------------535635233106673687621220
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
ļ ϴ ݣ 2333333
д 㶫
----------------------------535635233106673687621220--
大部分属性大致看一下也能明白它的含义,需要注意的是Post请求中content-type是multipart/form-data时,必须指定boundary,这是属性是参数的分隔符。明白请求报文的格式后,只需中拼凑出这样格式相同的报文头就可以了。
2 . 响应报文
HTTP/1.1 200
X-Application-Context: application:8081
Content-Type: text/html;charset=UTF-8
Content-Length: 3
Date: Sun, 01 Jul 2018 00:27:47 GMT
111
3 . 模拟客户端请求
下面使用了3种方式模拟客户端请求
(1) . HttpCilent
需要用到apache的两个jar包
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.5</version>
</dependency>
客户端代码如下:
/**
* 模拟客户端表单提交
* */
public static void fileUpload(File file,String url){
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(url);
//设置请求头
post.setHeader("Accept-Language","zh-CN,zh;q=0.8");
try {
//设置参数
//文件附件
MultipartEntityBuilder mEntityBuilder = MultipartEntityBuilder.create();
mEntityBuilder.addBinaryBody("file", file);
//其他参数,类似表单的Input
mEntityBuilder.addTextBody("name", "lsj");
mEntityBuilder.addTextBody("age","20");
post.setEntity(mEntityBuilder.build());
CloseableHttpResponse response = client.execute(post);
HttpEntity entity = response.getEntity();
String data = EntityUtils.toString(entity, StandardCharsets.UTF_8);
System.out.println(data);
} catch(Exception e1){
e1.printStackTrace();
} finally {
try{
client.close();
}catch(Exception ex){
ex.printStackTrace();
}
}
}
(2) . URL
URL的方式需要自己去拼接请求报文的格式
/**
* 模拟在客户端表单提交
* */
public static void fileUpload(File file, String urlPath){
try {
// 1.创建URL对象
URL url = new URL(urlPath);
// 2.构建附件输入流
// io文件对象转为输入流
InputStream fileInputStream = new FileInputStream(file);
// 3.模拟表单中普通input的数据
Map<String,String> map = new HashMap<>();
map.put("name","lsj");
//核心方法------发送请求---------
InputStream responseStream = sendUrlRequest(url,fileInputStream,map);
//打印响应数据
if(responseStream != null){
// 将字节流包装成字符流
InputStreamReader inputStreamReader = new InputStreamReader(responseStream);
// 创建一个输入缓冲区对象,将要输入的字符流对象传入
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
// 创建缓冲字符对象
StringBuilder stringBuilder = new StringBuilder();
//
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
// 打印响应数据
System.out.println(stringBuilder.toString());
}else{
System.out.println("请求异常 ! ");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 请求地址
* url
*
* 请求中的附件
* fileInputStream
* */
private static InputStream sendUrlRequest(URL url,InputStream fileInputStream,Map<String,String> dataMap){
try {
// 获取连接对象
URLConnection connection = url.openConnection();
// 设置请求类型 "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
((HttpURLConnection) connection).setRequestMethod("POST");
// 设置连接超时时间
connection.setConnectTimeout(1000);
//设置读取超时时间
connection.setReadTimeout(1000);
// 设置允许输入流输入数据到本机
connection.setDoOutput(true);
// 设置允许输出流输出数据到服务器
connection.setDoInput(true);
// 设置不使用缓存
connection.setUseCaches(false);
// 设置请求头参数
connection.setRequestProperty("Accept-Charset", "utf-8");
// 设置请求参数中的内容类型为multipart/form-data,设置请求内容的分割线为******
connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY);
// 从连接对象中获取输出流
OutputStream outputStream = connection.getOutputStream();
// 实例化数据输出流对象,将输出流传入
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
// 表单中的普通数据,写入到输出流对象-----------------------------开始
// 获取表单中上传控件之外的控件数据,写入到输出流对象(根据抓包的内容格式拼凑字符串);
if (!dataMap.isEmpty()) {
for (Map.Entry<String, String> entry : dataMap.entrySet()) {
String key = entry.getKey(); // 键,相当于表单提交中Input的name属性
String value = dataMap.get(key); // 值,相当于表单提交中Input的输入值
dataOutputStream.writeBytes(PREFIX + BOUNDARY + NEWLINE); // 像请求体中写分割线,就是前缀+分界线+换行
dataOutputStream.writeBytes("Content-Disposition: form-data;name=" + key + NEWLINE);
dataOutputStream.writeBytes(NEWLINE); // 空行,一定不能少,键和值之间有一个固定的换行
dataOutputStream.writeBytes(URLEncoder.encode(value, "utf-8")); // 将值写入,设置编码
dataOutputStream.writeBytes(NEWLINE); // 换行
}
}
// 表单中的普通数据,写入到输出流对象-----------------------------结束
// 表单中上传附件的数据,写入到输出流对象-----------------------------开始
// 向数据输出流中写出分割符
dataOutputStream.writeBytes(PREFIX + BOUNDARY + NEWLINE);
// 向数据输出流中写出文件参数名与文件名
dataOutputStream.writeBytes("Content-Disposition:form-data;name=file;filename=abc.txt" + NEWLINE);
// 向数据输出流中写出结束标志, 重要!!不要忘了
dataOutputStream.writeBytes(NEWLINE);
// 定义缓冲区大小
int bufferSize = 1024;
// 定义字节数组对象,用来读取缓冲区数据
byte[] buffer = new byte[bufferSize];
// 定义一个整形变量,用来存放当前读取到的文件长度
int length ;
// 循环从文件输出流中读取1024字节的数据,将每次读取的长度赋值给length变量,直到文件读取完毕,值为-1结束循环
while ((length = fileInputStream.read(buffer)) != -1) {
// 向数据输出流中写出数据
dataOutputStream.write(buffer, 0, length);
}
// 每写出完成一个完整的文件流后,需要向数据输出流中写出结束标志符
dataOutputStream.writeBytes(NEWLINE);
// 关闭文件输入流
fileInputStream.close();
// 表单中上传附件的数据,写入到输出流对象-----------------------------结束
// 向数据输出流中写出分隔符
dataOutputStream.writeBytes(PREFIX + BOUNDARY + PREFIX + NEWLINE);
// 刷新数据输出流
dataOutputStream.flush();
// 依次关流,先开后关原则
fileInputStream.close();
dataOutputStream.close();
outputStream.close();
// 从连接对象中获取字节输入流,即response,response中的数据以流的形式返回
return connection.getInputStream();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
(3) . Socket
public static void sendTest(){
Socket socket;
try {
String para = "name=abc&age=22";
int length = para.length();
socket = new Socket("localhost", 8081);
BufferedWriter wr = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),
"UTF8"));
InputStream ins = socket.getInputStream();
StringBuffer sb = new StringBuffer();
sb.append("POST /test HTTP/1.1\r\n");// 注意\r\n为回车换行
sb.append("Accept-Language: zh-cn\r\n");
sb.append("Connection: Keep-Alive\r\n");
sb.append("Host:localhost\r\n");
sb.append("Content-Length:"+length+"\r\n");
sb.append("Content-Type: application/x-www-form-urlencoded\r\n");
sb.append("\r\n");
sb.append(para);
// 接收Web服务器返回HTTP响应包
wr.write(sb.toString());
wr.flush();
BufferedReader rd = new BufferedReader(new InputStreamReader(ins));
String line;
while ((line = rd.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
这里一定要注意的是Content-Length的计算,因为在HTTP协议中,如果你的长度计算错误,那么服务器就会一直在等待读取,导致请求超时。
4 . 在服务端中转Http请求
上面的代码只是模拟客户端发送http请求,而如果需要在服务端请求第三方的接口,并把接口返回的数据再返回到前台,方式就稍微有点区别。主要的区别就是对返回的输入流的处理,只要把返回的输入流复制到当前请求的response的输出流中即可,代码如下:
(1) . HttpCilent
/**
* 模拟在服务端中转请求
*
* request 当前请求的Request对象
* response 当前请求的Response对象
* forWardUrlPath 要请求的url
* */
public static void fileUpload(MultipartFile multipartFile, String forWardUrlPath , HttpServletResponse response){
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(forWardUrlPath);
try {
//文件附件
MultipartEntityBuilder mEntityBuilder = MultipartEntityBuilder.create();
// 附件名称
String fileName = multipartFile.getOriginalFilename();
mEntityBuilder.addBinaryBody("file", multipartFile.getInputStream(), ContentType.APPLICATION_OCTET_STREAM,fileName);
//其他参数,类似表单的Input
mEntityBuilder.addTextBody("name", "lsj");
mEntityBuilder.addTextBody("age","20");
post.setEntity(mEntityBuilder.build());
CloseableHttpResponse forWardResponse = client.execute(post);
//获取返回的输入流
InputStream inputStream = forWardResponse.getEntity().getContent();
//将返回的数据输入流,写到当请请求response的输出流中
forWardStream(inputStream,response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
(2) . URL
/**
* 模拟在服务端中转请求
*
* request 当前请求的Request对象
* response 当前请求的Response对象
* forWardUrlPath 要请求的url
* */
public static void fileUploadForWard(String forWardUrlPath, HttpServletRequest request, HttpServletResponse response, Map<String, String> dataMap){
try {
// 1.创建URL对象
URL url = URI.create(forWardUrlPath).toURL();
// 2.通过HttpServletRequest 获取到上传的附件
Part part = request.getPart("file");
// 构建附件输入流
// 也可通过 MultipartFile 对象,通过该对象的 getInputStream()方法获得附件的输入流
InputStream fileInputStream = part.getInputStream();
// 3.模拟表单中普通input的数据,可通过request获取
dataMap.put("name","lsj");
//核心方法------发送请求---------
InputStream responseStream = sendUrlRequest(url,fileInputStream,dataMap);
if(responseStream != null){
// 这里把返回的inputStream写入到response输出流中
forWardStream(responseStream,response.getOutputStream());
}else{
System.out.println("请求异常 !");
}
} catch (Exception e) {
e.printStackTrace();
}
}