Appearance
Java 基础:网络编程
1. 连接到服务器
1.1. 一个简单示例
打开一个 PowerShell 并执行:
Bash
$ telnet time-a.nist.gov 13得到输出:
Text
60186 23-08-30 02:50:46 50 0 0 854.7 UTC(NIST) *下面这段 Java 代码的作用与上述例子中的 telnet 是相同的:
Java
package socket;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
/**
* This program makes a socket connection to the atomic clock in Boulder, Colorado, and prints
* the time that the server sends.
*
* @author Cay Horstmann
* @version 1.22 2018-03-17
*/
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);
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1.2. 套接字超时
可以通过调用 setSoTimeout 方法设置读、写操作的超时时间,在超过了时间限制后将会抛出 SocketTimeoutException 异常:
Java
var s = new Socket(...);
s.setSoTimeout(10000); // time out after 10 seconds
try {
InputStream in = s.getInputStream(); // read from in
// ...
} catch (SocketTimeoutException e) {
// react to timeout
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
另外 Socket(String host, int port) 该构造器会一直无限期地阻塞下去,直到建立了到达主机的初始连接为止。我们可以通过先构建一个无连接的套接字,然后再使用一个超时来进行连接的方式解决这个问题:
Java
var s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);1
2
2
1.3. 因特网地址
如果需要在主机名和因特网地址之间进行转换,那么就可以使用 InetAddress 类(如果操作系统支持 IPv6 格式的地址,那么 java.net 包也支持)。静态的 getByName 方法可以返回代表某个主机的 InetAddress 对象,例如:
Java
InetAddress address = InetAddress.getByName("timea.nist.gov");将返回一个 InetAddress 对象,该对象封装了一个 4 字节的序列(例如 129.6.15.28)。然后,可以使用 getAddress 方法来访问这些字节:
Java
byte[] addressBytes = address.getAddress();一些访问量较大的主机名通常会对应于多个因特网地址,以实现负载均衡。例如,主机名 google.com 可能就对应着 12 个不同的因特网地址。当访问主机时,会随机选取其中的一个。可以通过调用 getAllByName 方法来获得所有主机:
Java
InetAddress[] addresses = InetAddress.getAllByName(host);如果想要获取本地主机的地址,则可以使用静态的 getLocalHost 方法来得到本地主机的地址:
Java
InetAddress address = InetAddress.getLocalHost();2. 实现服务器
2.1. 服务器套接字
以下是一个使用服务器套接字的简单完整示例:
Java
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class EchoServer {
private static final int PORT = 8189;
private static final String EXIT_COMMAND = "BYE";
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("Server started on port " + PORT);
while (true) {
try (Socket clientSocket = serverSocket.accept()) {
handleClient(clientSocket);
} catch (IOException e) {
System.out.println("Error accepting client connection: " + e.getMessage());
}
}
} catch (IOException e) {
System.out.println("Error starting the server: " + e.getMessage());
}
}
private static void handleClient(Socket clientSocket) {
try (
InputStream inStream = clientSocket.getInputStream();
OutputStream outStream = clientSocket.getOutputStream();
Scanner in = new Scanner(inStream, StandardCharsets.UTF_8);
PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true)
) {
out.println("Hello! Enter " + EXIT_COMMAND + " to exit.");
while (in.hasNextLine()) {
String line = in.nextLine();
out.println("Echo: " + line);
if (line.trim().equals(EXIT_COMMAND)) {
break;
}
}
} catch (IOException e) {
System.out.println("Error handling client communication: " + e.getMessage());
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ServerSocket 用于建立套接字。accept 方法会不停地等待,直到有客户端连接到这个端口。一旦有人通过网络发送了正确的连接请求,并以此连接到了端口上,该方法就会返回一个表示连接已经建立的 Socket 对象,你可以使用这个对象来得到输入流和输出流进行后续的处理。
编译并运行这个程序后,便可以使用 telnet 连接到服务器 localhost 和端口 8189。可以随意键入一条信息,然后观察屏幕上的回送信息。输入 BYE 可以断开当前连接。
2.2. 为多个客户端服务
假设我们希望有多个客户端同时连接到我们的服务器。每当程序建立一个新的套接字连接,也就是说当调用 accept() 时,将会启动一个新的线程来处理服务器和该客户端之间的连接,而主程序将立即返回并等待下一个连接。为了实现这种机制,服务器应该具有类似以下代码的循环操作:
Java
while (true) {
Socket incoming = s.accept();
var r = new ThreadedEchoHandler(incoming);
var t = new Thread(r);
t.start();
}
class ThreadedEchoHandler implements Runnable {
// ...
public void run() {
try (InputStream inStream = incoming.getInputStream();
OutputStream outStream = incoming.getOutputStream()) {
// Process input and send response
} catch(IOException e) {
// Handle exception
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这下每个连接都会启动一个新的线程,所以多个客户端就可以同时连接到服务器了。对此可以做个简单的测试:
- 编译和运行服务器程序;
- 打开多个
telnet窗口; - 在这些窗口之间切换,并键入命令(注意你可以同时通过这些窗口进行通信);
- 当完成之后,切换到启动服务器程序的窗口,并使用
CTRL + C强行关闭它;
Note:在这个程序中,我们为每个连接生成一个单独的线程。这种方法并不能满足高性能服务器的要求。为使服务器实现更高的吞吐量,可以使用
java.nio包中一些特性。详情请参见以下链接:http://www.ibm.com/developerworks/java/library/j-javaio。
2.3. 半关闭
半关闭(half-close)提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍旧可以接收来自另一端的数据。
这是一种很典型的情况,例如我们在向服务器传输数据,但是一开始并不知道要传输多少数据。在向文件写数据时,我们只需在数据写入后关闭文件即可。但是,如果关闭一个套接字,那么与服务器的连接将立刻断开,因而也就无法读取服务器的响应了。
使用半关闭的方法就可以解决上述问题。可以通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保持输入流处于打开状态。
如下代码演示了如何在客户端使用半关闭方法:
Java
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();
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
服务器端将读取输入信息,直至到达输入流的结尾,然后它再发送响应。
当然,该协议只适用于一站式(one-shot)的服务,例如 HTTP 服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。
2.4. 可中断套接字
当连接到一个套接字时,当前线程将会被阻塞直到建立连接或产生超时为止。同样地,当通过套接字读数据时,当前线程也会被阻塞直到操作成功或产生超时为止。在交互式的应用中,也许会考虑为用户提供一个选项,用以取消那些看似不会产生结果的连接。但是,当线程因套接字无法响应而发生阻塞时,则无法通过调用 interrupt 来解除阻塞。为了中断套接字操作,可以使用 java.nio 包提供的一个特性 —— SocketChannel 类。可以使用如下方法打开 SocketChannel:
Java
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));通道(channel)并没有与之相关联的流。实际上,它所拥有的 read 和 write 方法都是通过使用 Buffer 对象来实现的。ReadableByteChannel 接口和 WritableByteChannel 接口都声明了这两个方法。
如果不想处理缓冲区,可以使用 Scanner 类从 SocketChannel 中读取信息,因为 Scanner 有个带 ReadableByteChannel 参数的构造器:
Java
var in = new Scanner(channel, StandardCharsets.UTF_8);通过调用静态方法 Channels.newOutputStream,可以将通道转换成输出流:
Java
OutputStream outStream = Channels.newOutputStream(channel);上述操作就是所有要做的事情。当线程正在执行打开、读取或写入操作时,如果线程发生中断,那么这些操作将不会陷入阻塞,而是以抛出异常的方式结束。
以下是一个完整的参考示例:
Java
package interruptible;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.net.*;
import java.io.*;
import java.nio.charset.*;
import java.nio.channels.*;
import javax.swing.*;
/**
* This program shows how to interrupt a socket channel.
*
* @author Cay Horstmann
* @version 1.05 2018-03-17
*/
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 {
connectBlocking();
} catch (IOException e) {
messages.append("\nInterruptibleSocketTest.connectBlocking: " + 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
*/
public void connectInterruptibly() throws IOException {
messages.append("Interruptible:\n");
try (SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8189))) {
in = new Scanner(channel, StandardCharsets.UTF_8);
while (!Thread.currentThread().isInterrupted()) {
messages.append("Reading ");
if (in.hasNextLine()) {
String 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
*/
public void connectBlocking() 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()) {
String 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 {
public void run() {
try (var s = new ServerSocket(8189)) {
while (true) {
Socket incoming = s.accept();
Runnable r = new TestServerHandler(incoming);
new Thread(r).start();
}
} catch (IOException e) {
messages.append("\nTestServer.run: " + e);
}
}
}
/**
* 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 i the incoming socket
*/
public TestServerHandler(Socket i) {
incoming = i;
}
public void run() {
try {
try {
OutputStream outStream = incoming.getOutputStream();
var out = new PrintWriter(
new OutputStreamWriter(outStream, StandardCharsets.UTF_8),
true /* autoFlush */
);
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);
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
服务器将连续发送数字,并在每发送一个数字之后停滞一下。点击两个按钮中的任何一个,都会启动一个线程来连接服务器并打印输出。第一个线程使用可中断套接字,而第二个线程使用阻塞套接字。如果在第一批的十个数字的读取过程中点击 “Cancel” 按钮,这两个线程都会中断。但是,在第一批十个数字之后,就只能中断第一个线程了,第二个线程将保持阻塞直到服务器最终关闭连接。
3. 获取 Web 数据
3.1. URL 和 URI
URL 和 URLConnection 类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息。例如,可以自一个字符串构建一个 URL 对象:
Java
var url = new URL(urlString);如果只是想获得该资源的内容,可以使用 URL 类中的 openStream 方法。该方法将产生一个 InputStream 对象,然后就可以按照一般的用法来使用这个对象了,比如用它构建一个 Scanner 对象:
Java
InputStream inStream = url.openStream();
var in = new Scanner(inStream, StandardCharsets.UTF_8);1
2
2
java.net 包对统一资源定位符(Uniform Resource Locators,URL)和统一资源标识符(Uniform Resource Identifiers,URI)进行了非常有用的区分。
URI 是个纯粹的语法结构,包含用来指定 Web 资源的字符串的各种组成部分。URL 是 URI 的一个特例,它包含了用于定位 Web 资源的足够信息。其他 URI,比如 mailto:cay@horstmann.com 则不属于定位符,因为根据该标识符我们无法定位任何数据。像这样的 URI 我们称之为 URN(Uniform Resource Name,统一资源名称)。
在 Java 类库中,URI 类并不包含任何用于访问资源的方法,它的唯一作用就是解析。但是,URL 类可以打开一个连接到资源的流。因此,URL 类只能作用于那些 Java 类库知道该如何处理的模式,例如 http:、https:、ftp:、本地文件系统(file:)和 JAR 文件(jar:)。
URI 规范给出了标记这些标识符的规则。一个 URI 具有以下句法:
EBNF
uri = [scheme, ":"], scheme specific, ["#", fragment];包含 scheme 部分的 URI 称为绝对 URI。否则,称为相对 URI。
如果绝对 URI 的 scheme specific 部分不是以 / 开头的,我们就称它是不透明的。例如:
Text
mailto:cay@horstmann.com所有绝对的透明 URI 和所有相对 URI 都是分层的(hierarchical)。例如:
Text
http://horstmann.com/index.html
../../java/net/Socket.html#Socket()1
2
2
一个分层 URI 的 scheme specific 具有以下结构:
EBNF
hierarchical scheme specific = ["//", authority], [path], ["?", query];对于那些基于服务器的 URI,authority 部分具有以下形式:
EBNF
server based authority = [user info, "@"], host, [":", port];
port = [digit - "0"], digit, { digit };
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";1
2
3
2
3
RFC 2396(标准化 URI 的文献)还支持一种基于注册表的机制,此时 authority 采用了一种不同的格式。不过,这种情况并不常见。
URI 类的作用之一是解析标识符并将它分解成各种不同的组成部分。你可以用以下方法读取它们:
Text
getScheme
getSchemeSpecificPart
getAuthority
getUserInfo
getHost
getPort
getPath
getQuery
getFragment1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
URI 类的另一个作用是处理绝对标识符和相对标识符。如果存在一个如下的绝对 URI:
Text
http://docs.mycompany.com/api/java/net/ServerSocket.html和一个如下的相对 URI:
Text
../../java/net/Socket.html#Socket()那么可以用它们组合出一个绝对 URI:
Text
http://docs.mycompany.com/api/java/net/Socket.html#Socket()这个过程称为解析相对 URL。
与此相反的过程称为相对化(relativization)。例如,假设有一个基本 URI:
Text
http://docs.mycompany.com/api和另一个 URI:
Text
http://docs.mycompany.com/api/java/lang/String.html那么相对化之后的 URI 就是:
Java
java/lang/String.htmlURI 类同时支持以下两个操作:
Java
relative = base.relativize(combined);
combined = base.resolve(relative);1
2
2
3.2. 使用 URLConnection 获取信息
如果想从某个 Web 资源获取更多信息,那么应该使用 URLConnection 类:
调用
URL类中的openConnection方法获得URLConnection对象:JavaURLConnection connection = url.openConnection();使用以下方法来设置任意的请求属性:
TextsetDoInput setDoOutput setIfModifiedSince setUseCaches setAllowUserInteraction setRequestProperty setConnectTimeout setReadTimeout1
2
3
4
5
6
7
8调用
connect方法连接远程资源:Javaconnection.connect();除了与服务器建立套接字连接外,该方法还可用于向服务器查询头信息(header information)。
与服务器建立连接后,你可以查询头信息。
getHeaderFieldKey和getHeaderField这两个方法枚举了消息头的所有字段。getHeaderFields方法返回一个包含了消息头中所有字段的标准Map对象。为了方便使用,以下方法可以查询各标准字段:JavagetContentType getContentLength getContentEncoding getDate getExpiration getLastModified1
2
3
4
5
6最后,访问资源数据。使用
getInputStream方法获取一个输入流用以读取信息(这个输入流与URL类中的openStream方法所返回的流相同)。另一个方法getContent在实际操作中并不是很有用。由标准内容类型(比如text/plain和image/gif)所返回的对象需要使用com.sun层次结构中的类来进行处理(也可以注册自己的内容处理器);
Warning:一些程序员在使用
URLConnection类的过程中形成了错误的观念,他们认为URLConnection类中的getInputStream和getOutputStream方法与Socket类中的这些方法相似,但是这种想法并不十分正确。URLConnection类具有很多表象之下的神奇功能,尤其在处理请求和响应消息头时。正因为如此,严格遵循建立连接的每个步骤显得非常重要。
下面将详细介绍一下 URLConnection 类中的一些方法。有几个方法可以在与服务器建立连接之前设置连接属性,其中最重要的是 setDoInput 和 setDoOutput。在默认情况下,建立的连接只产生从服务器读取信息的输入流,并不产生任何执行写操作的输出流。如果想获得输出流(例如,用于向一个 Web 服务器提交数据),那么需要调用:
Java
connection.setDoOutput(true);接下来,也许想设置某些请求头(request headers)。请求头是与请求命令一起被发送到服务器的。例如:
Text
GET www.server.com/index.html HTTP/1.0
Referer: http://www.somewhere.com/links.html
Proxy-Connection: Keep-Alive
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4)
Host: www.server.com
Accept: text/html, image/gif, image/jpeg, image/png, */*
Accept-Language: en
Accept-Charset: iso-8859-1,*,utf-8
Cookie: orangemilano=1922188878219871
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
setIfModifiedSince 方法用于告诉连接你只对自某个特定日期以来被修改过的数据感兴趣。
最后我们再介绍一个总览全局的方法:setRequestProperty, 它可以用来设置对特定协议起作用的任何 “名 - 值(name/value)对”。关于 HTTP 请求头的格式,请参见 RFC 2616。
要枚举所有响应头的字段,可以调用如下方法(似乎是为了展示自己的个性,该类的实现者引入了另一种迭代协议):
Java
String key = connection.getHeaderFieldKey(n);可以获得响应头的第 n 个键,其中 n 从 1 开始!如果 n 为 0 或大于消息头的字段总数,该方法将返回 null 值。没有哪种方法可以返回字段的数量,必须反复调用 getHeaderFieldKey 方法直到返回 null 为止。同样地,调用以下方法:
Java
String value = connection.getHeaderField(n);可以得到第 n 个值。
getHeaderFields 方法可以返回一个封装了响应头字段的 Map 对象。
Java
Map<String,List<String>> headerFields = connection.getHeaderFields();Tip:可以用
connection.getHeaderField(0)或headerFields.get(null)获取响应状态行(例如 "HTTP/1.1 200 OK")。
为了简便起见,Java 提供了 6 个方法用以访问最常用的消息头类型的值,并在需要的时候将它们转换成数字类型,这些方法的详细信息请参见表 3.1。返回类型为 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 |
以下是一个简单的示例程序:
Java
package urlConnection;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
/**
* This program connects to an 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.
*
* @author Cay Horstmann
* @version 1.12 2018-03-17
*/
public class URLConnectionTest {
public static void main(String[] args) {
try {
String urlName;
if (args.length > 0) urlName = args[0];
else urlName = "http://horstmann.com";
var url = new URL(urlName);
URLConnection 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;
Base64.Encoder encoder = Base64.getEncoder();
String encoding = encoder.encodeToString(input.getBytes(StandardCharsets.UTF_8));
connection.setRequestProperty("Authorization", "Basic " + encoding);
}
connection.connect();
// print header fields
Map<String, List<String>> headers = connection.getHeaderFields();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String 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("----------");
String 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();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
3.3. 提交表单数据
在 POST 请求中,我们不会在 URL 上附着参数,而是从 URLConnection 中获得输出流,并将名/值对写入到该输出流中。我们仍旧需要对这些值进行 URL 编码,并用 & 字符将它们隔开。在提交数据给服务器端程序之前,首先需要创建一个 URLConnection 对象:
Java
var url = new URL("http://host/path");
URLConnection connection = url.openConnection();1
2
2
然后,调用 setDoOutput 方法建立一个用于输出的连接:
Java
connection.setDoOutput(true);接着,调用 getOutputStream 方法获得一个流,可以通过这个流向服务器发送数据。如果要向服务器发送文本信息,那么可以非常方便地将流包装在 PrintWriter 对象中。
Java
var out = new PrintWriter(connection.getOutputStream(), StandardCharsets.UTF_8);现在,可以向服务器发送数据了。
Java
out.print(name1 + "=" + URLEncoder.encode(value1, StandardCharsets.UTF_8) + "&");
out.print(name2 + "=" + URLEncoder.encode(value2, StandardCharsets.UTF_8));1
2
2
之后,关闭输出流:
Java
out.close();最后,调用 getInputStream 方法读取服务器的响应。
以下程序用于将 POST 数据发送给任何脚本,它将数据放在如下的 .properties 文件:
Properties
url=https://tools.usps.com/tools/app/ziplookup/zipByAddress
User-Agent=HTTPie/0.9.2
address1=1 Market Street
address2=
city=San Francisco
state=CA
companyName=
...1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在从写出请求切换到读取响应的任何部分时,就会发生与服务器的实际交互。Content-Length 头被设置为输出的尺寸,而 Content-Type 头被设置为 application/x-www-formurlencoded,除非指定了不同的内容类型。这些头信息和数据都被发送给服务器,然后,响应头和服务器响应会被读取,并可以被查询。在我们的示例程序中,这种切换发生在对 connection.getContentEncoding() 的调用中。
在读取响应过程中会碰到一个问题。如果服务器端出现错误,那么调用 connection.getInputStream() 时就会抛出一个 FileNotFoundException 异常。但是,此时服务器仍然会向浏览器返回一个错误页面(例如,常见的 “错误 404 —— 找不到该页”)。为了捕捉这个错误页,可以调用 getErrorStream 方法:
Java
InputStream err = connection.getErrorStream();Note 1:
getErrorStream方法与这个程序中的许多其他方法一样,属于URLConnection类的子类HttpURLConnection。如果要创建以http://或https://开头的 URL,那么可以将所产生的连接对象强制转型为HttpURLConnection。
Note 2
如果 cookies 需要在重定向中从一个站点发送给另一个站点,那么可以像下面这样配置一个全局的 cookie 处理器:
JavaCookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));然后,cookie 就可以被正确地包含在重定向请求中了。
Java
package post;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
/**
* This program demonstrates how to use the URLConnection class for a POST request.
*
* @author Cay Horstmann
* @version 1.42 2018-03-17
*/
public class PostTest {
public static void main(String[] args) throws IOException {
String propsFilename = args.length > 0 ? args[0] : "post/post.properties";
var props = new Properties();
try (InputStream in = Files.newInputStream(Paths.get(propsFilename))) {
props.load(in);
}
String urlString = props.remove("url").toString();
Object userAgent = props.remove("User-Agent");
Object redirects = props.remove("redirects");
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
String 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)
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('&');
String name = pair.getKey().toString();
String value = pair.getValue().toString();
out.print(name);
out.print('=');
out.print(URLEncoder.encode(value, StandardCharsets.UTF_8));
}
}
String encoding = connection.getContentEncoding();
if (encoding == null) encoding = "UTF-8";
if (redirects > 0) {
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
String location = connection.getHeaderField("Location");
if (location != null) {
URL 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) {
InputStream err = connection.getErrorStream();
if (err == null) throw e;
try (var in = new Scanner(err)) {
response.append(in.nextLine());
response.append("\n");
}
}
return response.toString();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
4. HTTP 客户端
URLConnection 类是在 HTTP 成为 Web 普适协议之前设计的,它提供了对大量协议的支持,但是它对 HTTP 的支持有些笨重。当做出决定要支持 HTTP/2 时,情况就很清楚了,它最好是提供一个新的客户端接口,而不是对现有 API 做重构。HttpClient 提供了更便捷的 API 和对 HTTP/2 的支持。在 Java 9 和 10 中,其 API 类位于 jdk.incubator.http 包中,使该 API 有机会成为根据用户反馈不断演化的产物。到了 Java 11,HttpClient 位于 java.net.http 包中。
Note:在使用 Java 9 和 10 时,需要用下面的命令行选项来运行程序:
Text--add-modules jdk.incubator.httpclient
可以通过下面的调用获取客户端:
Java
HttpClient client = HttpClient.newHttpClient()或者,如果需要配置客户端,可以使用像下面这样的构建器 API:
Java
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();1
2
3
2
3
还可以遵循构建器模式来定制请求,下面是一个 Get 请求:
Java
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://horstmann.com"))
.GET()
.build();1
2
3
4
2
3
4
对于 POST 请求,需要一个 “体发布器”(body publisher),它会将请求数据转换为要推送的数据。有针对字符串、字节数组和文件的体发布器。例如,如果请求是 JSON 格式的,那么只需将 JSON 字符串提供给某个字符串体发布器:
Java
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonString))
.build();1
2
3
4
5
2
3
4
5
在发送请求时,必须告诉客户端如何处理响应。如果只是想将体当作字符串处理,那么就可以像下面这样用 HttpResponse.BodyHandlers.ofString() 来发送请求:
Java
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());HttpResponse 类是一个泛化类,它的类型参数表示体的类型。可以直接获取响应体字符串:
Java
String bodyString = response.body();还有其他的响应体处理器,可以将响应作为字节数组或输入流来获取。BodyHandlers.ofFile(filePath) 会产生一个处理器,将响应存储到给定的文件中,BodyHandlers.ofFileDownload(directoryPath) 会用 Content-Disposition 头中的信息将响应存入给定的目录中。最后,从 BodyHandlers.discarding() 中获得的处理器会直接丢弃响应。
HttpResponse 对象还提供了状态码和响应头信息。
Java
int status = response.statusCode();
HttpHeaders responseHeaders = response.headers();1
2
2
可以将 HttpHeaders 对象转换为一个映射表:
Java
Map<String, List<String>> headerMap = responseHeaders.map();如果知道某个键不会有多个值,那么还可以使用 firstValue 方法:
Java
Optional<String> lastModified = responseHeaders.firstValue("Last-Modified");可以异步地处理响应。在构建客户端时,可以提供一个执行器:
Java
ExecutorService executor = Executors.newCachedThreadPool();
HttpClient client = HttpClient.newBuilder().executor(executor).build();1
2
2
构建一个请求,然后在该客户端上调用 sendAsync 方法,就会收到一个 CompletableFuture<HttpResponse<T>> 对象,其中 T 是体处理器的类型。
Note 1:关于
CompletableFutureAPI 的介绍可以参考这里。
Note 2
为了启用针对
HttpClient记录日志的功能,需要在 JDK 的net.properties文件中添加下面的行:Propertiesjdk.httpclient.HttpClient.log=all除了
all,还可以指定为一个由逗号分隔的列表,其包含headers、requests、content、errors、ssl、trace和frames,后面还可以选择跟着::control、:data、:window或:all中间不要使用任何空格。然后,将名为
jdk.httpclient.HttpClient的日志记录器的日志级别设置为 INFO,例如,在 JDK 的logging.properties文件中添加下面的行:Propertiesjdk.httpclient.HttpClient.level=INFO
Java
package client;
import java.io.*;
import java.math.*;
import java.net.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import java.net.http.*;
import java.net.http.HttpRequest.*;
class MoreBodyPublishers {
public static BodyPublisherofFormData(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[]>();
byte[] 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();
String 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 Map<Character, String> replacements = Map.of(’\b’, "\\b", ‘\f’, "\\f",
‘\n’, "\\n", ‘\r’, "\\r", ‘\t’, "\\t", ‘"’, "\\\"", ‘\\’, "\\\\");
private static StringBuilder jsonEscape(String str) {
var result = new StringBuilder("\"");
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
String replacement = replacements.get(ch);
if (replacement == null) result.append(ch);
else result.append(replacement);
}
result.append("\"");
return result;
}
}
public class HttpClientTest {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException {
System.setProperty("jdk.httpclient.HttpClient.log", "headers,errors");
String propsFilename = args.length > 0 ? args[0] : "client/post.properties";
Path propsPath = Paths.get(propsFilename);
var props = new Properties();
try (InputStream in = Files.newInputStream(propsPath)) {
props.load(in);
}
String urlString = "" + props.remove("url");
String contentType = "" + props.remove("Content-Type");
if (contentType.equals("multipart/formdata")) {
var generator = new Random();
String 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
);
}
String 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 {
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
BodyPublisher publisher = null;
if (contentType.startsWith("multipart/formdata")) {
String boundary = contentType.substring(contentType.lastIndexOf("=") + 1);
publisher = MoreBodyPublishers.ofMimeMultipartData(data, boundary);
} else if (contentType.equals("application/xwww-form-urlencoded")) {
publisher = MoreBodyPublishers.ofFormData(data);
} else {
contentType = "application/json";
publisher = MoreBodyPublishers.ofSimpleJSON(data);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("Content-Type", contentType)
.POST(publisher)
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
5. 发送 E-mail
过去,编写程序通过创建到邮件服务器上 SMTP 专用的端口 25 来发送邮件是一件很简单的事。简单邮件传输协议用于描述 E-mail 消息的格式。一旦连接到服务器,就可以发送一个邮件报头(采用 SMTP 格式,该格式很容易生成)。紧随其后的是邮件消息。
以下是操作的详细过程:
打开一个到达主机的套接字:
Javavar s = new Socket("mail.yourserver.com", 25); // 25 is SMTP var out = new PrintWriter(s.getOutputStream(), StandardCharsets.UTF_8);1
2发送以下信息到打印流:
TextHELO sending host MAIL FROM: sender e-mail address RCPT TO: recipient e-mail address DATA Subject: subject (blank line) mail message (any number of lines) . QUIT1
2
3
4
5
6
7
8
9
SMTP 规范(RFC 821)规定,每一行都要以 \r 再紧跟一个 \n 来结尾。
SMTP 曾经总是例行公事般地路由任何人的 E-mail,但是,在蠕虫泛滥的今天,许多服务器都内置了检查功能,并且只接受来自授信用户或授信 IP 地址范围的请求。其中,认证通常是通过安全套接字连接来实现的。
实现人工认证模式的代码非常冗长乏味,因此,我们将展示如何利用 JavaMail API 在 Java 程序中发送 E-mail。
可以从 http://www.oracle.com/technetwork/java/javamail 处下载 JavaMail,然后将它解压到硬盘上的某处。
如果要使用 JavaMail,则需要设置一些和邮件服务器相关的属性。例如,在使用 GMail 时,需要设置:
Properties
mail.transport.protocol=smtps
mail.smtps.auth=true
mail.smtps.host=smtp.gmail.com
mail.smtps.user=accountname@gmail.com1
2
3
4
2
3
4
我们的示例程序是从一个属性文件中读取这些属性值的。
出于安全的原因,我们没有将密码放在属性文件中,而是要求提示用户需要输入。
首先要读入属性文件,然后像下面这样获取一个邮件会话:
Java
Session mailSession = Session.getDefaultInstance(props);接着,用恰当的发送者、接受者,主题和消息文本来创建消息:
Java
var message = new MimeMessage(mailSession);
message.setFrom(new InternetAddress(from));
message.addRecipient(RecipientType.TO, new InternetAddress(to));
message.setSubject(subject);
message.setText(builder.toString());1
2
3
4
5
2
3
4
5
然后将消息发送走:
Java
Transport tr = mailSession.getTransport();
tr.connect(null, password);
tr.sendMessage(message, message.getAllRecipients());
tr.close();1
2
3
4
2
3
4
以下程序是从具有下面这种格式的文本文件中读取消息的:
Text
Sender
Recipient
Subject
Message text (any number of lines)1
2
3
4
2
3
4
要运行该程序,需要从 https://javaee.github.io/javamail 下载 JavaMail 的实现,还需要 Java 激活框架(Java Activation Framework)的 JAR 文件,可以从 http://www.oracle.com/technetwork/java/javase/jaf-135115.html 处获得,或者可以在 Maven Central 中搜索。然后运行:
Bash
$ java -classpath .:javax.mail.jar:activation-1.1.1.jar path/to/message.txtJava
package mail;
import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.internet.MimeMessage.RecipientType;
/**
* This program shows how to use JavaMail to send mail messages.
*
* @author Cay Horstmann
* @version 1.01 2018-03-17
*/
public class MailTest {
public static void main(String[] args) throws MessagingException, IOException {
var props = new Properties();
try (InputStream in = Files.newInputStream(Paths.get("mail", "mail.properties"))) {
props.load(in);
}
List<String> lines = Files.readAllLines(Paths.get(args[0]), StandardCharsets.UTF_8);
String from = lines.get(0);
String to = lines.get(1);
String subject = lines.get(2);
var builder = new StringBuilder();
for (int i = 3; i < lines.size(); i++) {
builder.append(lines.get(i));
builder.append("\n");
}
Console console = System.console();
var password = new String(console.readPassword("Password: "));
Session mailSession = Session.getDefaultInstance(props);
// mailSession.setDebug(true);
var message = new MimeMessage(mailSession);
message.setFrom(new InternetAddress(from));
message.addRecipient(RecipientType.TO, new InternetAddress(to));
message.setSubject(subject);
message.setText(builder.toString());
Transport tr = mailSession.getTransport();
try {
tr.connect(null, password);
tr.sendMessage(message, message.getAllRecipients());
} finally {
tr.close();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Note
如果你搞不清楚为什么你的邮件连接无法正常工作,那么可以调用:
JavamailSession.setDebug(true);并检查消息。而且,JavaMail API FAQ 也有些挺有用的调试提示。