目录
- 简介
- 一、发送同步GET请求
- 二、发送带请求提的请求
- 三、发送异步请求
- 四、WebSocket客户端支持
- 五、基于WebSocket的多人实时聊天
简介
HTTPClient主要包含了两个部分:①用于发送基于HTTP协议的GET、POST、PUT、DELETE请求HTTPClient部分;②支持Web Socket的客户端API。
不管是HTTPURLConnection,还是HTTPClient,他们使用的基本原理相似,都是要向服务器端发送HTTP请求,然后获取并解析服务器响应。使用HTTPClient涉及的核心API:
(1)HttpClient:HTTPClient的核心对象,用于发送和接受请求。Java为该类提供了HttpClient.Builder接口。
(2)HttpRequest:代表请求对象。Java为该类提供了HttpClient.Builder接口。
(3)HttpResponse:代表响应对象。
为了封装请求参数和处理响应数据,HTTPClient还提供了如下两个API:
(1)HttpRequest.BodyPublishers:用于创建HttpRequest.BodyPublisher的工厂类,其中HttpRequest.BodyPublisher代表请求参数,这些请求参数来自字符串、字节数组、文件和输如流。
(2)HttpResponse.BodyHandlers:用于创建HttpResponse.BodyHandler的工厂类,其中HttpResponse.BodyHandler代表对响应体的转换处理,该对象可以将服务器响应抓换成字节数组、字符串、文件、输入流和逐行输入等。
需要说明的是,当HttpResponse.BodyHandler对服务器响应进行转换时,需要根据服务器响应的内容进行转换。比如服务器响应本身时图片、视频等二进制数据,那就不要尝试将服务器响应转换成字符串或逐行输入——HttpResponse.BodyHandler并不能改变响应数据本身,它只是简化响应数据的处理。
归纳起来,使用HTTPClient发送请求的步骤:
1、创建HttpClient对象。
2、创建HttpRequest对象作为请求对象。如有需要,使用HttpRequest.BodyPublisher为请求本身添加请求参数。
3、调用HttpClient的send()或sendAsync()方法发送请求,其中sendAsync()方法用于发送异步请求。调用这两个方法发送请求时,需要传入HttpResponse.BodyHandler对象,指定对响应数据进行转换处理。
一、发送同步GET请求
发送同步GET请求时最简单的情况,因为请求参数被直接附在请求的URL后面,无须使用HttpRequest.BodyPublisher添加请求参数。
下面程序示范了使用HttpClient发送同步GET请求的步骤和详细过程:
package HTTPCLIENT;
import java.net.http.*;
import java.time.*;
import java.net.*;
public class GetTest
{
public static void main(String[] args) throws Exception
{
// ①、创建HttpClient对象
HttpClient client = HttpClient.newBuilder()//1
// 指定HTTP协议的版本
.version(HttpClient.Version.HTTP_2)
// 指定重定向策略
.followRedirects(HttpClient.Redirect.NORMAL)
// 指定超时的时长
.connectTimeout(Duration.ofSeconds(20))
// 如有必要,可通过该方法指定代理服务器地址
// .proxy(ProxySelector.of(new InetSocketAddress("proxy.crazyit.com", 80)))
.build();
// ②、创建HttpRequest对象
HttpRequest request = HttpRequest.newBuilder()//2
// 执行请求的URL
.uri(URI.create("https://baike.baidu.com/item/%E7%AB%A5%E7%8E%B2/974254?fr=aladdin"))
// 指定请求超时的时长
.timeout(Duration.ofMinutes(2))
// 指定请求头
.header("Content-Type", "text/html")
// 创建GET请求
.GET()
.build();
// HttpResponse.BodyHandlers.ofString()指定将服务器响应转化成字符串
HttpResponse.BodyHandler<String> bh = HttpResponse.BodyHandlers.ofString();
// ③、发送请求,获取服务器响应
HttpResponse<String> response = client.send(request,bh);//3
// 获取服务器响应的状态码
System.out.println("响应的状态码:" + response.statusCode());
System.out.println("响应头:\n" + response.headers());
System.out.println("响应体:" + response.body());
}
}
上面代码1,2,3展示了HttpClient发送请求的三个步骤。程序调用send()方法发送同步GET请求时,指定使用HttpResponse.BodyHandler转换响应数据,因此服务器响应数据也是字符串。
运行结果:
二、发送带请求提的请求
HTTP Client也可以发送HTTP协议支持的各种请求,只需要调用HttpRequest.Builder对象的如下方法创建对应的请求即可
(1)DELETE():创建DELETE请求。
(2)GET():创建GET请求。
(3)method(String method, HttpRequest.BodyPublisher bodyPublisher):创建method参数指定的各种请求。其中bodyPublisher参数用于设置请求体(包含请求参数);method参数必须是DELETE、POST、HEAD、PATCH等有效方法,否则将会引发IllegalArgumentException异常。
(4) POST(HttpRequest.BodyPublisher bodyPublisher):创建POST请求。其中bodyPublisher参数用于设置请求参数。
(5) PUT(HttpRequest.BodyPublisher bodyPublisher):创建PUT请求。其中bodyPublisher参数用于设置请求体(包含请求参数)。
提示:对于RESTful客户端而言,GET、POST、PUT、DELETE是4中最基本的请求方法,其中GET请求用于获取实体数据;POST请求用于添加实体;PUT用于请求修改实体;DELETE用于删除实体。
从上面方法可以看出,不管发送那种请求,如果设置请求体,都需要通过HttpRequest.Builder参数进行设置,HTTPClient提高了HttpRequest.Builders来创建该对象,这些请求数据可来自字符串、文件、字节数组、二进制流等。
如下代码创建了请求体参数来自字符串的POST请求:
HttpRequest request=HttpRequest.newBuilder()
.uri(URI.create("https://www.crazyit.org/"))
//指定提交表单的方式编码请求体
.header("Contend-Type","application/x-www-form-urlencoded")
//通过字符串创建请求体,然后作为POST请求的请求参数
.POST(HttpRequest.BodyPublishers.ofString("name==crazyit.org&pass=leegang"))//1
.build();
上面代码1调用ofString()方法通过字符串来创建请求体。
如下代码创建请求提参数来自文件的PUT请求:
HttpRequest request=HttpRequest.newBuilder()
.uri(URI.create("https://www.crazyit.org/"))
//设置请求内容是JSON
.header("Contend-Type","application/json")
//通过文件创建请求体,然后作为PUT请求的请求参数
.PUT(HttpRequest.BodyPublishers.ofFile("file.json"))//1
.build();
上面代码1调用ofFile()方法通过字符串来创建请求体。
如下代码创建请求体参数来自字节数组的POST请求:
HttpRequest request=HttpRequest.newBuilder()
.uri(URI.create("https://foo.com/"))
//通过字节数组创建请求体,然后作为POST请求的请求参数
.POST(HttpRequest.BodyPublishers.ofByteArray(new byte[]{...}))//1
.build();
上面代码1调用ofByteArray()方法通过字符串来创建请求体。此外HTTP Client还提供了ofInputStream()方法通过二进制流创建请求体。
下面程序示范使用带请求体的POST请求来提交登录请求,当用户登陆后在此使用GET请求访问前面的secret.jsp页面时。应该可以访问到登录后的资源。
为了有效管理用户登陆后于服务器端的Session,程序需要为HttpClient对象设置一个Cookie处理器,此处使用Java默认的Cookie处理器即可。
package HTTPCLIENT;
import java.net.http.*;
import java.time.*;
import java.net.*;
public class PostTest
{
public static void main(String[] args) throws Exception
{
// 为CookieHandler设置默认的Cookie管理器
CookieHandler.setDefault(new CookieManager());
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
// 设置默认的Cookie处理器
.cookieHandler(CookieHandler.getDefault())//1
.build();
// 创建发送POST请求的request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8888/foo/login.jsp"))
.timeout(Duration.ofMinutes(2))
// 指定以提交表单的方式编码请求体
.header("Content-Type", "application/x-www-form-urlencoded")
// 通过字符串创建请求体,然后作为POST请求的请求参数
.POST(HttpRequest.BodyPublishers.ofString("name=crazyit.org&pass=leegang"))//2
.build();
// HttpResponse.BodyHandlers.ofString()指定将服务器响应转化成字符串
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("POST请求的响应码:" + response.statusCode());
System.out.println("POST请求的响应体:" + response.body());
// 创建发送GET请求的request
request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8888/foo/secret.jsp"))
.timeout(Duration.ofMinutes(2))
.header("Content-Type", "text/html")
.GET()
.build();
// HttpResponse.BodyHandlers.ofString()指定将服务器响应转化成字符串
response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("GET请求的响应码:" + response.statusCode());
System.out.println("GET请求的响应体:" + response.body());
}
}
上面程序代码1为HttpClient设置了默认的Cookie处理器,这样该HttpClient对象才能维护与服务器端的Session状态(Web应用的登录状态一般保持在Session中)。程序代码2处创建了POST请求,并通过字符串创建请求,并指定通过字符串创建请求体,这样该HttpClient对象即可向服务器发送登录的POST请求。
三、发送异步请求
相比HttpURLConnection,HTTPClient支持发送异步请求。
只要发送网络请求,就有可能出现网络延迟、等待服务器响应等时间开销。使用send()方法发送同步请求,在服务器响应到来之前,该方法不能返回,当前线程也会在该send()方法处阻塞——这就是同步请求的缺点;如果调用sendAsync()方法发送异步请求,该方法不会阻塞当前线程,程序执行了sendAsync()方法之后回立即向下执行。
程序使用sendAsync()方法发送请求之后,该方法会立即返回一个CompletableFuture对象,他代表一个将要完成的任务——但具体何时完成,不确定。因此程序需要为CompletableFuture设置消费监听器,当CompletableFuture代表的任务完成时,该监听器就会被激发。
下面改写上面的程序,使用sendAsync()方法发送异步请求:
package HTTPCLIENT;
import java.net.http.*;
import java.time.*;
import java.net.*;
public class AsyncTest
{
public static void main(String[] args) throws Exception
{
// 为CookieHandler设置默认的Cookie管理器
CookieHandler.setDefault(new CookieManager());
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
// 设置默认的Cookie处理器
.cookieHandler(CookieHandler.getDefault())
.build();
// 创建发送POST请求的request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8888/foo/login.jsp"))
.timeout(Duration.ofMinutes(2))
// 指定以提交表单的方式编码请求体
.header("Content-Type", "application/x-www-form-urlencoded")
// 通过字符串创建请求体,然后作为POST请求的请求参数
.POST(HttpRequest.BodyPublishers.ofString("name=crazyit.org&pass=leegang"))
.build();
// 创建发送GET请求的request
HttpRequest getReq = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8888/foo/secret.jsp"))
.timeout(Duration.ofMinutes(2))
.header("Content-Type", "text/html")
.GET()
.build();
// 发送异步的POST请求,返回CompletableFuture对象
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())//1
// 当CompletableFuture完成时,传入的Lambda表达式对该返回值进行转换
.thenApply(resp -> new Object[] {resp.statusCode(), resp.body()})
// 当CompletableFuture完成时,传入的Lambda表达式处理该返回值
.thenAccept(rt -> {
System.out.println("POST请求的响应码:" + rt[0]);
System.out.println("POST请求的响应体:" + rt[1]);
// 发送异步的GET请求,返回CompletableFuture对象
client.sendAsync(getReq, HttpResponse.BodyHandlers.ofString())//2
// 当CompletableFuture完成时,传入的Lambda表达式处理该返回值
.thenAccept(resp -> {
System.out.println("GET请求的响应码:" + resp.statusCode());
System.out.println("GET请求的响应体:" + resp.body());
});
});
System.out.println("--程序结束--");
Thread.sleep(1000);
}
}
上面程序代码1使用了sendAsync()方法发送异步的POST请求,该方法的返回值是一个CompletableFuture对象,因此程序又调用thenApply()、thenAccept()方法来处理POST请求的返回值。
代码2使用sendAsync()方法发送异步的GET请求,但该方法同样返回一个CompletableFuture对象。上面程序使用sendAsync()发送了两个异步请求,但这两个请求不会阻塞当前线程,因此程序将会先输出“--程序结束--”,然后才会输出POST请求的响应和GET请求的响应。
最后一行代码让程序当前线程暂停1000ms,这是因为本程序使用的sendAsync()方法发送异步请求的,二异步请求不会阻塞当前线程,因此程序将会一直运行下去。如果没有暂替线程这行代码,程序执行完成,这样就无法看到服务器的响应。
四、WebSocket客户端支持
WebSocket就像普通的Socket,只不过它是与Web应用之间创建的Socket连接,而且通常都是以异步方法进行通信的。
HTTPClient为WebSocket提供了如下三个接口
(1)WebSocket:代表WebSocket客户端,提供了一系列的sendSxx()发送数据。
(2)WebSocket.Builder:用于创建WebSocket对象,在创建WebSocket对象时需要传入一个监听器,该监听器负责监听、处理服务器端发送来的消息。
(3)WebSocket.Listener:WebSocket的监听器。
程序要实现WebSocket监听器,就需要实现WebSocket.Listener监听器接口,实现接口中的以下方法:
★onBinary(WebSocket webSocket, ByteBuffer data, boolean last):当收到服务端发送回来的二进制数据时激发该方法。
★onClose(WebSocket webSocket, int statusCode, String reason):当WebSocket被关闭时激发该方法。
★onError(WebSocket webSocket, Throwable error):当连接出现错误时激发该方法。
★onOpen(WebSocket webSocket):当该WebSocket客户端与服务端打开连接时激发该方法。
★onPing(WebSocket webSocket, ByteBuffer message):当收到服务端发送回来的Ping消息时激发该方法。
★onPong(WebSocket webSocket, ByteBuffer message):当收到服务端发送回来的Pong消息时激发该方法。
★onText(WebSocket webSocket, CharSequence data, boolean last):当收到服务端发送回来的文本数据时激发该方法。
通过上面的介绍,实现WebSocket客户端需要两步:
(1)创建一个WebSocket.Listener对象作为监听器,更具需要重写该监听器中的方法
(2)使用WebSocket.Listener对象作为监听器,使用WebSocketBuilder构建WebSocket客户端。
下面程序实现了最简单的WebSocket客户端的示例程序:
package HTTPCLIENT;
import java.net.http.*;
import java.net.*;
import java.util.concurrent.*;
public class WebSocketTest
{
public static void main(String[] args) throws Exception
{
// 构建WebSocket.Listener监听器对象
WebSocket.Listener listener = new WebSocket.Listener()
{
// 与服务端打开连接时激发该方法
@Override
public void onOpen(WebSocket webSocket)
{
System.out.println("已打开连接");
webSocket.sendText("我是疯狂软件教育中心!", true);
// 请求获取下一次的消息
webSocket.request(1);
}
// 接收到服务端发送回来的文本消息时激发该方法
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence message, boolean last)
{
System.out.println(message);
// 请求获取下一次的消息
webSocket.request(1);
return null;
}
};
HttpClient client = HttpClient.newHttpClient();
// 传入监听器作为参数,创建WebSocket客户端
client.newWebSocketBuilder().buildAsync(
URI.create("ws://127.0.0.1:8888/foo/simpleSocket"), listener);
Thread.sleep(5000);
}
}
上面程序创建了一个WebSocket.Listener监听器,并重写onOpen()和onText()方法,这就意味着该监听器只处理与服务器建立连接和接受服务器发送回来的文本信息这两个事件。
程序倒数第二句代码传入监听器,创建WebSocket客户端,这样程序就可以通过该WebSocket向服务器端发送数据。
用于WebSocket已成为主流的技术规范,在Java EE规范中也加入了WebSocket支持,而Tomcat 9作为Java Web服务器,提供了对WebSocket的支持。
基于Java EE规范来开发WebSocket服务器端也非常简单,此处直接给除了WebSocket服务器端的代码:
package org.fkjava.web;
import javax.websocket.*;
import javax.websocket.server.*;
// @ServerEndpoint注解修饰的类将作为WebSocket的服务端
@ServerEndpoint(value="/simpleSocket")
public class SimpleEndpoint
{
@OnOpen // 该注解修饰的方法将会客户端连接时被激发
public void start(Session session)
{
System.out.println("客户端连接进来了,session id:"
+ session.getId());
}
@OnMessage // 该注解修饰的方法将会客户端消息到达时被激发
public void message(String message, Session session) throws Exception
{
System.out.println("接收到消息了:" + message);
RemoteEndpoint.Basic remote = session.getBasicRemote();
remote.sendText("收到!收到!欢迎加入WebSocket的世界!");
System.out.println("发送完成!!!");
}
@OnClose // 该注解修饰的方法将会客户端连接关闭时被激发
public void end(Session session, CloseReason closeReason)
{
System.out.println("客户端连接关闭了,session id:"
+ session.getId());
}
@OnError // 该注解修饰的方法将会客户端出错时被激发
public void error(Session session, Throwable throwable)
{
System.err.println("客户端连接出错了,session id:"
+ session.getId());
}
}
在Web服务器中部署foo应用,并启动Web服务器,然后运行上面的WebSocketTest代码,将会看到如下输出:
已打开连接
收到!收到!欢迎加入WebSocket的世界!
在Tomcat服务端的控制台将看到下面的输出:
客户端连接进来了,session id:0
接收到消息了:我是疯狂软件教育中心!
发送完成!!!
五、基于WebSocket的多人实时聊天
每个客户端与服务器建立一个WebSocket,客户端随时可以通过WebSocket把数据发送到服务器上;当服务器端接受到任何一个客户端发来的消息之后,再将该消息向每个客户端发送一遍。
客户端代码使用WebSocket建立与远程服务器的连接,再通过sendText()发送消息,并通过监听器的onText()方法来接受该消息即可。下面是WebSocket客户端程序代码:
package HTTPCLIENT;
import java.net.http.*;
import java.time.*;
import java.net.*;
import java.util.*;
import java.io.*;
import java.util.concurrent.*;
public class WebSocketChat
{
public static void main(String[] args) throws Exception
{
// 构建WebSocket.Listener监听器对象
WebSocket.Listener listener = new WebSocket.Listener()
{
// 与服务端打开连接时激发该方法
@Override
public void onOpen(WebSocket webSocket)
{
System.out.println("已打开连接");
// 请求获取下一次的消息
webSocket.request(1);
}
// 接收到服务端发送回来的文本消息时激发该方法
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence message, boolean last)
{
System.out.println(message);
// 请求获取下一次的消息
webSocket.request(1);
return null;
}
};
HttpClient client = HttpClient.newHttpClient();
// 传入监听器作为参数,创建WebSocket客户端
client.newWebSocketBuilder().buildAsync(
URI.create("ws://127.0.0.1:8888/foo/chatSocket"), listener)
.thenAccept(webSocket -> {
try
{
// 创建BufferedReader对象
BufferedReader br = new BufferedReader(new InputStreamReader(
System.in));
String line = null;
// 不断读取用户键盘输入,并将用户输入发送到WebSocket
while ((line = br.readLine()) != null)
{
webSocket.sendText(line, true);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
})
.join();
}
}
本程序同样先定义了一个WebSocket.Listener监听器,然后创建了WebSocket对象,接下来程序不断读取用户的键盘输入,并将用户输入通过WebSocket发送到服务器端。
本应用的服务器端同样需要基于Tomcat 9,因此开发起来并不难。但是由于该服务器端程序在接收到WebSocket消息后将消息广播到各个WebSocket客户端,因此需要在服务器端程序中定义一个集合来管理所有的WebSocket客户端的Session。
package org.fkjava.web;
import javax.websocket.*;
import java.util.*;
import javax.websocket.server.*;
/**
* Description:<br>
* 网站: <a href="http://www.crazyit.org">疯狂Java联盟</a><br>
* Copyright (C), 2001-2020, Yeeku.H.Lee<br>
* This program is protected by copyright laws.<br>
* Program Name:<br>
* Date:<br>
* @author Yeeku.H.Lee kongyeeku@163.com<br>
* @version 5.0
*/
// @ServerEndpoint注解修饰的类将作为WebSocket的服务端
@ServerEndpoint(value="/chatSocket")
public class ChatEndpoint
{
static List<Session> clients = Collections
.synchronizedList(new ArrayList<Session>());
@OnOpen // 该注解修饰的方法将会客户端连接时被激发
public void start(Session session)
{
// 每当有客户连接进来时,收集该客户对应的session
clients.add(session);
}
@OnMessage // 该注解修饰的方法将会客户端消息到达时被激发
public void message(String message, Session session) throws Exception
{
// 收到消息后,将消息向所有客户发送一次
for (var s : clients)
{
RemoteEndpoint.Basic remote = s.getBasicRemote();
remote.sendText(message);
}
}
@OnClose // 该注解修饰的方法将会客户端连接关闭时被激发
public void end(Session session, CloseReason closeReason)
{
// 每当有客户连接关闭时,删除该客户对应的session
clients.remove(session);
}
@OnError // 该注解修饰的方法将会客户端出错时被激发
public void error(Session session, Throwable throwable)
{
// 每当有客户连接出错时,删除该客户对应的session
clients.remove(session);
}
}