内容大纲
1. 背景
相信大家对HttpURLConnection
这个类的使用并不陌生,通常我们都是通过这个类来封装一个请求后台数据的方法,暴露给外部使用。比如在之前的博客中:
- Android文件下载——多线程下载
使用setRequestProperty
方法来设置请求头信息。但以往的好几篇博客和自己使用过程中都是默认GET
方式请求,参数传递只需要在url
中拼接即可,尚未使用POST
方式。这里就来简单使用并梳理下HttpURLConnection
究竟能封装到什么样子。
2. HttpURLConnection
package java.net;
abstract public class HttpURLConnection extends URLConnection{
/* valid HTTP methods */
private static final String[] methods = {
"GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
};
/** * An {@code int} representing the three digit HTTP Status-Code. * <ul> * <li> 1xx: Informational * <li> 2xx: Success * <li> 3xx: Redirection * <li> 4xx: Client Error * <li> 5xx: Server Error * </ul> */
protected int responseCode = -1;
/* 2XX: generally "OK" */
public static final int HTTP_OK = 200;
/* 3XX: relocation/redirect */
/** * HTTP Status-Code 304: Not Modified. */
public static final int HTTP_NOT_MODIFIED = 304;
...
}
我从该类中摘要了一些方法,可以看出HttpURLConnection
支持多种请求方法,且在该类中有完整的状态码说明。在使用中,
HttpURLConnection
不仅从服务器读取数据,还有向服务器写入数据的功能,是双向交互,即通过获取到输入输出流来进行相关的数据写入和读取。下面是一些常用的方法:
功能 | 方法 | 说明 |
---|---|---|
获取HttpURLConnection 实例 |
(HttpURLConnection) url.openConnection() |
无 |
设置请求参数的流对象 | getOutputStream() |
若需要通过流来传递请求体参数,需要先通过setDoOutput(true) 来配置包含请求体 |
获取响应流对象 | getInputStream() |
也存在一个类似的方法``,可不调用默认为true |
关闭连接 | disconnect() |
调用后该连接可能会关闭也可能被复用 |
获取响应数据长度 | getContentLength() |
无 |
获取响应错误信息 | getErrorStream() |
如果getInputStream() 抛出了一个IOException 问题,可通过getErrorStream() 方法来获取到错误的响应信息,请求头也可使用getHeaderFields() 来获取 |
设置请求体大小 | setFixedLengthStreamingMode(int) 方法用来设定已知数据的大小 |
无 |
设置请求头部信息 | setRequestProperty("Accept-Encoding", "identity") |
多种头部参数,可在任意浏览器抓包参考 |
其余的还有一些设置Cookie
、HTTPS
以及认证的一些相关API
,本次不涉及。
上述方法在本篇学习中已经够用,先理下本篇需要做什么:
POST
请求体传递请求参数格式;POST
简单请求示例&POST
传递参数示例;POST
上传文件示例;
3. POST协议
在HttpURLConnection
类中我们能够看见一些链接地址指向rfc
文档,感兴趣可以去看看。这里先看下HTTP
报文结构。
3.1 HTTP报文
HTTP报文的三个组成部分:起始行(start line
)、首部(header
)和实体的主体部分(body
)。
- 起始行和首部就是由行分隔的
ASCII
文本; - 报文的主体是一个可选的数据块,可以包含文本或二进制数据,也可以为空。
为了得到一个简单直观的认知,抓包:
wireshark
-追踪流-get
:
GET /api/toolbox/geturl.php?h=BAE113FC0CA26810AEDEF9452C1B3468&v=11.7.0.5464&r=0000_sogou_pinyin_117a HTTP/1.1
User-Agent: SOGOU_UPDATER
Host: config.pinyin.sogou.com
Accept: */*
HTTP/1.1 200 OK
Server: nginx
Date: Thu, 22 Jun 2023 08:16:02 GMT
Content-Type: application/octet-stream
Content-Length: 0
Connection: keep-alive
wireshark
-追踪流-post
:
POST /a HTTP/1.1
Connection: Keep-Alive
User-Agent: WinHttpClient
Content-Length: 3514
Host: static-pcs-sdk-server.alibaba.com
{
"value" : {
"aes_key_version" : "1",
},
"version" : "3"
}
HTTP/1.1 200 OK
Server: Tengine
Date: Thu, 22 Jun 2023 08:16:18 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 53
Connection: close
Content-Language: zh-CN
{"encrypted":true,"value":"8EUS6GbXLfmy+3v4DS/FIw=="}
3.1.1 起始行
也就是我们上面抓包中的首行内容。三个部分构成:请求方法、请求接口和协议版本。
3.1.2 请求头(首部)
也就是紧接着的空行之上的键值对参数。
3.1.3 请求体
注意到get
方式没有请求体,post
方法在空行之后传入了一个json
字符串。另因我们使用的是追踪流方式,上面的抓包内容中也看见了响应的消息。格式类似也有三部分构成。
3.2 HTTP之POST
在前面了解到POST
方式的请求参数放置在请求体,也就是在抓包请求报文的空行之后。回顾我们所使用的请求参数方式,无论是GET
还是POST
,参数均是键值对
方式,故实际上在POST
请求报文中传递了两个参数,分别为value
和version
。
4. 示例
4.1 简单POST请求
我们有这么一个接口:
http://127.0.0.1:9090/test
params:
- name: string
- age: int
method:
- POST
return:
- result: string
那么使用HttpURLConnection
方式该如何来请求呢,见下:
public String doTest() {
HashMap<String, Object> datas = new HashMap<>();
datas.put("name", "123");
datas.put("age", 123);
return doPostWithParams("http://127.0.0.1:9090/test", datas);
}
public String doPostWithParams(String url, Map<String, Object> params) {
final String NEWLINE = "\r\n";
HttpURLConnection httpConn = null;
BufferedInputStream bis = null;
DataOutputStream dos = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
URL urlObj = new URL(url);
httpConn = (HttpURLConnection) urlObj.openConnection();
httpConn.setDoInput(true);
// 允许传入body参数
httpConn.setDoOutput(true);
httpConn.setRequestMethod("POST");
// POST不支持缓存
httpConn.setUseCaches(false);
httpConn.setRequestProperty("Connection", "Keep-Alive");
httpConn.setRequestProperty("Accept", "*/*");
httpConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
httpConn.setRequestProperty("Cache-Control", "no-cache");
httpConn.setRequestProperty("Charset", "utf-8");
// 这个比较重要,按照上面分析的拼装出Content-Type头的内容 https://blog.csdn.net/weiguang102/article/details/119645861
httpConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
httpConn.connect();
dos = new DataOutputStream(httpConn.getOutputStream());
if (params != null && !params.isEmpty()) {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
Object value = params.get(key);
if(stringBuilder.length() == 0) {
stringBuilder.append(key).append("=").append(value == null ? "" : value.toString());
} else {
stringBuilder.append("&").append(key).append("=").append(value == null ? "" : value.toString());
}
}
// 这里要个换行
dos.write((stringBuilder + NEWLINE).getBytes());
dos.flush();
dos.close();
}
byte[] buffer = new byte[8 * 1024];
int c = 0;
if (httpConn.getResponseCode() == 200) {
bis = new BufferedInputStream(httpConn.getInputStream());
while ((c = bis.read(buffer)) != -1) {
baos.write(buffer, 0, c);
baos.flush();
}
}
// 将输入流转成字节数组,返回给客户端。
return baos.toString("utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (dos != null)
dos.close();
if (bis != null)
bis.close();
baos.close();
if(httpConn != null) {
httpConn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
同样抓包可以看见:
POST /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Charset: utf-8
Content-Type: application/x-www-form-urlencoded
Pragma: no-cache
User-Agent: Java/11.0.15
Host: 127.0.0.1:9090
Connection: keep-alive
Content-Length: 18
name=123&age=123
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Date: Thu, 22 Jun 2023 09:01:32 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{hello: 123}
注意到,我们这里的请求body
内容为拼接字符串,前面抓包中有json
格式的,我们能否也用json
格式?试了下发现不行。之后看看为什么?
4.2 上传文件
提交的是表单类型数据,即:
上传文件链接:http://127.0.0.1:9090/
<form action="/upload/file" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>选择上传文件:</td>
<td><input type="file" name="file" /></td>
<td><input type="submit" value="上传" /></td>
</tr>
</table>
</form>
上传完毕后会返回:
{
"fileName": "ReadMe.md",
"md5": "C0BF1B7D3233E92EC874615ADEFFF103",
"date": "1687399358555",
"length": "2342",
"url": "http://127.0.0.1:9090/download/file?name=ReadMe.md&md5=C0BF1B7D3233E92EC874615ADEFFF103"
}
在HttpURLConnection
中就是经过一些请求头和标记行来进行实现的:
public String upload(File file) {
HashMap<String, Object> datas = new HashMap<>();
datas.put("name", "123");
datas.put("age", 123);
return doRequest("http://127.0.0.1:9090/upload/file", datas, file);
}
private String doRequest(String baseUrl, Map<String, Object> params, File file) {
// 设置三个常用字符串常量:换行、前缀、分界线(NEWLINE、PREFIX、BOUNDARY);
final String NEWLINE = "\r\n";
final String PREFIX = "--";
final String BOUNDARY = "jalfjaljfalfjl";
HttpURLConnection httpConn = null;
BufferedInputStream bis = null;
DataOutputStream dos = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
URL urlObj = new URL(baseUrl);
httpConn = (HttpURLConnection) urlObj.openConnection();
httpConn.setDoInput(true);
httpConn.setDoOutput(true);
httpConn.setRequestMethod("POST");
httpConn.setUseCaches(false);
httpConn.setRequestProperty("Connection", "Keep-Alive");
httpConn.setRequestProperty("Accept", "*/*");
httpConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
httpConn.setRequestProperty("Cache-Control", "no-cache");
// 这个比较重要,按照上面分析的拼装出Content-Type头的内容
httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
httpConn.connect();
dos = new DataOutputStream(httpConn.getOutputStream());
// 获取表单中上传控件之外的控件数据,写入到输出流对象(根据上面分析的抓包的内容格式拼凑字符串);
if (params != null && !params.isEmpty()) {
// 这时请求中的普通参数,键值对类型的,相当于上面分析的请求中的username,可能有多个
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey(); // 键,相当于上面分析的请求中的username
Object value = params.get(key); // 值,相当于上面分析的请求中的sdafdsa
dos.writeBytes(PREFIX + BOUNDARY + NEWLINE); // 像请求体中写分割线,就是前缀+分界线+换行
dos.writeBytes("Content-Disposition: form-data; " + "name=\"" + key + "\"" + NEWLINE);
// 拼接参数名,格式就是Content-Disposition: form-data; name="key" 其中key就是当前循环的键值对的键,别忘了最后的换行
dos.writeBytes(NEWLINE); // 空行,一定不能少,键和值之间有一个固定的空行
dos.writeBytes(URLEncoder.encode(value.toString(), "utf-8")); // 将值写入
dos.writeBytes(NEWLINE); // 换行
}
}
byte[] bytes = FileUtil.readAllBytes(file);
// 获取表单中上传附件的数据,写入到输出流对象
if (bytes != null && bytes.length > 0) {
dos.writeBytes(PREFIX + BOUNDARY + NEWLINE);// 像请求体中写分割线,就是前缀+分界线+换行
// 格式是:Content-Disposition: form-data; name="请求参数名"; filename="文件名"
// 我这里吧请求的参数名写成了file,是死的,实际应用要根据自己的情况修改
// 不要忘了换行
// writeBytes会乱码
dos.write(("Content-Disposition: form-data; " + "name=\"file\"" + "; filename=\"" + file.getName() + "\"" + NEWLINE).getBytes(StandardCharsets.UTF_8));
// 换行,重要!!不要忘了
dos.writeBytes(NEWLINE);
dos.write(bytes); // 上传文件的内容
dos.writeBytes(NEWLINE); // 最后换行
}
dos.writeBytes(PREFIX + BOUNDARY + PREFIX + NEWLINE);
// 最后的分割线,与前面的有点不一样是前缀+分界线+前缀+换行,最后多了一个前缀
dos.flush();
// 调用HttpURLConnection对象的getInputStream()方法构建输入流对象;
byte[] buffer = new byte[8 * 1024];
int c = 0;
// 调用HttpURLConnection对象的getResponseCode()获取客户端与服务器端的连接状态码。如果是200,则执行以下操作,否则返回null;
if (httpConn.getResponseCode() == 200) {
bis = new BufferedInputStream(httpConn.getInputStream());
while ((c = bis.read(buffer)) != -1) {
baos.write(buffer, 0, c);
baos.flush();
}
}
// 将输入流转成字节数组,返回给客户端。
return baos.toString("utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (dos != null)
dos.close();
if (bis != null)
bis.close();
baos.close();
if(httpConn != null) {
httpConn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
抓包看见:
POST /upload/file HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=jalfjaljfalfjl
Pragma: no-cache
User-Agent: Java/11.0.15
Host: 127.0.0.1:9090
Connection: keep-alive
Content-Length: 1738
--jalfjaljfalfjl
name="name"
123
--jalfjaljfalfjl
name="age"
123
--jalfjaljfalfjl
Content-Disposition: form-data; name="file"; filename=".........-.........,..........lrc"
[00:00.000] ...... : ...
[00:14.380]...... : Johnny Yim
[00:20.170].................. ...............
[00:23.890]..................... ...............
[00:27.880]..................... ...............
[00:31.630].........
[00:36.020].................. ...............
[00:39.800]..................... ...............
[00:43.610]..................... ...............
[00:46.910]........................
[00:50.050]......... ............
[00:51.530]...............
[00:57.270]......... .....................
[01:04.770]............ ......... .....................
[01:12.180]......... .....................
[01:20.620]............ .........
[01:40.790].................. ...............
[01:44.790].................. ...............
[01:48.590]..................... ...............
[01:51.940].........
[01:56.910].................. ...............
[02:00.470]..................... ...............
[02:04.180]..................... ...............
[02:07.620]........................ .........
[02:11.360]............
[02:12.390]...............
[02:20.520]......... .....................
[02:27.420]............ ......... .....................
[02:34.880]......... .....................
[02:43.300]............ ......... .........
[03:17.340]......... ..................
[03:29.230]............ ............ ............
[03:41.080].........
[03:45.550].....................
[03:48.970].........High
[03:52.250]......... ........................
[03:56.460].........
[04:00.750].....................
[04:04.520]............ .........
--jalfjaljfalfjl--
HTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 22 Jun 2023 09:25:29 GMT
Connection: close
{"timestamp":"2023-06-22T09:25:29.616+00:00","status":400,"error":"Bad Request","path":"/upload/file"}
换个思路,也就是POST
方式的参数传递可以有两种方式:
- 使用参数拼接,类似
GET
请求; - 使用表单类型来提交参数;
后续继续网上找找资料,看提交的json
参数是如何实现的。
文章评论