[JDK/网络] java.net.HttpURLConnection 详解

发布时间 2023-11-06 17:14:28作者: 千千寰宇

1 java.net.HttpURLConnection 概述

1.1 HttpURLConnection 简述

  • HttpURLConnection 位于java.net包中;
  • 它对外提供访问HTTP协议的基本功能;
  • HttpURLConnection 是 Java 提供的发起 HTTP 请求的基础类库.
    • 其继承自URLConnection,可用于向指定网站发送GETPOST请求。
    • 其提供了 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的增强

X 参考与推荐文献