URLConnection-URL连接

Posted PacosonSWJTU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了URLConnection-URL连接相关的知识,希望对你有一定的参考价值。

【README】

  • 本文介绍了 URLConnection java类,通过 URLConnection 如何获取网络资源;
  • 本文还梳理了涉及网络编程的java类的进化过程;从 URL -> URLConnection -> HttpURLConnection 或 HttpClient
  • URL与URI的介绍, refer2   java URL和URI_PacosonSWJTU的博客-CSDN博客

1. 按理URL 有获取网络资源的接口(如getContent(), getFile()),为啥还要封装 URLConnection ? URL 获取资源方法如下:

2. 很显然, URL 只提供了客户端与服务器简单交互的功能,没有提供复杂的交互功能,比如请求头,缓存,字符编码,鉴权,等等,并根据请求头做出不同响应啊等等;所以引入 URLConnection来封装客户端与服务器间的复杂功能;

  • 2.1 URL 与 URLConnection 两者间还有一个重要区别:
    • 2.1.1 URL 只能读取网络资源内容(单向),而 URLConnection 提供了不仅从服务器读取数据,还有向服务器写入数据的功能,是双向交互;

3. 小结: URL 和 URLConnection的不同点

  • URLConnection 提供了对http的首部访问接口;
  • URLConnection 可以配置发送给服务器的请求参数;
  • URLConnection除了可以读取数据之外,还可以向服务器写入数据; (数据流是双向)

4.  那为啥还要在 URLConnection的基础上 引入 HttpURLConnection ?

abstract public class HttpURLConnection extends URLConnection {

 HttpURLConnection  继承了 URLConnection, 前者对后者的功能进行了扩展,以提供基于http协议的api,最显著的差别是,提供了获取错误输入流,响应码等方法,这是 URLConnection做不到的(根据下图的方法列表,HttpURLConnection也只是一点点扩展);

5.  我们再深入一下,既然有了 HttpURLConnection,那为啥 apache 还提供了 HttpClient-http客户端工具包进行网络编程?

在通常状况下,若是只是须要向Web站点的某个简单页面提交请求并获取服务器响应,HttpURLConnection彻底能够胜任。
但访问一些页面需要复制操作如鉴权,这就涉及Session、Cookie的处理了,若是打算使用HttpURLConnection来处理这些细节,固然也是可能实现的,只是处理起来难度就大了
因此 为了简化复杂的http网络编程,apache提供了HttpClient工具包;

补充: 第5点并不是说不用 HttpURLConnection,全部用 HttpClient ;

要知道, 性能上 HttpURLConnection高于 HttpClient,简单网络请求用 HttpURLConnection更快,复杂请求如 保存会话session,cookie,使用缓存等 使用 HttpClient;可以理解为 HttpURLConnection 是 轻量级网络请求, HttpClient 封装了其他额外的功能,比较重量级

但是我们写底层框架的时候,一般用的都是 HttpURLConnection,因为它是jdk自带的,HttpClient 需要额外引入apache依赖,不便于后期框架代码维护);


6. 下面给出 使用 URL, URLConnection, HttpURLConnection, HttpClient 读取网络资源的代码示例

  • 6.1 根据 URL 获取资源
// 根据 URL 获取资源
    @Test
    public void f0 () throws Exception {
        URL url = new URL("http://www.baidu.com");
        try (InputStream inputStream = url.openStream()) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            String result = "";
            while((result = bufferedReader.readLine()) != null) {
                System.out.println(result);
            }
        }
    }
  • 6.2  根据 URLConnection 获取资源
// 根据 URLConnection 获取资源
    @Test
    public void f1() throws Exception {
        URL u = new URL("http://www.baidu.com");
        // 打开连接获取 URLConnection对象
        URLConnection uc = u.openConnection();
        uc.setAllowUserInteraction(true);

        // 读取资源信息
        System.out.println("过期时间 " + uc.getExpiration());
        System.out.println("报文长度 " + uc.getContentLength());
        System.out.println("最后修改时间" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(uc.getLastModified())));

        // try资源块-自动关闭输入流
        try (InputStream inputStream = uc.getInputStream()) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            // 打印从服务器读取的报文
            String line = "";
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
  • 6.3 根据 HttpURLConnection 获取资源
// 根据 HttpURLConnection 获取资源
    @Test
    public void f1_1() throws Exception {
        URL u = new URL("http://www.baidu.com");
        // 获取 HttpURLConnection 连接
        HttpURLConnection httpUc = (HttpURLConnection) u.openConnection();

        // try资源块-自动关闭输入流
        try (InputStream inputStream = httpUc.getInputStream()) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            // 打印从服务器读取的报文
            String line = "";
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
  • 6.4 根据 apache HttpClient 获取资源
// 根据 apache HttpClient 获取资源
    @Test
    public void f1_2()  {
        //1.获得一个httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();
        //2.生成一个get请求
        HttpGet httpget = new HttpGet("http://www.baidu.com");
        //3.执行get请求并返回结果
        try (CloseableHttpResponse response = httpclient.execute(httpget)) {
            // 判断响应码
            if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
                // 读取报文
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8));
                String line = "";
                while((line = bufferedReader.readLine()) != null) {
                    System.out.println(line);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("请求错误");
        }
    }

 【1】URLConnection 简要介绍

1)URLConnection 是java的协议处理器机制的一部分,这个机制还包括 URLStatementHandler 类;

【1.1】打开 URLConnection

1)打开 URLConnection 与服务器交互步骤

  1. 构造 URL 对象;
  2. 调用 URL.openConnection() 获取 URLConnection 对象;
  3. 配置这个 URLConnection对象;
  4. 读取首部字段;(或有)
  5. 获取输入流并读取数据  调用 URLConnection.getInputStream() ; (或有)
  6. 获取输出流并写入数据; (或有)
  7. 关闭连接; 
// 打开 URLConnection 与服务器交互
    @Test
    public void f1() throws Exception {
        URL u = new URL("http://www.baidu.com");
        // 打开连接获取 URLConnection对象
        URLConnection uc = u.openConnection();
        uc.setAllowUserInteraction(true);
        try (InputStream inputStream = uc.getInputStream()) { // try资源块-自动关闭输入流
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            // 打印从服务器读取的报文
            String line = "";
            while((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }

【1.2】URLConnection 读取首部(请求头或响应头)

1)http首部包括 所请求网络资源的内容类型,长度,内容编码字符集,日期时间,内容过期时间,内容最后修改时间;

【1.2.0】常用首部包括:

  1. Content-type; 内容类型; MIME 内容类型; URLConnection.getContentType()
  2. Content-length;内容字节长度; URLConnection.getContentLength()
  3. Content-encoding,内容编码(非字符编码,字符编码格式在 content-type中的mime类型指定); URLConnection.getContentEncoding()
  4. Date, 内容产生时间; URLConnection.getDate()
  5. Last-modified, 最后修改时间, 以便于缓存; URLConnection.getLastModified()
  6. Expires, 到期时间, 以便于缓存;  URLConnection.getExpiration()

【1.2.1】 getContentType 返回响应主体的 MIME内容类型 (多用途互联网邮件扩展类型)

1)常用的 mime类型包括

  • text/html
  • text/plain
  • image/gif
  • application/xml
  • image/jpeg
  • application/json

2)如果内容类型是某种类型文本,那这个首部可能还包含一个字符集部分来标识文档的字符编码格式,如:

Content-type: text/html; charset=UTF-8 或  Content-type: application/json; charset=UTF-8

【1.2.2】getContentLength 获取响应报文体字节个数

  • 1)getContentLength 返回是 int 类型,最多标识 2gb(2^31=2g=20亿字节);
  • 2)随着网络发展,实际上很有可能资源大小超过 2gb; 在这种情况下 getContentLength 返回-1;

java7 新增了 getContentLengthLong 方法 返回类型是long, 2^63 个字节,理论上可以接收 8000PB=800万TB 个字节,足够使用了;

【1.2.3】getContentEncoding() 获取内容编码格式

web上常用的内容编码格式 可能是 x-gzip  或  GZipInputStream 直接解码;

【1.2.4】getDate()  指出文档发送给客户端的时间;

【1.2.5】getExpiration()  文档在服务器的过期时间

  • 1)提示客户端应该何时从缓存中删除文档,并从服务器重新下载;
  • 2)如果http首部没有 expiration字段,getExpiration() 返回0, 表示文档不会过期,将永远保留在缓存中;

【1.2.6】getLastModified 返回文档的最后修改时间

例子1, 读取http常用响应头

// 读取http常用响应头
    @Test
    public void f0() throws Exception {
        // 格式化器
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // 读取 URLConnection 的常用响应头
        URL url = new URL("http://www.baidu.com");
        URLConnection urlConnection = url.openConnection();
        // 读取资源内容类型, mime 类型
        System.out.println("内容类型 = " + urlConnection.getContentType());
        // 读取资源内容长度
        System.out.println("内容长度 = " + urlConnection.getContentLength());
        // 读取资源内容编码(如gzip,不是字符编码,字符编码在 content-type中的mime类型指定)
        System.out.println("内容编码 = " + urlConnection.getContentEncoding());
        // 读取指出文档发送给客户端的时间
        System.out.println("指出文档发送给客户端的时间 = " + simpleDateFormat.format(urlConnection.getDate()));
        // 读取文档在服务器的过期时间
        System.out.println("文档在服务器的过期时间 = " + simpleDateFormat.format(urlConnection.getExpiration()));
        // 读取 文档的最后修改时间
        System.out.println("文档的最后修改时间  = " + simpleDateFormat.format(urlConnection.getLastModified ()));
    }

结果:

内容类型 = text/html
内容长度 = 2381
内容编码 = null
指出文档发送给客户端的时间 = 2021-11-06 12:02:00
文档在服务器的过期时间 = 1970-01-01 08:00:00
文档的最后修改时间  = 1970-01-01 08:00:00

例子2,读取二进制资源文件,如图片(百度上随便搜索一张),mime类型为 image/jpeg ;

@Test
    public void f1() throws Exception {
        URL url = new URL("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbpic.588ku.com%2Felement_origin_min_pic%2F18%2F08%2F24%2F05dbcc82c8d3bd356e57436be0922357.jpg&refer=http%3A%2F%2Fbpic.588ku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1638764377&t=703444b029255bfa2ddba73484dd7c7c");
        // 打开连接,判断是否为图片格式
        URLConnection urlConnection = url.openConnection();
        String contentType = urlConnection.getContentType();
//        long contentLength = urlConnection.getContentLengthLong();
        int contentLength = urlConnection.getContentLength();
        System.out.println("文件内容类型=" + contentType + ", 内容长度=" + contentLength);
        if (!contentType.startsWith("image") || contentLength < 1) {
            throw new IOException("不是一个图片文件");
        }
        // 读取资源
        try (BufferedInputStream bufferedInputStream = new BufferedInputStream(url.openStream())) {
            byte[] data = new byte[contentLength];
            int offset = 0;
            int byteread = 0;
            while(offset < contentLength) {
                if ((byteread = bufferedInputStream.read(data, offset, data.length - offset)) == -1) {
                    break;
                }
                offset += byteread;
            }
            if (offset != contentLength) {
                throw new IOException(MessageFormat.format("读取失败, 读了{0}个字节,期望读取{1}个字节", offset, contentLength));
            }
            // 写入文件,获取文件名
            String filename = url.getFile();
            System.out.println("资源文件名 = " + filename);
            filename = "temp01." + contentType.split("[\\\\W+]")[1];// 非词字符正则分割
           try (FileOutputStream fos = new FileOutputStream("D:/temp/" + filename)) {
               fos.write(data);
               fos.flush();
           }
        }
    }

结果:

文件内容类型=image/jpeg, 内容长度=46417


【1.3】获取任意首部

// 获取任意首部
    @Test
    public void f1() throws Exception {
        URL url = new URL(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbpic.588ku.com%2Felement_origin_min_pic%2F18%2F08%2F24%2F05dbcc82c8d3bd356e57436be0922357.jpg&refer=http%3A%2F%2Fbpic.588ku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1638764377&t=703444b029255bfa2ddba73484dd7c7c");
        URLConnection urlConnection = url.openConnection();
        // 获取所有首部(响应头)的键值对
        Map<String, List<String>> headerFileds = urlConnection.getHeaderFields();
        Set<Map.Entry<String, List<String>>> entrySet = headerFileds.entrySet();
        entrySet.forEach(entry -> {
            String key = entry.getKey();
            System.out.println("key = " + key + ", 值列表=" + entry.getValue());
        }); 
        // 获取单个首部
        System.out.println("获取单个首部");
        System.out.println("Content-Type = " + urlConnection.getHeaderField("Content-Type"));
    }

结果:

key = null, 值列表=[HTTP/1.1 200 OK]
key = Server, 值列表=[JSP3/2.0.14]
key = Access-Control-Allow-Origin, 值列表=[*]
key = Ohc-Upstream-Trace, 值列表=[118.112.225.100]
key = Connection, 值列表=[keep-alive]
key = Last-Modified, 值列表=[Thu, 01 Jan 1970 00:00:00 GMT]
key = Ohc-File-Size, 值列表=[46417]
key = Date, 值列表=[Sat, 06 Nov 2021 04:42:29 GMT]
key = Accept-Ranges, 值列表=[bytes]
key = Ohc-Cache-HIT, 值列表=[cd6ct100 [4], bdix100 [4]]
key = Ohc-Response-Time, 值列表=[1 0 0 0 0 0]
key = ETag, 值列表=[927f0861741bd2135df3cbac979cdded]
key = Timing-Allow-Origin, 值列表=[*]
key = Expires, 值列表=[Sat, 04 Dec 2021 06:42:40 GMT]
key = Content-Length, 值列表=[46417]
key = Age, 值列表=[1287]
key = Content-Type, 值列表=[image/jpeg]
获取单个首部
Content-Type = image/jpeg


【2】 缓存

【2.1】缓存涉及的 http报文头

1)Expires 报文头指示这个资源可以缓存,期限是指定的时间为止;

2)Cache-control 首部提供了更加细粒度的缓存策略;

Cache-control 会覆盖 Expires ,即优先级 前者高于后者;服务器可以在一个首部中发送多个 Cache-control 首部,只要它们没有冲突; 

3)Last-modified 指示资源最后一次修改日期。只有当本地缓存的副本早于这个日期,才会到服务器获取资源,否则客户端一直使用本地缓存;

4)Etag首部: 资源改变时的唯一标识; 当客户端资源副本的Etag 与 服务器不同时,才会到服务器获取资源,否则客户端一直使用本地缓存;

5)缓存策略(干货):

  1. 如果本地缓存中有这个资源的一个表示, 而且还没有到它的过期时间 Expires,那么可以直接使用这个资源,而无需请求服务器;
  2. 如果本地缓存中有这个资源的一个表示,但到了过期时间,在完成 get请求前,可以检查服务器首部的head首部 Etag,查看资源是否已经改变;

6)java自带的web缓存类

默认情况下,java并没有缓存。需要安装URL类使用的系统级缓存,需要有:

  • ResponseCache的子类;
  • CacheRequest的子类;
  • CacheResponse的子类;

【3】配置 URLConnection连接

【3.1】配置属性及其方法

1)URLConnection定义了7个保护字段,定义了客户端如何向服务器发送请求;

  • 1.1)protected URL url, 指定这个 URLConnection 连接 URL;初始化一次,不能修改;
  • 1.2)protected boolean connected,是否连接;连接打开,该值为true,连接关闭,该值为false;connect(), getInputStream(), getOutputStream() 都会打开连接; disconnect() 关闭连接; 
  • 1.3)protected boolean allowUserInteraction, 指示了是否允许用户交互;只能在打开连接前设置,连接后设置抛出异常;
  • 1.4)protected boolean doInput,是否可以从服务器服务器读取资源;补充 URConnection 可以用于读取服务器,写入服务器,或读写服务器;但响应字段要设置为true;
  • 1.5)protected boolean doOutput,是否可以向服务器写入数据; 当为一个 http URL的doOutput设置为 true时,请求方法从 GET 修改为 POST
  • 1.6)protected boolean ifModifiedSince,设置为true,则客户端请求报文头包括一个首部 If-Modified-Since,值日期时间格式;如果服务器文档在这个时间之后修改,则发送该文档,否则不发送(而发送响应码 304 Not Modified),客户端使用本地缓存;  URLConnection的 ifModifiedSince 字段指定了 放置在 If-Modified-Since 首部字段中的日期,调用 setIfModifiedSince(long ifModifiedSince毫秒数) 来设置;
  • 1.7)protected boolean useCaches, 客户端是否使用本地缓存; URCConnection.setUseCahces(false) 用于禁用本地缓存; 

2)URLConnection 连接属性设置例子

 // URCConnection 连接属性设置例子
    @Test
    public void f3() throws Exception {
        URL u = new URL("http://www.baidu.com");
        // 打开连接获取 URLConnection对象
        URLConnection uc = u.openConnection();
        uc.setAllowUserInteraction(true); // 允许交互
        uc.setDoInput(true);  // 可以从服务器读取数据
        uc.setDoOutput(true); // 可以向服务器写入数据
        uc.setIfModifiedSince(new Date().getTime()); // 设置资源最后修改时间
        uc.setUseCaches(false); // 禁用缓存

        // try资源块-自动关闭输入流
        try (InputStream inputStream = uc.getInputStream()) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            // 打印从服务器读取的报文
            String line = "";
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }

【3.2】超时

  • setConnectionTimeout(int),设置连接获取超时时间;
  • setReadTimeout(int) ,设置读取服务器资源超时时间; 

【4】配置客户端请求http首部

【4.1】增加首部字段

1) URLConnection.setRequestProperty(String name, String value) 为http首部增加字段;

其允许一个name,有多个value,多个value通过逗号隔开; 该方法只能在 打开连接前使用;

【4.2】向服务器写入数据

// 向服务器写入数据
    @Test
    public void f4() throws Exception {
        URL u = new URL("http://www.baidu.com");
        // 打开连接,把请求方法从get变为post
        URLConnection uc = u.openConnection();
        uc.setDoOutput(true); // get 变为 post
        uc.setRequestProperty("cookie", "username=zhangsan; password=123456; session=B2C7E8A1F2F5E8"); // 设置cookie请求头  

        // 写入数据到server
        try(OutputStream outputStream = uc.getOutputStream()) {
            outputStream.write("四川省成都市高新区".getBytes(StandardCharsets.UTF_8));
        }
        System.out.println("写入数据成功,bingo");
    }

【5】HttpURLConnection

HttpURLConnection 是抽象类,构造函数是保护类型,所以不能直接创建;

URL.openConnection() 返回的就是一个 HttpURLConnection的一个实例; 如下:

URL u = new URL("http://www.baidu.com");
// 获取 HttpURLConnection 连接
HttpURLConnection httpUc = (HttpURLConnection) u.openConnection();

【5.1】 请求方法

1)改变请求方法

HttpURLConnection.setRequestMethod() 用于修改请求方法;

2)请求方法包括:

  • GET,请求资源,但没有请求体;
  • POST,请求资源, 有请求体;
  • HEAD,告诉服务器只返回http首部,不用实际发送文件 ;常见用途是检查文件的最后修改时间;
  • PUT,html编辑器或向上传文件到服务器使用put方法;
  • DELETE,删除web服务器上的文件;
  • OPTIONS,询问某个url 支持哪些选项;
  • TRACE ,trace会发送http报文头,服务器接收这个报文头;可以查看 服务器和客户端之间的代理服务器做了哪些修改;

3)restful api中 方法类型与业务操作对应关系

序号方法类型业务操作
1get查询数据
2post新增数据
3put修改或更新数据
4delete删除数据

【5.2】断开与服务器的连接

调用 HttpURLConnection.disconnect() 方法可以关闭连接,同时关闭流;但关闭流不会关闭连接;

【5.3】处理服务器响应

1)响应报文示例

 

注意: 响应报文头 与 响应实体间 有一个空行(作为分隔符),图片中我忘记标识出来了,特此说明

 2)获取响应码和响应报文

// 获取响应码和响应报文
    @Test
    public void f6() throws Exception {
        URL u = new URL("http://www.baidu.com");
        // 打开连接,把请求方法从get变为post
        HttpURLConnection httpURLConnection = (HttpURLConnection) u.openConnection();
        System.out.println("响应码  = " + httpURLConnection.getResponseCode());
        System.out.println("响应消息  = " + httpURLConnection.getResponseMessage());

        // 获取所有响应头
        for (int i = 1; ; i++) {
            String header = httpURLConnection.getHeaderField(i);
            String key = httpURLConnection.getHeaderFieldKey(i);
            if (header == null || key == null) break;
            System.out.println("key=" + key + ", value=" + header);
        }
    }

打印结果:

响应码  = 200
响应消息  = OK
key=Content-Length, value=2381
key=Content-Type, value=text/html
key=Server, value=bfe
key=Date, value=Sat, 06 Nov 2021 08:51:40 GMT

3)补充:HttpURLConnection 封装了很多常量响应码

以上是关于URLConnection-URL连接的主要内容,如果未能解决你的问题,请参考以下文章

使用实体框架迁移时 SQL Server 连接抛出异常 - 添加代码片段

错误:E/RecyclerView:未连接适配器;跳过片段上的布局

连接MySQL出现错误:ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using password: YES)(代码片段

部分代码片段

谷歌地图片段显示,但没有地图

根据图片的url地址下载图片到本地保存代码片段