资讯详情

Java 编程问题:十三、HTTP 客户端和 WebSocket API

原文:Java Coding Problems

协议:CC BY-NC-SA 4.0

贡献者:飞龙

本文来自【ApacheCN Java 谷歌翻译用谷歌翻译。

本章包括 20 一个问题,旨在介绍 HTTP 客户端和 WebSocket API。

你还记得HttpUrlConnection吗?好吧,JDK11 附带了 HTTP 客户端 API,它是对HttpUrlConnection重新发明。HTTP 客户端 API 易于使用,支持 HTTP/2(默认)和 HTTP/1.1。为了向后兼容,当服务器不支持 HTTP/2 时,HTTP 客户端 API 将自动从 HTTP/2 降级到 HTTP 1.1。此外,HTTP 客户端 API 支持同步和异步编程模型,并依步编程模型。它还支持 WebSocket 协议用于实时 Web 应用程序提供客户端-服务器通信,消息成本较低。

问题

用以下问题来测试你 HTTP 客户端和 WebSocketAPI 编程能力。在使用解决方案和下载示例程序之前,我强烈建议您尝试每个问题:

  1. :简要介绍 HTTP/2 协议

  2. :编写程序,使用 HTTP 客户端 API 触发异步GET请求,并显示响应代码和文本。

  3. :写一个使用 HTTP 客户端 API 通过代理建立连接程序。

  4. :编写程序,在请求中添加额外标头,以获得响应标头。

  5. :编写指定请求 HTTP 方法程序(例如GETPOSTPUTDELETE)。

  6. :编写程序,使用 HTTP 客户端 API 请求添加文本。

  7. :编写程序,使用 HTTP 客户端 API 连接认证通过用户名和密码设置。

  8. :编写程序,使用 HTTP 客户端 API 设置等待响应的时间量(加时)。

  9. :根据需要编写程序 HTTP 客户端 API 自动重定向。

  10. :在同步和异步模式下,编写同样的请求。

  11. :编写程序,使用 HTTP 客户端 API 设置 Cookie 处理器。

  12. :编写程序,使用 HTTP 客户端 API 获取响应信息(如 URI、版本、头、状态码、文本等)。

  13. :写几段代码举例说明如何通过HttpResponse.BodyHandlers处理常见的响应体类型。

  14. :编写程序,使用 HTTP 客户端 API 获取、更新和保存 JSON。

  15. :编写处理压缩响应的程序(如.gzip

  16. :写一个使用 HTTP 客户端 API 提交数据表单的程序(application/x-www-form-urlencoded

  17. :编写使用 HTTP 客户端 API 下载资源程序。

  18. :写一个使用 HTTP 客户端 API 上传资源程序。

  19. :通过编写程序 HTTP 客户端 API 演示 HTTP/2 服务器推送特性。

  20. :编写程序,打开到 WebSocket 端点连接,收集数据 10 秒,然后关闭连接。

解决方案

以下部分介绍了上述问题的解决方案。请记住,通常没有正确的方法来解决特定的问题。此外,请记住,这里显示的解释只包括解决问题所需的最有趣和最重要的细节。您可以下载示例解决方案,查看更多详细信息并尝试程序。

250 HTTP/2

对它来说是一个有效的协议 协议明显改进。

作为大局的一部分, 有两部分:

  • :这是 复用核心能力
  • :它包含数据(我们通常称之为数据) HTTP)

下图描述了 (顶部)和 (底部)通信:

[外链图片转存失败,源站可能有防盗链机制,建议保存图片并直接上传(img-pic6ZWCy-1657346355161)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/0ecf8f36-941b-4f96-8d50-01b5ea7f9b31.png)]

广泛应用于服务器和浏览器 与以下改进相比:

  • 帧层二进制分帧协议不易被人类读取,但更容易机器操作。
  • :请求和响应交织在一起。多个请求同时运行在同一连接上。
  • :服务器决定向客户端发送额外资源。
  • 单通信线路用于每个源(域)(TCP 连接)。
  • 依靠 HPACK 压缩以减少标头。这对冗余字节有很大影响。
  • :大部分通过电线传输的数据都是加密的。

251 触发异步 GET 请求

触发异步GET请求是三步工作,如下:

  1. 新建HttpClient对象(java.net.http.HttpClient):
HttpClient client = HttpClient.newHttpClient(); 
  1. 构建HttpRequest对象(java.net.http.HttpRequest并指定请求(默认为GET请求):
HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

为了设置 URI,我们可以调用HttpRequest.newBuilder(URI)构造器,或者在Builder实例上调用uri(URI)方法(就像我们以前做的那样)。

  1. 触发请求并等待响应(java.net.http.HttpResponse。作为同步请求,应用将阻止,直到响应可用:
HttpResponse<String> response 
  = client.send(request, BodyHandlers.ofString());

如果我们将这三个步骤分组,并添加用于在控制台上显示响应代码和正文的行,那么我们将获得以下代码:

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

HttpResponse<String> response 
  = client.send(request, BodyHandlers.ofString());

System.out.println("Status code: " + response.statusCode());
System.out.println("\n Body: " + response.body());

上述代码的一个可能输出如下:

Status code: 200
Body:
{ 
        
  "data": { 
        
    "id": 2,
    "email": "janet.weaver@reqres.in",
    "first_name": "Janet",
    "last_name": "Weaver",
    "avatar": "https://s3.amazonaws.com/..."
 }
}

默认情况下,此请求使用 HTTP/2 进行。但是,我们也可以通过HttpRequest.Builder.version()显式设置版本。此方法获取一个参数,其类型为HttpClient.Version,是一个enum数据类型,它公开了两个常量:HTTP_2HTTP_1_1。以下是显式降级到 HTTP/1.1 的示例:

HttpRequest request = HttpRequest.newBuilder()
  .version(HttpClient.Version.HTTP_1_1)
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

HttpClient的默认设置如下:

  • HTTP/2 协议
  • 没有验证器
  • 无连接超时
  • 没有 Cookie 处理器
  • 默认线程池执行器
  • NEVER的重定向策略
  • 默认代理选择器
  • 默认 SSL 上下文

我们将在下一节中查看查询参数生成器。

查询参数生成器

使用包含查询参数的 URI 意味着对这些参数进行编码。完成此任务的 Java 内置方法是URLEncoder.encode()。但将多个查询参数连接起来并对其进行编码会导致类似以下情况:

URI uri = URI.create("http://localhost:8080/books?name=" +
  URLEncoder.encode("Games & Fun!", StandardCharsets.UTF_8) +
  "&no=" + URLEncoder.encode("124#442#000", StandardCharsets.UTF_8) +
  "&price=" + URLEncoder.encode("$23.99", StandardCharsets.UTF_8)
);

当我们必须处理大量的查询参数时,这种解决方案不是很方便。但是,我们可以尝试编写一个辅助方法,将URLEncoder.encode()方法隐藏在查询参数集合的循环中,也可以依赖 URI 生成器。

在 Spring 中,URI 生成器是org.springframework.web.util.UriComponentsBuilder。以下代码是不言自明的:

URI uri = UriComponentsBuilder.newInstance()
  .scheme("http")
  .host("localhost")
  .port(8080)
  .path("books")
  .queryParam("name", "Games & Fun!")
  .queryParam("no", "124#442#000")
  .queryParam("price", "$23.99")
  .build()
  .toUri();

在非 Spring 应用中,我们可以依赖 URI 生成器,比如urlbuilder库。这本书附带的代码包含了一个使用这个的例子。

252 设置代理

为了建立代理,我们依赖于Builder方法的HttpClient.proxy()方法。proxy()方法获取一个ProxySelector类型的参数,它可以是系统范围的代理选择器(通过getDefault())或通过其地址指向的代理选择器(通过InetSocketAddress)。

假设我们在proxy.host:80地址有代理。我们可以按以下方式设置此代理:

HttpClient client = HttpClient.newBuilder()
  .proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 80)))
  .build();

或者,我们可以设置系统范围的代理选择器,如下所示:

HttpClient client = HttpClient.newBuilder()
  .proxy(ProxySelector.getDefault())
  .build();

253 设置/获取标头

HttpRequestHttpResponse公开了一套处理头文件的方法。我们将在接下来的章节中学习这些方法。

设置请求头

HttpRequest.Builder类使用三种方法来设置附加头:

  • header​(String name, String value)setHeader​(String name, String value):用于逐个添加表头,如下代码所示:
HttpRequest request = HttpRequest.newBuilder()
  .uri(...)
  ...
  .header("key_1", "value_1")
  .header("key_2", "value_2")
  ...
  .build();

HttpRequest request = HttpRequest.newBuilder()
  .uri(...)
  ...
  .setHeader("key_1", "value_1")
  .setHeader("key_2", "value_2")
  ...
  .build();

header()setHeader()的区别在于前者添加指定的头,后者设置指定的头。换句话说,header()将给定值添加到该名称/键的值列表中,而setHeader()覆盖该名称/键先前设置的任何值。

  • headers​(String... headers):用于添加以逗号分隔的表头,如下代码所示:
HttpRequest request = HttpRequest.newBuilder()
  .uri(...)
  ...
  .headers("key_1", "value_1", "key_2",
    "value_2", "key_3", "value_3", ...)
  ...
  .build();

例如,Content-Type: application/jsonReferer: https://reqres.in/头可以添加到由https://reqres.in/api/users/2URI 触发的请求中,如下所示:

HttpRequest request = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .header("Referer", "https://reqres.in/")
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

您还可以执行以下操作:

HttpRequest request = HttpRequest.newBuilder()
  .setHeader("Content-Type", "application/json")
  .setHeader("Referer", "https://reqres.in/")
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

最后,你可以这样做:

HttpRequest request = HttpRequest.newBuilder()
  .headers("Content-Type", "application/json",
    "Referer", "https://reqres.in/")
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

根据目标的不同,可以将这三种方法结合起来以指定请求头。

获取请求/响应头

可以使用HttpRequest.headers()方法获取请求头。HttpResponse中也存在类似的方法来获取响应的头。两个方法都返回一个HttpHeaders对象。

这两种方法可以以相同的方式使用,因此让我们集中精力获取响应头。我们可以得到这样的标头:

HttpResponse<...> response ...
HttpHeaders allHeaders = response.headers();

可以使用HttpHeaders.allValues()获取头的所有值,如下所示:

List<String> allValuesOfCacheControl
  = response.headers().allValues("Cache-Control");

使用HttpHeaders.firstValue()只能获取头的第一个值,如下所示:

Optional<String> firstValueOfCacheControl
  = response.headers().firstValue("Cache-Control");

如果表头返回值为Long,则依赖HttpHeaders.firstValueAsLong()。此方法获取一个表示标头名称的参数并返回Optional<Long>。如果指定头的值不能解析为Long,则抛出NumberFormatException

254 指定 HTTP 方法

我们可以使用HttpRequest.Builder中的以下方法指示请求使用的 HTTP 方法:

  • GET():此方法使用 HTTPGET方法发送请求,如下例所示:
HttpRequest requestGet = HttpRequest.newBuilder()
  .GET() // can be omitted since it is default
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();
  • POST():此方法使用 HTTPPOST方法发送请求,如下例所示:
HttpRequest requestPost = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString(
    "{\"name\": \"morpheus\",\"job\": \"leader\"}"))
  .uri(URI.create("https://reqres.in/api/users"))
  .build();
  • PUT():此方法使用 HTTPPUT方法发送请求,如下例所示:
HttpRequest requestPut = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .PUT(HttpRequest.BodyPublishers.ofString(
    "{\"name\": \"morpheus\",\"job\": \"zion resident\"}"))
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();
  • DELETE():此方法使用 HTTPDELETE方法发送请求,如下例所示:
HttpRequest requestDelete = HttpRequest.newBuilder()
  .DELETE()
  .uri(URI.create("https://reqres.in/api/users/2"))
  .build();

客户端可以处理所有类型的 HTTP 方法,不仅仅是预定义的方法(GETPOSTPUTDELETE)。要使用不同的 HTTP 方法创建请求,只需调用method()

以下解决方案触发 HTTPPATCH请求:

HttpRequest requestPatch = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .method("PATCH", HttpRequest.BodyPublishers.ofString(
    "{\"name\": \"morpheus\",\"job\": \"zion resident\"}"))
  .uri(URI.create("https://reqres.in/api/users/1"))
  .build();

当不需要请求体时,我们可以依赖于BodyPublishers.noBody()。以下解决方案使用noBody()方法触发 HTTPHEAD请求:

HttpRequest requestHead = HttpRequest.newBuilder()
  .method("HEAD", HttpRequest.BodyPublishers.noBody())
  .uri(URI.create("https://reqres.in/api/users/1"))
  .build();

如果有多个类似的请求,我们可以依赖copy()方法来复制生成器,如下代码片段所示:

HttpRequest.Builder builder = HttpRequest.newBuilder()
  .uri(URI.create("..."));

HttpRequest request1 = builder.copy().setHeader("...", "...").build();
HttpRequest request2 = builder.copy().setHeader("...", "...").build();

255 设置请求体

请求体的设置可以通过HttpRequest.Builder.POST()HttpRequest.Builder.PUT()来完成,也可以通过method()来完成(例如method("PATCH", HttpRequest.BodyPublisher)POST()PUT()采用HttpRequest.BodyPublisher类型的参数。API 在HttpRequest.BodyPublishers类中附带了此接口(BodyPublisher的几个实现,如下所示:

  • BodyPublishers.ofString()
  • BodyPublishers.ofFile()
  • BodyPublishers.ofByteArray()
  • BodyPublishers.ofInputStream()

我们将在下面几节中查看这些实现。

从字符串创建标头

使用BodyPublishers.ofString()可以从字符串创建正文,如下代码片段所示:

HttpRequest requestBody = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString(
    "{\"name\": \"morpheus\",\"job\": \"leader\"}"))
  .uri(URI.create("https://reqres.in/api/users"))
  .build();

要指定charset调用,请使用ofString(String s, Charset charset)

InputStream创建正文

InputStream创建正文可以使用BodyPublishers.ofInputStream()来完成,如下面的代码片段所示(这里,我们依赖于ByteArrayInputStream,当然,任何其他InputStream都是合适的):

HttpRequest requestBodyOfInputStream = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofInputStream(()
    -> inputStream("user.json")))
  .uri(URI.create("https://reqres.in/api/users"))
  .build();

private static ByteArrayInputStream inputStream(String fileName) { 
        

  try (ByteArrayInputStream inputStream = new ByteArrayInputStream(
    Files.readAllBytes(Path.of(fileName)))) { 
        

    return inputStream;
  } catch (IOException ex) { 
        
    throw new RuntimeException("File could not be read", ex);
  }
}

为了利用延迟创建,InputStream必须作为Supplier传递。

从字节数组创建正文

从字节数组创建正文可以使用BodyPublishers.ofByteArray()完成,如下代码片段所示:

HttpRequest requestBodyOfByteArray = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofByteArray(
    Files.readAllBytes(Path.of("user.json"))))
  .uri(URI.create("https://reqres.in/api/users"))
  .build();

我们也可以使用ofByteArray(byte[] buf, int offset, int length)发送字节数组的一部分。此外,我们还可以使用ofByteArrays(Iterable<byte[]> iter)提供字节数组的Iterable数据。

从文件创建正文

从文件创建正文可以使用BodyPublishers.ofFile()完成,如下代码片段所示:

HttpRequest requestBodyOfFile = HttpRequest.newBuilder()
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofFile(Path.of("user.json")))
  .uri(URI.create("https://reqres.in/api/users"))
  .build();

256 设置连接认证

通常,对服务器的认证是使用用户名和密码完成的。在代码形式下,可以使用Authenticator类(此协商 HTTP 认证凭证)和PasswordAuthentication类(用户名和密码的持有者)一起完成,如下:

HttpClient client = HttpClient.newBuilder()
  .authenticator(new Authenticator() { 
        

    @Override
    protected PasswordAuthentic

标签: jy27连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台