文章目录
网络
4.1连接到服务器
运行在远程计算机上的服务器软件不停地等待那些希望与自己监听的端口相连接的网络请求。当远程计算机上的操作系统接收到一个请求与相应的端口连接的网络数据包时,它便唤醒正在监听网络连接请求的服务器进程,并为两者建立连接。这种连接将一直保持下去,直到被其中任何一方终止。
如果一台web服务器用相同的ip地址为多个域提供宿主环境,那么在连接这台web server时,就必须提供host键/值对。如果服务器只为单个域提供宿主环境,则可以忽略该键/值对。
4.1.2用java连接到服务器
/**
* This program makes a socket connection to the atomic clock in Boulder, Colorado,
* and prints the time that the server sends.
* 该程序只适用于非常简单的服务器。在比较复杂的网络程序中,客户端发送请求数据给服务器,
* 而服务器可能在响应结束时并不立刻断开连接
*/
public class SocketTest {
public static void main(String[] args) throws IOException {
// 打开一个套接字,是网络软件中的一个抽象概念,负责启动该程序内部和外部之间的通信
try (var s = new Socket("time-a.nist.gov", 13);
var in = new Scanner(s.getInputStream(), StandardCharsets.UTF_8)) {
while (in.hasNextLine()) {
String line = in.nextLine();
System.out.println(line);
}
}
}
}
UDP协议可以用于发送数据包(也称为数据报),它所需付出的开销要比TCP少得多。UDP有一个重要的缺点:数据包无须按照顺序传递到接收应用程序,它们甚至可能在传输过程中全部丢失。UDP让数据包的接收者自己负责对它们进行排序,并请求发送者重新发送那些丢失的数据包。UDP比较适合于那些可以忍受数据包丢失的应用,例如用于音频流和视频流的传输,或者用于连续测量的应用领域。
4.1.3套接字超时
从套接字读取信息时,在有数据可供访问之前,读操作将会被阻塞。如果此时主机不可达,那么应用将要等待很长的时间,并且因为受底层操作系统的限制而最终会导致超时。
对于不同的应用,应该确定合理的超时值。然后调用setSoTimeout
方法设置这个超时值(单位:毫秒)。
var s = new Socket(...);
s.setSoTimeout(10000); // time out after 10 seconds
如果已经为套接字设置了超时值,并且之后的读操作和写操作在没有完成之前就超过了时间限制,那么这些操作就会抛出
SocketTimeoutException
异常。可以捕获这个异常,并对超时做出反应。
try {
InputStream in = s.getInputStream(); // read from in
// ...
} catch (SocketTimeoutException e) {
// react to timeout
}
另外还有一个超时问题是必须解决的。下面这个构造器:
Socket(String host, int port)
会一直无限期地阻塞下去,直到建立了到达主机的初始连接为止。
可以通过先构建一个无连接的套接字,然后再使用一个超时来进行连接的方式来解决这个问题。
var s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);
4.1.4因特网地址
一个因特网地址由4个字节组成(在ipv6中是16个字节),比如129.6.15.28。但是,如果需要在主机名和因特网地址之间进行转换,那么就可以使用
InetAddress
类。
静态的getByName
方法可以返回代表某个主机的InetAddress
对象。例如:
InetAddress address = InetAddress.getByName("time-a.nist.gov");
将返回一个
InetAddress
对象,该对象封装了一个4字节的序列。然后,可以使用getAddress
方法来访问这些字节:
byte[] addressBytes = address.getAddress();
一些访问量较大的主机名通常会对应于多个因特网地址,以实现负载均衡。当访问主机时,会随机选取其中的一个。可以通过调用
getAllByName
方法来获得所有主机:
InetAddress[] addresses = InetAddress.getAllByName(host);
最后需要说明的是,有时可能需要本地主机的地址。如果只是要求得到localhost的地址,那总会得到本地回环地址127.0.0.1,但是其他程序无法用这个地址来连接到这台机器上。此时,可以使用静态的
getLocalHost
方法来得到本地主机的地址:
InetAddress address = InetAddress.getLocalHost();
/**
* This program demonstrates the InetAddress class. Supply a host name as command-line argument,
* or run without command-line arguments to see the address of the local host.
*/
public class InetAddressTest {
public static void main(String[] args) throws UnknownHostException {
if (args.length > 0) {
String host = args[0];
var addresses = InetAddress.getAllByName(host);
for (InetAddress a : addresses) {
System.out.println(a);
}
} else {
var localHostAddress = InetAddress.getLocalHost();
System.out.println(localHostAddress);
}
}
}
4.2实现服务器
4.2.1服务器套接字
一旦启动了服务器程序,它便会等待某个客户端连接到它的端口。
ServerSocket
类用于建立套接字:
// 建立一个负责监控短空8189的服务器
var s = new ServerSocket(8189);
以下命令:
Socket incoming = s.accept();
用于告诉程序不停地等待,直到有客户端连接到这个端口。一旦有人通过网络发送了正确的连接请求,并以此连接到了端口上,该方法就会返回一个表示连接已经建立的
Socket
对象。可以使用这个对象来得到输入流和输出流:
InputStream inStream = incoming.getInputStream();
OutputStream outStream = incoming.getOutputStream();
每一个服务器程序,比如一个http web服务器,都会不间断地执行下面这个循环:
- 通过输入数据流从客户端接收一个命令。
- 解码这个客户端命令。
- 收集客户端所请求的信息。
- 通过输出数据流发送信息给客户端。
/**
* This program implements a simple server that listens to port 8189 and echoes back all client input.
*/
public class EchoServer {
public static void main(String[] args) throws IOException {
// establish server socket
try (var s = new ServerSocket(8189)) {
// wait for client connection
try (var incoming = s.accept()) {
var inStream = incoming.getInputStream();
var outStream = incoming.getOutputStream();
try (var in = new Scanner(inStream, StandardCharsets.UTF_8)) {
// autoFlush
var out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true);
out.println("Hello! Enter BYE to exit.");
// echo client input
var done = false;
while (!done && in.hasNextLine()) {
var line = in.nextLine();
out.println("Echo: " + line);
if (line.trim().equals("BYE")) {
done = true;
}
}
}
}
}
}
}
4.2.2为多个客户端服务
通常,服务器总是不间断地运行在服务器计算机上,来自整个因特网的用户希望同时使用服务器。简单服务器会提供对客户端连接的支持,使得任何一个客户端都可以因长时间地连接服务而独占服务,其实可以使用多线程进行优化。
每当程序建立一个新的套接字连接,也就是说当调用accept
时,将会启动一个新的线程来处理服务器和该客户端之间的连接,而主程序将立即返回并等待下一个连接。
/**
* This program implements a multithreaded server that listens to port 8189 and echoes back all client input.
* 在这个程序中,为每个连接生成一个单独的线程,这种方法并不能满足高性能服务器的要求。
* 为使服务器实现更高的吞吐量,可以使用java.nio包中的一些特性
*/
public class ThreadedEchoServer {
public static void main(String[] args) {
try (var s = new ServerSocket(8189)) {
int i = 1;
while (true) {
var incoming = s.accept();
System.out.println("Spawning " + i);
var r = new ThreadedEchoHandler(incoming);
var t = new Thread(r);
t.start();
i++;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* This class handles the client input for one server socket connection.
*/
class ThreadedEchoHandler implements Runnable {
private Socket incoming;
/**
* Constructs a handler.
* @param incoming the incoming socket
*/
public ThreadedEchoHandler(Socket incoming) {
this.incoming = incoming;
}
@Override
public void run() {
try (var inStream = incoming.getInputStream();
var outStream = incoming.getOutputStream();
var in = new Scanner(inStream, StandardCharsets.UTF_8);
var out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true)) {
out.println("Hello! Enter BYE to exit.");
// echo client input
var done = false;
while (!done && in.hasNextLine()) {
var line = in.nextLine();
out.println("Echo: " + line);
if (line.trim().equals("BYE")) {
done = true;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2.3半关闭
半关闭提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍旧可以接收来自另一端的数据。
这是一种很典型的情况,例如在向服务器传输数据,但是一开始并不知道要传输多少数据。在向文件写数据时,只需在数据写入后关闭文件即可。但是,如果关闭一个套接字,那么与服务器的连接将立刻断开,因而也就无法读取服务器的响应了。
使用半关闭的方法就可以解决上述问题。可以通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保持输入流处于打开状态。
try (var socket = new Socket(host, port)) {
var in = new Scanner(socket.getInputStream(), StandardCharsets.UTF_8);
var writer = new PrintWriter(socket.getOutputStream());
// send request data
writer.print(...);
writer.flush();
socket.shutdownOutput();
// now socket is half-closed
// read response data
while (in.hasNextLine() != null) {
String line = in.nextLine();
// ...
}
}
服务器端将读取输入信息,直至到达输入流的结尾,然后它再发送响应。
当然,该协议只适用于一站式的服务,例如http服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。
4.2.4可中断套接字
当连接到一个套接字时,当前线程将会被阻塞直到建立连接或产生超时为止。同样的,当通过套接字读数据时,当前线程也会被阻塞直到操作成功或产生超时为止。
在交互式的应用中,也许会考虑为用户提供一个选项,用以取消那些看似不会产生结果的连接。但是,当线程因套接字无法响应而发生阻塞时,则无法通过interrupt
来解除阻塞。
为了中断套接字操作,可以使用java.nio包提供的一个特性——SocketChannel
类。可以用如下方法打开SocketChannel
:
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));
通道并没有与之相关联的流。实际上,它所拥有的
read
和write
方法都是通过使用Buffer
对象来实现的。ReadableByteChannel
和WritableByteChannel
接口都声明了这两个方法。
如果不想处理缓冲区,可以使用Scanner
类从SocketChannel
中读取信息,因为Scanner
有一个带ReadableByteChannel
参数的构造器:
var in = new Scanner(channel, StandardCharsets.UTF_8);
通过调用静态方法
Channels.newOutputStream
,可以将通道转换成输出流。
OutputStream outStream = Channels.newOutputStream(channel);
上述操作就是所有要做的事情。当线程正在执行打开、读取或写入操作时,如果线程发生中断,那么这些操作将不会陷入阻塞,而是以抛出异常的方式结束。
/**
* 对比可中断套接字和阻塞套接字:服务器将连续发送数字,并在每发送十个数字之后停滞一下
*/
public class InterruptibleSocketTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
var frame = new InterruptibleSocketFrame();
frame.setTitle("InterruptibleSocketTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
class InterruptibleSocketFrame extends JFrame {
private Scanner in;
private JButton interruptibleButton;
private JButton blockingButton;
private JButton cancelButton;
private JTextArea messages;
private TestServer server;
private Thread connectThread;
public InterruptibleSocketFrame() {
var northPanel = new JPanel();
add(northPanel, BorderLayout.NORTH);
final int TEXT_ROWS = 20;
final int TEXT_COLUMNS = 60;
messages = new JTextArea(TEXT_ROWS, TEXT_COLUMNS);
add(new JScrollPane(messages));
interruptibleButton = new JButton("Interruptible");
blockingButton = new JButton("Blocking");
northPanel.add(interruptibleButton);
northPanel.add(blockingButton);
interruptibleButton.addActionListener(event -> {
interruptibleButton.setEnabled(false);
blockingButton.setEnabled(false);
cancelButton.setEnabled(true);
connectThread = new Thread(() -> {
try {
connectInterruptibly();
} catch (IOException e) {
messages.append("\nInterruptibleSocketTest.connectInterruptibly: " + e);
}
});
connectThread.start();
});
blockingButton.addActionListener(event -> {
interruptibleButton.setEnabled(false);
blockingButton.setEnabled(false);
cancelButton.setEnabled(true);
connectThread = new Thread(() -> {
try {
connectBlocing();
} catch (IOException e) {
messages.append("\nInterruptibleSocketTest.connectBlocing: " + e);
}
});
connectThread.start();
});
cancelButton = new JButton("Cancel");
cancelButton.setEnabled(false);
northPanel.add(cancelButton);
cancelButton.addActionListener(event -> {
connectThread.interrupt();
cancelButton.setEnabled(false);
});
server = new TestServer();
new Thread(server).start();
pack();
}
/**
* Connects to the test server, using interruptible I/O.
* @throws IOException
*/
public void connectInterruptibly() throws IOException {
messages.append("Interruptible:\n");
try (var channel = SocketChannel.open(new InetSocketAddress("localhost", 8189))) {
in = new Scanner(channel, StandardCharsets.UTF_8);
while (!Thread.currentThread().isInterrupted()) {
messages.append("Reading ");
if (in.hasNextLine()) {
var line = in.nextLine();
messages.append(line);
messages.append("\n");
}
}
} finally {
EventQueue.invokeLater(() -> {
messages.append("Channel closed\n");
interruptibleButton.setEnabled(true);
blockingButton.setEnabled(true);
});
}
}
/**
* Connects to the test server, using blocking I/O.
* @throws IOException
*/
public void connectBlocing() throws IOException {
messages.append("Blocking:\n");
try (var sock = new Socket("localhost", 8189)) {
in = new Scanner(sock.getInputStream(), StandardCharsets.UTF_8);
while (!Thread.currentThread().isInterrupted()) {
messages.append("Reading ");
if (in.hasNextLine()) {
var line = in.nextLine();
messages.append(line);
messages.append("\n");
}
}
} finally {
EventQueue.invokeLater(() -> {
messages.append("Socket closed\n");
interruptibleButton.setEnabled(true);
blockingButton.setEnabled(true);
});
}
}
/**
* A multithreaded server that listens to port 8189 and sends numbers to the client,
* simulating a hanging server after 10 numbers.
*/
class TestServer implements Runnable {
@Override
public void run() {
try (var s = new ServerSocket(8189)) {
while (true) {
var incoming = s.accept();
var r = new TestServerHandler(incoming);
new Thread(r).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* This class handles the client input for one server socket connection.
*/
class TestServerHandler implements Runnable {
private Socket incoming;
private int counter;
/**
* Constructs a handler.
* @param incoming the incoming socket
*/
public TestServerHandler(Socket incoming) {
this.incoming = incoming;
}
@Override
public void run() {
try {
try {
var outStream = incoming.getOutputStream();
var out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true);
while (counter < 100) {
counter++;
if (counter <= 10) {
out.println(counter);
}
Thread.sleep(100);
}
} finally {
incoming.close();
messages.append("Closing server\n");
}
} catch (Exception e) {
messages.append("\nTestServerHandler.run: " + e);
}
}
}
}
4.3获取web数据
4.3.1URL
和URI
URL
和URLConnection
类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息。例如,可以自一个字符串构建一个URL
对象:
var url = new URL(urlString);
如果只是想获得该资源的内容,可以使用
URL
类中的openStream
方法。该方法将产生一个InputStream
对象,然后就可以按照一般的用法来使用这个对象了,比如用它构建一个Scanner
对象:
InputStream inStream = url.openStream();
var in = new Scanner(inStream, StandardCharsets.UTF_8);
java.net包对统一资源定位符(URL)和统一资源标识符(URI)进行非常有用的区分。
URI
是个纯粹的语法结构,包含用来指定web资源的字符串的各种组成部分。URL
是URI
的一个特例,它包含了用于定位web资源的足够信息。其他URI,比如mailto:cay@horstmann.com
则不属于定位符,因为根据该标识符无法定位任何数据。
在java类库中,URI
类并不包含任何用于访问资源的方法,它的唯一作用就是解析。但是,URL
类可以打开一个连接到资源的流。因此,URL
类只能作用于那些java类库知道该如何处理的模式,例如http:
、https:
、ftp:
、本地文件系统(file:
)和jar文件(jar:
)。
由于
URL
会变得很复杂,因此对URI
进行解析并非小事一桩。URI
规范给出了标记这些标识符的规则。一个URI
具有以下语法:[scheme:]schemeSpecificPart[#fragment]
。
- 上式中,
[...]
表示可选部分,并且:
和#
可以被包含在标识符内。- 包含
scheme:
部分的URI
称为绝对URI
。否则,称为相对URI
。- 如果绝对
URI
的schemeSpecificPart
不是以/
开头的,就称它是不透明的。例如,mailto:cay@horstmann.com
。- 所有绝对的透明
URI
和所有相对URI
都是分层的。例如,http://horstmann.com/index.html
和../../java/net/Socket.html#Socket()
。- 一个分层
URI
的schemeSpecificPart
具有以下结构:[//authority][path][?query]
。在这里,[...]
同样表示可选的部分。- 对于那些基于服务器的
URI
,authority
部分具有以下形式:[user-info@]host[:port]
,port
必须是一个整数。
URI
类的作用之一是解析标识符并将它分解成各种不同的组成部分,可以用相应的方法读取它们(详见API)。
URI
类的另一个作用是处理绝对标识符和相对标识符。如果存在一个绝对URI
:http://docs.mycompany.com/api/java/net/ServerSocket.html
和一个相对URI
:../../java/net/Socket.html#Socket()
,那么可以用它们组合出一个绝对URI
:http://docs.mycompany.com/api/java/net/Socket.html#Socket()
。这个过程称为解析相对URL
。
与此相反的过程称为相对化。例如,假设有一个基本URI
:http://docs.mycompany.com/api
和另一个URI
:http://docs.mycompany.com/api/java/lang/String.html
,那么相对化之后的URI
就是java/lang/String.html
。
URI
类同时支持以下两个操作:
relative = base.relativize(combined);
combined = base.resolve(relative);
4.3.2使用URLConnection
获取信息
如果想从某个web资源获取更多信息,那么应该使用
URLConnection
类,通过它能够得到比基本的URL
类更多的控制功能。
当操作一个URLConnection
对象时,必须像下面这样非常小心地安排操作步骤:
- 调用
URL
类中的openConnection
方法获得URLConnection
对象:
URLConnection connection = URL.openConnection();
- 使用以下方法来设置任意的请求属性:
setDoInput(boolean doinput);
setDoOutput(boolean dooutput);
setIfModifiedSince(long ifmodifiedsince);
setUseCaches(boolean usecaches);
setAllowUserInteraction(boolean allowuserinteraction);
setRequestProperty(String key, String value);
setConnectTimeout(int timeout);
setReadTimeout(int timeout);
- 调用
connect
方法连接远程资源,除了与服务器建立套接字连接外,该方法还可以用于向服务器查询头信息:
connection.connect();
- 与服务器建立连接后,可以查询头信息。
getHeaderFieldKey
和getHeaderField
这两个方法枚举了消息头的所有字段。getHeaderFields
方法返回一个包含了消息头中所有字段的标准Map
对象。为了方便使用,以下方法可以查询各标准字段:
getContentType();
getContentLength();
getContentEncoding();
getDate();
getExpiration();
getLastModified();
- 最后,访问资源数据。使用
getInputStream
方法获取一个输入流用以读取信息(这个输入流与URL
类中的openStream
方法所返回的流相同)。另一个方法getContent
在实际操作中并不是很有用。由标准内容类型(比如text/plain
和image/gif
)所返回的对象需要使用com.sun层次结构中的类来进行处理。也可以注册自己的内容处理器。
在默认情况下,建立的连接只产生从服务器读取信息的输入流,并不产生任何执行写操作的输出流。如果想获得输出流(例如,用于向一个web服务器提交数据),那么需要调用:
connection.setDoOutput(true);
setIfModifiedSince
方法用于告诉连接,只对自某个特定日期以来被修改过的数据感兴趣。
setRequestProperty
可以用来设置对特定协议起作用的任何键值对。
一旦调用了
connect
方法,就可以查询响应头的信息了。
String key = connection.getHeaderFieldKey(n);
可以获得响应头的第n个键,其中n从1开始!如果n为0或大于消息头的字段总数,该方法将返回
null
值。没有哪种方法可以返回字段的数量,必须反复调用getHeaderFieldKey
方法直到返回null
为止。同样的,调用以下方法:
String value = connection.getHeaderField(n);
可以得到第n个值。
getHeaderFields
方法可以返回一个封装了响应头字段的Map
对象。
Map<String, List<String>> headerFields = connection.getHeaderFields();
可以用
connection.getHeaderField(0)
或headerFields.get(null)
获取响应状态行(例如HTTP/1.1 200 OK
)。
为了简便起见,java提供了6个方法用以访问最常用的消息头类型的值,并在需要的时候将它们转换成数字类型。返回类型为
long
的方法返回的是从格林尼治时间1970年1月1日开始计算的秒数。
键名 | 方法名 | 返回类型 |
---|---|---|
Date | getDate | long |
Expires | getExpiration | long |
Last-Modified | getLastModified | long |
Content-Length | getContentLength | int |
Content-Type | getContentType | String |
Content-Encoding | getContentEncoding | String |
/**
* This program connects to a URL and displays the response header data and the first 10 lines of the requested data.
*
* Supply the URL and an optional username and password (for HTTP basic authentication) on the command line.
*/
public class URLConnectionTest {
public static void main(String[] args) {
try {
String urlName;
if (args.length > 0) {
urlName = args[0];
} else {
urlName = "https://horstmann.com";
}
var url = new URL(urlName);
var connection = url.openConnection();
// set username, password if specified on command line
if (args.length > 2) {
String username = args[1];
String password = args[2];
String input = username + ":" + password;
var encoder = Base64.getEncoder();
var encoding = encoder.encodeToString(input.getBytes(StandardCharsets.UTF_8));
connection.setRequestProperty("Authorization", "Basic " + encoding);
}
connection.connect();
// print header fields
var headers = connection.getHeaderFields();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
var key = entry.getKey();
for (String value : entry.getValue()) {
System.out.println(key + ": " + value);
}
}
// print convenience functions
System.out.println("----------");
System.out.println("getContentType: " + connection.getContentType());
System.out.println("getContentLength: " + connection.getContentLength());
System.out.println("getContentEncoding: " + connection.getContentEncoding());
System.out.println("getDate: " + connection.getDate());
System.out.println("getExpiration: " + connection.getExpiration());
System.out.println("getLastModifed: " + connection.getLastModified());
System.out.println("----------");
var encoding = connection.getContentEncoding();
if (encoding == null) {
encoding = "UTF-8";
}
try (var in = new Scanner(connection.getInputStream(), encoding)) {
// print first ten lines of contents
for (int n = 1; in.hasNextLine() && n <= 10; n++) {
System.out.println(in.nextLine());
}
if (in.hasNextLine()) {
System.out.println("...");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.3.3提交表单数据
在向web服务器发送信息时,通常有两个命令会被用到:
GET
和POST
。
在使用GET
命令时,只需将参数附在URL
的结尾处即可。
URL url = new URL("http://host/path?query");
URLConnection connection = url.openConnection();
connection.setRequestProperty("accept", "*/*");
// ...
connection.connect();
在
POST
请求中,不会在URL
上附着参数,而是从URLConnection
中获得输出流,并将键值对写入到该输出流中。仍旧需要对这些值进行URL
编码,并用&
字符将它们隔开。
var url = new URL("http://host/path");
URLConnection connection = url.openConnection();
// 建立一个用于输出的连接
connection.setDoOutput(true);
// 向服务器发送数据
var out = new PrintWriter(connection.getOutputStream(), StandardCharsets.UTF_8);
out.print(name1 + "=" + URLEncoder.encode(value1, StandardCharsets.UTF_8) + "&");
out.print(name2 + "=" + URLEncoder.encode(value2, StandardCharsets.UTF_8));
// 关闭输出流
out.close();
// 调用getInputStream方法读取服务器的响应
/**
* This program demonstrates how to use the URLConnection class for a POST request.
*/
public class PostTest {
public static void main(String[] args) throws IOException {
var propsFilename = args.length > 0 ? args[0] : "/Users/kirito/program/java/study/java_study/core_java_volume_two_11/src/main/resources/post.properties";
var props = new Properties();
try (var in = Files.newInputStream(Paths.get(propsFilename))) {
props.load(in);
}
var urlString = props.remove("url").toString();
var userAgent = props.remove("User-Agent");
var redirects = props.remove("redirects");
// 如果cookie需要在重定向中从一个站点发送给另一个站点
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
var result = doPost(new URL(urlString), props, userAgent == null ? null : userAgent.toString(), redirects == null ? -1 : Integer.parseInt(redirects.toString()));
System.out.println(result);
}
/**
* Do an HTTP POST.
* @param url the URL to post to
* @param nameValuePairs the query parameters
* @param userAgent the user agent to use, or null for the default user agent
* @param redirects the number of redirects to follow manually, or -1 for automatic redirects
* @return the data returned from the server
*/
public static String doPost(URL url, Map<Object, Object> nameValuePairs, String userAgent, int redirects) throws IOException {
var connection = (HttpURLConnection) url.openConnection();
if (userAgent != null) {
connection.setRequestProperty("User-Agent", userAgent);
}
if (redirects >= 0) {
// 尽管重定向通常是自动处理的,但是有些情况下,需要自己完成重定向。例如,HTTP和HTTPS之间的自动重定向因为安全原因而不被支持。
// 在这些情况下,可以人工实现重定向。在连接到服务器之前,将关闭自动重定向
connection.setInstanceFollowRedirects(false);
}
// 打开输出流
connection.setDoOutput(true);
try (var out = new PrintWriter(connection.getOutputStream())) {
var first = true;
for (Map.Entry<Object, Object> pair : nameValuePairs.entrySet()) {
if (first) {
first = false;
} else {
out.print('&');
}
var name = pair.getKey().toString();
var value = pair.getValue().toString();
out.print(name);
out.print('=');
out.print(URLEncoder.encode(value, StandardCharsets.UTF_8));
}
}
// 在从写出请求切换到读取响应的任何部分时,就会发生与服务器的实际交互
var encoding = connection.getContentEncoding();
if (encoding == null) {
encoding = "UTF-8";
}
if (redirects > 0) {
// 在将POST数据发送给服务器时,服务器端产生的响应可能是redirect:,后面跟着一个完全不同的URL,该URL应该被调用以获取实际的信息
var responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
var location = connection.getHeaderField("Location");
if (location != null) {
var base = connection.getURL();
connection.disconnect();
return doPost(new URL(base, location), nameValuePairs, userAgent, redirects - 1);
}
}
} else if (redirects == 0) {
throw new IOException("Too many redirects");
}
var response = new StringBuilder();
try (var in = new Scanner(connection.getInputStream(), encoding)) {
while (in.hasNextLine()) {
response.append(in.nextLine());
response.append("\n");
}
} catch (IOException e) {
// 在读取响应过程中会碰到一个问题。如果服务器端出现错误,那么调用connection.getInputStream()时就会抛出一个FileNotFoundException异常。
// 但是,此时服务器仍然会向浏览器返回一个错误页面(例如,404)。为了捕捉这个错误页,可以调用getErrorStream方法
var err = connection.getErrorStream();
if (err == null) {
throw e;
}
try (var in = new Scanner(err)) {
response.append(in.nextLine());
response.append("\n");
}
}
return response.toString();
}
}
4.4HTTP客户端
HttpClient
提供了更便捷的api和对http/2的支持。
与URLConnection
类相比,http客户端api从设计初始就提供了一种更简单的连接到web服务器的机制。
HttpClient
对象可以发出请求并接收响应。可以通过下面的调用获取客户端:
HttpClient client = HttpClient.newHttpClient();
或者,如果需要配置客户端,可以使用像下面这样的构建器api:
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
即,获取一个构建器,调用其方法定制需要待构建的项,然后调用
build
方法来终结构建过程。这是一种构建不可修改对象的常见模式。
还可以遵循构建器模式来定制请求:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://horstmann.com"))
.GET()
.build();
对于
POST
请求,需要一个体发布器,它会将请求数据转换为要推送的数据。有针对字符串、字节数组和文件的体发布器。例如,json请求:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonString))
.build();
遗憾的是,该api不支持对常见内容类型做上面锁要求的格式化处理。
在发送请求时,必须告诉客户端如果处理响应。如果只是想将体当作字符串处理,那么就可以像下面这样来发送请求:
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse
类是一个泛型类,它的类型参数表示体的类型。可以直接获取响应体字符串:
String bodyString = response.body();
还有其他的响应体处理器,可以将响应作为字节数组或输入流来获取。
BodyHandlers.ofFile(filePath)
会产生一个处理器,将响应存储到给定的文件中,BodyHandlers.ofFileDownload(directoryPath)
会用Content-Disposition
头中的信息将响应存入给定的目录中。最后,从BodyHandlers.dicarding()
中获取的处理器会直接丢弃响应。
HttpResponse
对象还会产生状态码与响应头:
int status = response.statusCode();
HttpHeaders responseHeaders = response.headers();
可以将
HttpHeader
对象转换为一个映射表:
Map<String, List<String>> headerMap = responseHeaders.map();
如果只想要某个特定键的值,并且知道它没有多个值,那么可以调用
firstValue
方法:
Optional<String> lastModified = headerMap.firstValue("Last-Modified");
可以异步地处理响应。在构建客户端时,可以提供一个执行器:
ExecutorService executor = Executors.newCachedThreadPool();
HttpClient client = HttpClient.newBuilder().executor(executor).build();
构建一个请求,然后在该客户端上调用
sendAsync
方法,就会得到一个CompletableFuture<HttpResponse<T>>
对象,其中T
是体处理器的类型。
HttpRequest request = HttpRequest.newBuilder().uri(url).GET().build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(reponse -> ...);
public class HttpClientTest {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException {
System.setProperty("jdk.httpclient.HttpClient.log", "headers,errors");
var propsFilename = args.length > 0 ? args[0] : "client/post.properties";
var propsPath = Paths.get(propsFilename);
var props = new Properties();
try (var in = Files.newInputStream(propsPath)) {
props.load(in);
}
var urlString = "" + props.remove("url");
var contentType = "" + props.remove("Content-Type");
if (contentType.equals("multipart/form-data")) {
var generator = new Random();
var boundary = new BigInteger(256, generator).toString();
contentType = ";boundary=" + boundary;
props.replaceAll((k, v) ->
v.toString().startsWith("file://") ? propsPath.getParent().resolve(Paths.get(v.toString().substring(7))) : v
);
}
var result = doPost(urlString, contentType, props);
System.out.println(result);
}
public static String doPost(String url, String contentType, Map<Object, Object> data) throws IOException, URISyntaxException, InterruptedException {
var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
BodyPublisher publisher = null;
if (contentType.startsWith("multipart/form-data")) {
var boundary = contentType.substring(contentType.lastIndexOf("=") + 1);
publisher = MoreBodyPublishers.ofMimeMultipartData(data, boundary);
} else if (contentType.equals("application/x-www-form-urlencoded")) {
publisher = MoreBodyPublishers.ofFormData(data);
} else {
contentType = "application/json";
publisher = MoreBodyPublishers.ofSimpleJSON(data);
}
var request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("Content-Type", contentType)
.POST(publisher)
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
}
class MoreBodyPublishers {
private static Map<Character, String> replacements = Map.of('\b', "\\b", '\f', "\\f", '\n', "\\n", '\r', "\\r", '\t', "\\t", '"', "\\\"", '\\', "\\\\");
public static BodyPublisher ofFormData(Map<Object, Object> data) {
var first = true;
var builder = new StringBuilder();
for (Map.Entry<Object, Object> entry : data.entrySet()) {
if (first) {
first = false;
} else {
builder.append("&");
}
builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8));
builder.append("=");
builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8));
}
return BodyPublishers.ofString(builder.toString());
}
private static byte[] bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
public static BodyPublisher ofMimeMultipartData(Map<Object, Object> data, String boundary) throws IOException {
var byteArrays = new ArrayList<byte[]>();
var separator = bytes("--" + boundary + "\nContent-Disposition: form-data; name=");
for (Map.Entry<Object, Object> entry : data.entrySet()) {
byteArrays.add(separator);
if (entry.getValue() instanceof Path) {
var path = (Path) entry.getValue();
var mimeType = Files.probeContentType(path);
byteArrays.add(bytes("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName() + "\"\nContent-Type: " + mimeType + "\n\n"));
byteArrays.add(Files.readAllBytes(path));
} else {
byteArrays.add(bytes("\"" + entry.getKey() + "\"\n\n" + entry.getValue() + "\n"));
}
}
byteArrays.add(bytes("--" + boundary + "--"));
return BodyPublishers.ofByteArrays(byteArrays);
}
public static BodyPublisher ofSimpleJSON(Map<Object, Object> data) {
var builder = new StringBuilder();
builder.append("{");
var first = true;
for (Map.Entry<Object, Object> entry : data.entrySet()) {
if (first) {
first = false;
} else {
builder.append(",");
}
builder.append(jsonEscape(entry.getKey().toString())).append(": ").append(jsonEscape(entry.getValue().toString()));
}
builder.append("}");
return BodyPublishers.ofString(builder.toString());
}
private static StringBuilder jsonEscape(String str) {
var result = new StringBuilder("\"");
for (int i = 0; i < str.length(); i++) {
var ch = str.charAt(i);
var replacement = replacements.get(ch);
if (replacement == null) {
result.append(ch);
} else {
result.append(replacement);
}
}
result.append("\"");
return result;
}
}