Java 套接字教程展示了如何使用套接字在 Java 中进行网络编程。 套接字编程是低级的。 本教程的目的是介绍包括这些低级详细信息的网络编程。 有些高级 API 可能更适合实际任务。 例如,Java 11 引入了 HttpClient,而 Spring 具有 Webclient。
Java 套接字
在编程中,套接字是网络上运行的两个程序之间的通信端点。 套接字类用于在客户端程序和服务器程序之间创建连接。 Socket
代表客户端套接字,ServerSocket
代表服务器套接字。
Note: In networking, the term socket has a different meaning. It is used for the combination of an IP address and a port number.
ServerSocket
绑定到端口号,这是通过客户端和服务器同意进行通信的唯一 ID。
Socket
和ServerSocket
用于 TCP 协议。 DatagramSocket
和DatagramPacket
用于 UDP 协议。
TCP 更可靠,具有大量错误检查并需要更多资源。 HTTP,SMTP 或 FTP 等服务使用它。 UDP 的可靠性要差得多,错误检查的能力也有限,所需资源也更少。 VoIP 等服务使用它。
DatagramSocket
是用于发送和接收数据报包的套接字。 数据报包由DatagramPacket
类表示。 在数据报套接字上发送或接收的每个数据包都经过单独寻址和路由。 从一台机器发送到另一台机器的多个数据包可能会以不同的方式路由,并且可能以任何顺序到达。
Java 套接字时间客户端
是提供当前时间的服务器。 客户端无需任何命令即可直接连接到服务器,服务器以当前时间作为响应。
Note: Time servers come and go, so we might need to find a working server on https://www.ntppool.org/en/.
在我们的示例中,我们选择了瑞典的服务器。
com/zetcode/SocketTimeClient.java
package com.zetcode;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
// time servers come and go; we might need to
// find a functioning server on https://www.ntppool.org/en/
public class SocketTimeClient {
public static void main(String[] args) throws IOException {
var hostname = "3.se.pool.ntp.org";
int port = 13;
try (var socket = new Socket(hostname, port)) {
try (var reader = new InputStreamReader(socket.getInputStream())) {
int character;
var output = new StringBuilder();
while ((character = reader.read()) != -1) {
output.append((char) character);
}
System.out.println(output);
}
}
}
}
该示例连接到时间服务器并接收当前时间。
var hostname = "3.se.pool.ntp.org";
int port = 13;
这是瑞典的时间服务器; 13 端口是白天服务的标准端口。
try (var socket = new Socket(hostname, port)) {
流客户端套接字已创建。 它连接到命名主机上的指定端口号。 使用 Java 的 try-with-resources 语句自动关闭套接字。
try (var reader = new InputStreamReader(socket.getInputStream())) {
getInputStream()
返回此套接字的输入流。 我们从此输入流中读取服务器的响应。 套接字之间的通信以字节为单位; 因此,我们将InputStreamReader
用作字节和字符之间的桥梁。
int character;
var output = new StringBuilder();
while ((character = reader.read()) != -1) {
output.append((char) character);
}
System.out.println(output);
由于响应消息很小,因此我们可以逐个字符地读取它,而对性能的影响很小。
Java 套接字 Whois 客户端
Whois 是基于 TCP 的面向事务的查询/响应协议,被广泛用于向 Internet 用户提供信息服务。 它用于查询域名或 IP 地址块所有者等信息。
Note: Most whois servers provide only limited information (for instance only for selected domain names) and the information about owners is often anonymized by domain registrars.
Whois 协议使用端口 43。
com/zetcode/WhoisClientEx.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
// probing whois.iana.org might give the right
// whois server
public class WhoisClientEx {
public static void main(String[] args) throws IOException {
var domainName = "example.com";
var whoisServer = "whois.nic.me";
int port = 43;
try (var socket = new Socket(whoisServer, port)) {
try (var writer = new PrintWriter(socket.getOutputStream(), true)) {
writer.println(domainName);
try (var reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
}
}
在该示例中,我们探查了有关域名所有者的信息。
try (var writer = new PrintWriter(socket.getOutputStream(), true)) {
writer.println(domainName);
...
我们获得套接字的输出流,并将其包装到PrintWriter
中。 PrintWriter
会将我们的字符转换为字节。 使用println()
,将域名写入流中。 通过套接字的通信被缓冲。 PrintWriter
的第二个参数是autoFlush
; 如果设置为true
,则在每个println()
之后将刷新缓冲区。
try (var reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
来自服务器的响应被读取并写入控制台。
Java 套接字 GET 请求
在下面的示例中,我们创建一个 GET 请求。 HTTP GET 请求用于检索特定资源。
com/zetcode/JavaSocketGetRequest.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class SocketGetRequest {
public static void main(String[] args) throws IOException {
try (var socket = new Socket("webcode.me", 80)) {
try (var wtr = new PrintWriter(socket.getOutputStream())) {
// create GET request
wtr.print("GET / HTTP/1.1\r\n");
wtr.print("Host: www.webcode.me\r\n");
wtr.print("\r\n");
wtr.flush();
socket.shutdownOutput();
String outStr;
try (var bufRead = new BufferedReader(new InputStreamReader(
socket.getInputStream()))) {
while ((outStr = bufRead.readLine()) != null) {
System.out.println(outStr);
}
socket.shutdownInput();
}
}
}
}
}
该示例从网站检索 HTML 页面。
try (var socket = new Socket("webcode.me", 80)) {
我们在端口 80 上的指定网页上打开一个套接字。HTTP 协议使用端口 80。
try (var wtr = new PrintWriter(socket.getOutputStream())) {
我们将在协议上发布文本命令; 因此,我们为套接字输出流创建一个PrintWriter
。 由于我们没有将autoFlush
选项设置为true
,因此我们需要手动刷新缓冲区。
// create GET request
wtr.print("GET / HTTP/1.1\r\n");
wtr.print("Host: www.webcode.me\r\n");
wtr.print("\r\n");
wtr.flush();
我们创建一个 HTTP GET 请求,该请求检索指定网页的主页。 请注意,文本命令以\r\n
(CRLF)字符完成。 这些是必需的通信详细信息,在 RFC 2616 文档中进行了描述。
socket.shutdownOutput();
shutdownOutput
禁用此套接字的输出流。 最后必须关闭连接。
try (var bufRead = new BufferedReader(new InputStreamReader(
socket.getInputStream()))) {
对于服务器响应,我们打开一个套接字输入流,并使用InputStreamReader
将字节转换为字符。 我们还缓冲读取操作。
while ((outStr = bufRead.readLine()) != null) {
System.out.println(outStr);
}
我们逐行读取数据。
socket.shutdownInput();
最后,我们也关闭了输入流。
Java 套接字 HEAD 请求
在下一个示例中,我们使用 Java 套接字创建 HEAD 请求。 HEAD 方法与 GET 方法相同,不同之处在于服务器在响应中不返回消息正文。 它仅返回标头。
com/zetcode/SocketHeadRequest.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class SocketHeadRequest {
public static void main(String[] args) throws IOException {
var hostname = "webcode.me";
int port = 80;
try (var socket = new Socket(hostname, port)) {
try (var writer = new PrintWriter(socket.getOutputStream(), true)) {
writer.println("HEAD / HTTP/1.1");
writer.println("Host: " + hostname);
writer.println("User-Agent: Console Http Client");
writer.println("Accept: text/html");
writer.println("Accept-Language: en-US");
writer.println("Connection: close");
writer.println();
try (var reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
}
}
该示例检索指定网页的标题。
writer.println("HEAD / HTTP/1.1");
我们发出 HEAD 命令。
writer.println("Connection: close");
在 HTTP 协议版本 1.1 中,除非另有声明,否则所有连接均被视为持久连接(保持活动状态)。 通过将选项设置为false
,我们通知我们要在请求/响应周期之后完成连接。
Java ServerSocket
日期服务器
下面的示例使用ServerSocket
创建一个非常简单的服务器。 ServerSocket
创建绑定到指定端口的服务器套接字。
com/zetcode/DateServer.java
package com.zetcode;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.time.LocalDate;
public class DateServer {
public static void main(String[] args) throws IOException {
int port = 8081;
try (var listener = new ServerSocket(port)) {
System.out.printf("The started on port %d%n", port);
while (true) {
try (var socket = listener.accept()) {
try (var pw = new PrintWriter(socket.getOutputStream(), true)) {
pw.println(LocalDate.now());
}
}
}
}
}
}
该示例创建一个返回当前日期的服务器。 最后必须手动终止该程序。
int port = 8081;
try (var listener = new ServerSocket(port)) {
在端口 8081 上创建一个服务器套接字。
try (var socket = listener.accept()) {
accept()
方法侦听与此套接字建立的连接并接受它。 该方法将阻塞,直到建立连接为止。
try (var pw = new PrintWriter(socket.getOutputStream(), true)) {
pw.println(LocalDate.now());
}
我们将当前日期写入套接字输出流。
get_request.py
#!/usr/bin/env python3
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("localhost" , 8081))
s.sendall(b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: text/html\r\n\r\n")
print(str(s.recv(4096), 'utf-8'))
我们有一个 Python 脚本向服务器发出 GET 请求。
$ get_request.py
2019-07-15
这是输出。
Java 套接字客户端/服务器示例
在下面的示例中,我们有一个服务器和一个客户端。 服务器反转从客户端发送的文本。 这个例子很简单而且很阻塞。 为了改善它,我们需要包括线程。
com/zetcode/ReverseServer.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
// This server communicates only with one client at a time.
// It must disconnect from a client first to communicate
// with another client. It receives a bye command from a client
// to close a connection.
public class ReverseServer {
public static void main(String[] args) throws IOException {
int port = 8081;
try (var serverSocket = new ServerSocket(port)) {
System.out.println("Server is listening on port " + port);
while (true) {
try (var socket = serverSocket.accept()) {
System.out.println("client connected");
try (var reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
var writer = new PrintWriter(socket.getOutputStream(), true)) {
String text;
do {
text = reader.readLine();
if (text != null) {
var reversed = new StringBuilder(text).reverse().toString();
writer.println("Server: " + reversed);
System.out.println(text);
}
} while (!"bye".equals(text));
System.out.println("client disconnected");
}
}
}
}
}
}
ReverseServer
将反向字符串发送回客户端。 一次仅与一个客户端通信。 它必须首先与客户端断开连接才能与另一个客户端通信。 它从客户端收到一个 bye 命令以关闭连接。
try (var reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
var writer = new PrintWriter(socket.getOutputStream(), true)) {
我们有一个套接字输入流,用于读取客户端数据,以及套接字输出流,用于将响应发送回客户端; 输出流和连接已关闭。
do {
text = reader.readLine();
if (text != null) {
var reversed = new StringBuilder(text).reverse().toString();
writer.println("Server: " + reversed);
System.out.println(text);
}
} while (!"bye".equals(text));
为单个客户端创建了一个do-while
循环。 我们从客户端读取数据,然后将修改后的内容发送回去。 收到客户端的再见命令后,循环结束。 在此之前,没有其他客户端可以连接到服务器。
com/zetcode/ReverseClient.java
package com.zetcode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
// the client must send a bye command to
// inform the server to close the connection
public class ReverseClient {
public static void main(String[] args) throws IOException {
var hostname = "localhost";
int port = 8081;
try (var socket = new Socket(hostname, port)) {
try (var writer = new PrintWriter(socket.getOutputStream(), true)) {
try (var scanner = new Scanner(System.in)) {
try (var reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()))) {
String command;
do {
System.out.print("Enter command: ");
command = scanner.nextLine();
writer.println(command);
var data = reader.readLine();
System.out.println(data);
} while (!command.equals("bye"));
}
}
}
}
}
}
客户端将文本数据发送到服务器。
do {
System.out.print("Enter command: ");
command = scanner.nextLine();
writer.println(command);
var data = reader.readLine();
System.out.println(data);
} while (!command.equals("bye"));
我们从控制台读取输入,并将其发送到服务器。 当我们发送 bye 命令时,while 循环完成,该命令通知服务器可以关闭连接。
Java DatagramSocket
示例
UDP 是一种通信协议,它通过网络传输独立的数据包,不保证到达且也不保证传递顺序。 使用 UDP 的一项服务是每日报价(QOTD)。
下面的示例创建一个连接到 QOTD 服务的客户端程序。
com/zetcode/DatagramSocketEx.java
package com.zetcode;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
// DatagramSocket provides network communication via UDP protocol
// The Quote of the Day (QOTD) service is a member of the Internet protocol
// suite, defined in RFC 865
public class DatagramSocketEx {
public static void main(String[] args) throws IOException {
var hostname = "djxmmx.net";
int port = 17;
var address = InetAddress.getByName(hostname);
try (var socket = new DatagramSocket()) {
var request = new DatagramPacket(new byte[1], 1, address, port);
socket.send(request);
var buffer = new byte[512];
var response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
var quote = new String(buffer, 0, response.getLength());
System.out.println(quote);
}
}
}
该示例从报价服务检索报价并将其打印到终端。
var address = InetAddress.getByName(hostname);
我们从主机名获得 IP 地址。
try (var socket = new DatagramSocket()) {
创建了DatagramSocket
。
var request = new DatagramPacket(new byte[1], 1, address, port);
创建了DatagramPacket
。 由于 QOTD 服务不需要来自客户端的数据,因此我们发送了一个空的小型数组。 每次发送数据包时,我们都需要指定数据,地址和端口。
socket.send(request);
数据包通过send()
发送到其目的地。
var buffer = new byte[512];
var response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
我们收到来自服务的数据包。
var quote = new String(buffer, 0, response.getLength());
System.out.println(quote);
我们将接收到的字节转换为字符串并打印。
在本教程中,我们创建了带有套接字的网络 Java 程序。