1 java.net.HttpURLConnection 概述
1.1 HttpURLConnection 简述
HttpURLConnection
位于java.net
包中;- 它对外提供访问HTTP协议的基本功能;
- HttpURLConnection 是 Java 提供的发起 HTTP 请求的基础类库.
- 其继承自
URLConnection
,可用于向指定网站发送GET
、POST
请求。 - 其提供了 HTTP 请求的基本功能,不过封装的比较少,在使用时很多内容都需要自己设置,也需要【自己处理】请求流和响应流。
- 其继承自
1.2 URLConnection 类 API
URLConnection提供如下方法:
方法名 | 备注说明 |
---|---|
int getResponseCode(); | 获取服务器的响应代码 |
String getResponseMessage(); | 获取服务器的响应消息 |
String getResponseMethod(); | 获取发送请求的方法 |
void setRequestMethod(String method); | 设置发送请求的方法 |
1.3 HttpURLConnection 类 API
1.3.1 获取连接对象
【特别注意】 获取连接对象 ≠ 建立连接
// 定义 URL对象
final URL url = new URL("http//ip:port/xxx");
// 获取 URL 链接
URLConnection urlConnection = url.openConnection();
// 因为 URL 是根据 url 中的协议(此处http)生成的 URLConnection 类的子类 HttpURLConnection, 故:
// 此处转换为 HttpURLConnection子类,方便使用子类中的更多的API
HttpURLConnection connection = (HttpURLConnection)urlConnection;
1.3.2 设置参数(请求时间)
在 Http 请求时防止对方长时间无法连接等问题,一般会设置超时时间
// 设置连接超时时间, 值必须大于0,设置为0表示不超时 单位为“毫秒”
connection.setConnectTimeout(30000);
// 设置读超时时间, 值必须大于0,设置为0表示不超时 单位毫秒
connection.setReadTimeout(60000);
1.3.3 设置参数(请求方法)
在 Http 请求中包括 GET、POST、PUT等方法,可以通过如下方法设置 HttpURLConnection的请求方法
// 设置为 GET 请求,
connection.setRequestMethod("GET");
注意:此处 方法必须设置为 大写,否则会报错误:
java.net.ProtocolException: Invalid HTTP method: get
也可以通过定义枚举类型设置,如下所示:
// 定义请求方法枚举类
public enum HttpMethod {
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
}
// 在使用枚举设置请求方法
connection.setRequestMethod(HttpMethod.POST.name());
1.3.4 设置参数(请求头)
在请求时,经常会遇到设置自定义请求头,或者更改 Conent-Type 的值,可以通过如下方设置:
// 设置请求类型为 application/json
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
// 设置可接受的数据类型
connection.setRequestProperty("Accept", "*/*");
// 设置保持长链接
connection.setRequestProperty("Connection", "Keep-Alive");
// 设置自定义头 Token
connection.setRequestProperty("Token", "123456");
1.3.5 设置参数(其他)
// 设置不使用缓存, 默认为 true 使用缓存
connection.setUseCaches(false);
// 设置单次请求是否支持重定向,默认为 setFollowRedirects 方法设置的值
connection.setInstanceFollowRedirects(false);
// 设置是否进行重定向,注意此处为 静态方法,表示所有的请求都不支持重定向,默认为true
HttpURLConnection.setFollowRedirects(false);
注意:所有的参数,必须在建立连接之前设置,否则会报错误
java.lang.IllegalStateException: Already connected
- 如果需要写入数据需要调用
setDoOutput(true)
打开输出流
connection.setDoOutput(true);
1.3.6 建立连接
【特别注意】在调用
connection()
、getOutputStream()
、getInputStream()
方法之前设置好请求参数。
- 建立连接(显式连接)
在设置完所有参数后,可以通过调用 connect 方法,进行显式建立连接。
// 调用打开连接, 调用此方法,只是建立一个连接,并不会发送数据。
connection.connect();
- 建立连接(隐式连接)
除了上面的调用 connect 显式建立连接外,在调用如下方法时,会隐式的调用此方法,建立连接。
// 获取输出流 | 如果调用 `getOutputStream()` 方法,则:会自动把把请求方法改为 `POST`
connection.getOutputStream();
// 获取输入流
connection.getInputStream();
由于,在网络请求时,一般都会获取请求结果,故在实际应用中,一般不调用 connect() 方法进行显式打开连接。
1.3.7 发送数据
【POST请求】
众所周知,HTTP 中的 POST 请求的数据是包含在请求体中的。在 HttpURLConnection 中 POST 请求发送数据时,需要获取 连接的输出流对象,然后往输出流中写数据即可,如下所示:
// 要发送的数据
String connect = "我是一个POST请求数据";
// 因为这个是post请求,参数要放在
// http正文内,因此需要设为true, 默认情况下是false;
connection.setDoOutput(true);
// 从连接中获取 输出流对象
OutputStream os = connection.getOutputStream();
// 往输出流中写数据
os.write(connect.getBytes(StandardCharsets.UTF_8));
// 冲刷 并 关闭输出流
os.flush();
os.close();
【注意】
1、 需要写数据时,必须调用connection.setDoOutput(true);
方法,并且参数为true
, 且需要在调用getOutputStream()
方法之前调用。
2、此时写的数据,只是写到了缓冲区中,并不会把数据真正地发送给资源方。即:建立连接和调用
getOutputStream()
方法写入数据并关闭连接后,也不会发送数据,只有调用getInputStream()
才会真正的发送数据。
【GET请求】
Http 中的 GET 请求的参数是拼接在 URL 后进行发送的。
所以 发送 GET 请求时,在创建 连接时把参数拼接在后面即可。
但是,有一点需要注意,如果在 GET 请求中 也调用了 getOutputStream() 方法,那么,自动就会把请求改为 POST 请求。如下源码所示:
// HttpURLConnection 中的 方法,
private synchronized OutputStream getOutputStream0() throws IOException {
try {
if (!this.doOutput) {
} else {
// 如果设置的 方法为 GET 则改为 POST
if (this.method.equals("GET")) {
this.method = "POST";
}
}
}catch(Exception e){
}
}
1.3.8 响应数据(【请求结果】)
在 HTTP 请求中一般是需要知道请求状态,在 HttpURLConnection 中可以通过如下方式获取请求状态
// 获取请求状态,此状态即为 HTTP 请求的状态 200:成功,404:找不到资源 等
int responseCode = connection.getResponseCode();
// 获取请求描述信息
String msg = connection.getResponseMessage();
1.3.9 响应数据(【获取头信息】)
获取响应头有如下几种方式:
// 1、获取所有的响应头信息
Map<String, List<String>> headerFields = connection.getHeaderFields();
// 2、根据头信息名称获取响应头信息
String connectionHeader = connection.getHeaderField("Connection");
// 3、根据头信息索引获取响应头信息, 此下标 必须大于 0。
String secHeader = connection.getHeaderField(2);
1.3.10 读取数据
读取响应数据也是比较简单的,可以首先通过 HttpURLConnection 中的
getInputStream()
方法 获取输入流,然后,通过输入流获取数据即可,如下所示:
// 获取输入流
InputStream inputStream = connection.getInputStream();
// 定义一个临时字节输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// 开始读取数据
byte[] buffer = new byte[256];
int len = 0;
while ((len = inputStream.read(buffer)) > 0){
baos.write(buffer,0, len);
}
return new String(baos.toByteArray(), StandardCharsets.UTF_8);
} finally {
// 关闭输入、输出流
inputStream.close();
baos.close();
}
1.3.11 上传下载(上传)
-
在普通 Web 页面中上传文件是很简单的,只需要把 from 标签中加上 enctype="multipart/form-data" 即可,剩下的都交给浏览器去完成发送数据的收集并发送 Http 请求即可。
-
但是,在 HttpURLConnection 中脱离了浏览器,就需要我们自己去完成数据收集并发送请求了。那么我们首先看下浏览器是怎么收集上传数据,并发送请求的。
-
先看下浏览器发送上传时间的请求正文格式:
// 请求头中的 Content-Type 属性 其中定义了属性分割线
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfdRf0g4cPSTVeLkJ
// 请求数据正文信息
------WebKitFormBoundaryfdRf0g4cPSTVeLkJ
Content-Disposition: form-data; name="images"; filename="20150703212056_Yxi4L.jpeg"
Content-Type: image/jpeg
------WebKitFormBoundaryfdRf0g4cPSTVeLkJ
Content-Disposition: form-data; name="checkRecord"
{"describe":"","rectify":"立即整改"}
------WebKitFormBoundaryfdRf0g4cPSTVeLkJ--
分析上面的的数据我们能够发下如下规则:
- 数据正文中的第一行 ------WebKitFormBoundaryfdRf0g4cPSTVeLkJ 作为分隔符,然后是 \r\n 回车换行符。
- 第二行 Content-Disposition: form-data; name="images"; filename="*****", 代表 form 表单数据域,其中 name 表示 接口属性值,filename 为文件名称。
- 第三行 Content-Type: image/jpeg 表示上传文件的类型。
- 第四行是一个 回车换行符。
- 第五行 是 数据内容,由于此处为 图片故没有显示出来。
- 后面的也是遵从上述规律。
- 最后一行表示结束行,注意后面多两个--。
根据以上规律,我们 在 使用 HttpURLConnection 进行上传时,就可以按照此规律拼接发送的数据流。实例如下所示:
public void upload(File file) throws Exception {
final URL url = new URL("http://localhost:10010/user/upload");
// 获取 URL 链接
URLConnection urlConnection = url.openConnection();
// 因为 URL 是根据 url 中的协议(此处http)生成的 URLConnection 类的子类
// HttpURLConnection, 故此处转换为 HttpURLConnection子类,方便使用子类
// 中的更多的API
HttpURLConnection connection = (HttpURLConnection)urlConnection;
// 自定义分割线,并设置请求头信息
String boundary = "------------" + System.currentTimeMillis();
connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
// 设置请求为 POST 请求
connection.setRequestMethod(METHOD.POST.name());
// 打开输出流
connection.setDoOutput(true);
// 获取上传文件的类型
MagicMatch magicMatch = Magic.getMagicMatch(file, false, true);
String mimeType = magicMatch.getMimeType();
// 获取输出流
OutputStream outputStream = connection.getOutputStream();
//拼接请求数据
StringBuilder builder = new StringBuilder();
// 第一行分割行
builder.append("\r\n").append("--" + BOUNDARY).append( "\r\n");
// 第二行form表单数据
builder.append("Content-Disposition: form-data; name=\"file\"; filename=\"").append(file.getName() ).append("\"\r\n");
// 第三行 上传数据类型
builder.append( "Content-Type:").append(mimeType).append("\r\n");
// 第四行一个空行
builder.append("\r\n");
outputStream.write(builder.toString().getBytes(StandardCharsets.UTF_8));
// 开始写文件数据
InputStream fileInput = new FileInputStream(file);
byte[] buffer = new byte[512];
int len = 0;
while ((len = fileInput.read(buffer)) > 0){
outputStream.write(buffer, 0, len);
}
// 开始写基本数据
StringBuilder textBuffer = new StringBuilder();
// 分隔符行
textBuffer.append("\r\n").append("--" + BOUNDARY).append("\r\n");
// form表单数据
textBuffer.append("Content-Disposition: form-data; name=\"name\"\r\n");
// 一个空行
textBuffer.append("\r\n");
// 数据值
textBuffer.append("张三");
outputStream.write(textBuffer.toString().getBytes(StandardCharsets.UTF_8));
// 写入结束行
outputStream.write(("\r\n--" + BOUNDARY + "--\r\n").getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
fileInput.close();
int responseCode = connection.getResponseCode();
printHeaders(connection.getHeaderFields());
if(responseCode != 200){
LOGGER.error("请求失败, code: {}, message: {}", responseCode, connection.getResponseMessage());
}else {
InputStream inputStream = connection.getInputStream();
String reader = reader(inputStream);
LOGGER.info("服务端返回数据为: \n {}", reader);
}
}
【注意】基本数据比 file 缺少 Content-Type: image/jpeg 行
1.3.12 上传下载(下载)
文件的下载就比较简单了,获取输入流,然后读取输入流,并把读到的数据保存到本地即可,一下是下载网络上的图片为例。
/**
* 下载
* @param url 下载文件路径
* @param distDir 保存的文件路径
*/
public void download(String url, String distDir) throws Exception {
// 获取连接
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// 设置请求方法
connection.setRequestMethod("GET");
connection.setRequestProperty("Charset", "UTF-8");
// 获取文件名
String fileUrl = connection.getURL().getFile();
String fileName = fileUrl.substring(fileUrl.lastIndexOf(File.separatorChar) + 1);
LOGGER.info("文件名:{} -- {}", fileName, File.separator);
String filePath = distDir + File.separatorChar + fileName;
File file = new File(filePath);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
// 获取输入流,并写入文件
try (InputStream inputStream = connection.getInputStream();
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[256];
int len = 0;
while ((len = inputStream.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
os.flush();
}
}
1.3.13 关闭连接
- 关闭连接 :connection.disconnect()
- 当HttpURLConnection 是 "Connection: close " 模式时,关闭 inputStream 后就会自动断开连接。
- 当HttpURLConnection 是 "Connection: Keep-Alive" 模式时,关闭 inputStream 后,并不会断开底层的 Socket 连接
- 采用此种模式的优点:当需要连接到同一服务器地址时,可以复用该 Socket,如果要求断开连接,可调用 connection.disconnect()
2 案例实践
2.1 案例集
2.1.1 案例1:GET方式请求HTTP资源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* GET请求示例*/
public class GetDemo {
public static void main(String[] args) {
try {
// 1. 得到访问地址的URL
URL url = new URL("http://localhost:8080/index.jsp");
// 2. 得到网络访问对象java.net.HttpURLConnection
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
/* 3. 设置请求参数(过期时间,输入、输出流、访问方式),以流的形式进行连接 */
// 设置是否向HttpURLConnection输出
connection.setDoOutput(false);
// 设置是否从HttpUrlConnection读入
connection.setDoInput(true);
// 设置请求方式
connection.setRequestMethod("GET");
// 设置是否使用缓存
connection.setUseCaches(true);
// 设置此 HttpURLConnection 实例是否应该自动执行 HTTP 重定向
connection.setInstanceFollowRedirects(true);
// 设置超时时间
connection.setConnectTimeout(3000);
// 4.连接
connection.connect();
// 5. 得到响应状态码的返回值 responseCode
int code = connection.getResponseCode();
// 6. 如果返回值正常,数据在网络中是以流的形式得到服务端返回的数据
String responseContent = "";
if (code == 200) { // 正常响应
// 从流中读取响应信息
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) { // 循环从流中读取
responseContent += line + "\n";
}
reader.close(); // 关闭流
}
// 7. 断开连接,释放资源
connection.disconnect();
// 8. 显示响应结果
System.out.println(responseContent);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.1.2 案例2:POST方式请求HTTP资源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
/**
* POST请求示例*/
public class PostDemo {
public static void main(String[] args) {
try {
// 1. 获取访问地址URL
URL url = new URL("http://localhost:8080/index.jsp");
// 2. 创建HttpURLConnection对象
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
/* 3. 设置请求参数等 */
// 请求方式
connection.setRequestMethod("POST");
// 设置连接超时时间
connection.setConnectTimeout(3000);
// 设置是否向 HttpUrlConnection 输出,对于post请求,参数要放在 http 正文内,因此需要设为true,默认为false。
connection.setDoOutput(true);
// 设置是否从 HttpUrlConnection读入,默认为true
connection.setDoInput(true);
// 设置是否使用缓存
connection.setUseCaches(false);
// 设置此 HttpURLConnection 实例是否应该自动执行 HTTP 重定向
connection.setInstanceFollowRedirects(true);
// 设置使用标准编码格式编码参数的名-值对
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
// 添加 HTTP HEAD 中的一些参数。
// JDK8中,HttpURLConnection默认开启Keep-Alive
// connection.setRequestProperty("Connection", "Keep-Alive");
// 4. 连接
connection.connect();
/* 5. 处理输入输出 */
// 写入参数到请求中
String params = "username=test&password=123456";
OutputStream out = connection.getOutputStream();
out.write(params.getBytes());
out.flush();
out.close();
// 从连接中读取响应信息
String responseContent = "";
int code = connection.getResponseCode();
if (code == 200) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
responseContent += line + "\n";
}
reader.close();
}
// 6. 断开连接
connection.disconnect();
// 7. 处理结果
System.out.println(responseContent);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Y 【FAQ】
Y.1 HttpURLConnection的注意事项
-
HttpURLConnection对象不能直接构造
- 需使用
URL
类中的 openConnection() 方法来创建实例
- 需使用
-
HttpURLConnection对象属性设置,需在connect()方法执行之前完成
-
HttpURLConnection的
connect()
函数,其本质是建立一个与服务器的TCP连接- 并未实际发送HTTP请求
- HTTP请求靠调用getInputStream()、getResponseCode()等方法触发
-
HttpURLConnection是基于HTTP协议的,其底层通过socket通信实现
-
不设置超时(timeout),当网络异常的情况下,可能会导致程序僵死而不继续往下执行
-
HTTP正文内容是通过OutputStream流写入,向流中写入的数据不会立即发送到网络,而是存在于内存缓冲区中,待流关闭时,根据写入的内容生成HTTP正文。
调用getInputStream()方法时,会返回一个输入流,用于从中读取服务器对于HTTP请求的返回信息 -
当获取HTTP响应的时候,请求就会自动的发起
如使用HttpURLConnection.getInputStream()方法的时系统会自动调用connect()方法 -
HttpURLConnection长连接(Keep-Alive)相关说明
- JDK8自带的HttpURLConnection
- 默认启用keepAlive,支持HTTP / 1.1和HTTP / 1.0持久连接,
- 使用后的HttpURLConnection会放入缓存中供以后的同host:port的请求重用底层socket在keepAlive超时之前不会关闭
-
HttpURLConnection受system properties影响
http.keepAlive=<boolean>(默认值:true),是否启用keepAlive,如果设置为false,则HttpURLConnection不会缓存,使用完后会关闭socket连接。
http.maxConnections=<int>(默认值:5),每个目标host缓存socket连接的最大数。
当在HttpURLConnection的header中加入Connection: close,则此连接不会启用keepAlive
如果想启用keepAlive,程序请求完毕后必须调用HttpURLConnection.getInputStream().close()
(此操作用于归还长连接给缓存,下次同host:port的请求重用底层socket连接),
而不调用HttpURLConnection.disconnect()(表示关闭底层socket连接,不会启用keepAlive)
- keepAliveTimeout属性的获取原理:
从http response header中获取,如果没有取到,则默认为5秒
sun.net.www.http.KeepAliveCache.java中有一个线程,每5秒执行一次
检查缓存的连接的空闲时间是否超过keepAliveTimeout,如果超过则关闭连接
从KeepAliveCache中获取缓存的连接时也会检查获取到的连接的空闲时间是否超过keepAliveTimeout
如果超过则关闭连接,并且获取下一个连接,再执行以上检查,直达获取到空闲时间在keepAliveTimeout以内的缓存连接为此。
Y.2 HttpURLConnection同HttpClient的区别?
- 由于HttpClient是进行封装的框架,使用起来更加便捷。所以:
- 在一些复杂url请求处理时,可使用HttpClient
- 在一些简单的场景下,可使用HttpURLConnection
- 我们可以理解为:HttpClient是HttpURLConnection的增强