原文: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 编程能力。在使用解决方案和下载示例程序之前,我强烈建议您尝试每个问题:
-
:简要介绍 HTTP/2 协议
-
:编写程序,使用 HTTP 客户端 API 触发异步
GET
请求,并显示响应代码和文本。 -
:写一个使用 HTTP 客户端 API 通过代理建立连接程序。
-
:编写程序,在请求中添加额外标头,以获得响应标头。
-
:编写指定请求 HTTP 方法程序(例如
GET
、POST
、PUT
、DELETE
)。 -
:编写程序,使用 HTTP 客户端 API 请求添加文本。
-
:编写程序,使用 HTTP 客户端 API 连接认证通过用户名和密码设置。
-
:编写程序,使用 HTTP 客户端 API 设置等待响应的时间量(加时)。
-
:根据需要编写程序 HTTP 客户端 API 自动重定向。
-
:在同步和异步模式下,编写同样的请求。
-
:编写程序,使用 HTTP 客户端 API 设置 Cookie 处理器。
-
:编写程序,使用 HTTP 客户端 API 获取响应信息(如 URI、版本、头、状态码、文本等)。
-
:写几段代码举例说明如何通过
HttpResponse.BodyHandlers
处理常见的响应体类型。 -
:编写程序,使用 HTTP 客户端 API 获取、更新和保存 JSON。
-
:编写处理压缩响应的程序(如
.gzip
。 -
:写一个使用 HTTP 客户端 API 提交数据表单的程序(
application/x-www-form-urlencoded
。 -
:编写使用 HTTP 客户端 API 下载资源程序。
-
:写一个使用 HTTP 客户端 API 上传资源程序。
-
:通过编写程序 HTTP 客户端 API 演示 HTTP/2 服务器推送特性。
-
:编写程序,打开到 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
请求是三步工作,如下:
- 新建
HttpClient
对象(java.net.http.HttpClient
):
HttpClient client = HttpClient.newHttpClient();
- 构建
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)
方法(就像我们以前做的那样)。
- 触发请求并等待响应(
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_2
和HTTP_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 设置/获取标头
HttpRequest
和HttpResponse
公开了一套处理头文件的方法。我们将在接下来的章节中学习这些方法。
设置请求头
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/json
和Referer: https://reqres.in/
头可以添加到由https://reqres.in/api/users/2
URI 触发的请求中,如下所示:
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 方法,不仅仅是预定义的方法(GET
、POST
、PUT
、DELETE
)。要使用不同的 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