Java Socket 教程展示了如何在 Java 中使用套接字进行网络编程。套接字编程是低级的。本教程的目的是介绍网络编程,包括这些底层细节。有更高级的 API 可能更适合实际任务。例如,Java 11 引入了 HttpClient,Spring 引入了 Webclient。
在编程中,插件座 是网络上运行的两个程序之间通信的端点。套接字类用于在客户端程序和服务器程序之间创建连接。 Socket
表示客户端套接字,ServerSocket
表示服务器套接字。
一个ServerSocket
绑定了一个端口号,它是一个唯一的Id,通过客户端和服务器的约定来进行通信。
Socket
和 ServerSocket
用于 TCP 协议。 DatagramSocket
和 DatagramPacket
用于 UDP 协议。
TCP 更可靠,具有广泛的错误检查,并且需要更多资源。它由 HTTP、SMTP 或 FTP 等服务使用。 UDP 的可靠性要差得多,错误检查有限,并且需要的资源更少。它由 VoIP 等服务使用。
DatagramSocket
是用于发送和接收数据报包的套接字。数据报包由 DatagramPacket
类表示。在数据报套接字上发送或接收的每个数据包都被单独寻址和路由。从一台机器发送到另一台机器的多个数据包可能会有不同的路由,并且可能以任何顺序到达。
这些是提供当前时间的服务器。客户端无需命令即可简单地连接到服务器,服务器会以当前时间作为响应。
在我们的示例中,我们选择了瑞典的服务器。
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);
由于响应消息很小,我们可以逐个字符地读取它,而性能损失很小。
Whois 是一种基于 TCP 的面向事务的查询/响应协议,广泛用于向 Internet 用户提供信息服务。用于查询域名或IP地址块所有者等信息。
Whois 协议使用端口 43。
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); } }
来自服务器的响应被读取并写入控制台。
在以下示例中,我们创建一个 GET 请求。 HTTP GET 请求用于检索特定资源。
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端口上打开一个socket。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 请求。 HEAD 方法与 GET 方法相同,只是服务器不在响应中返回消息体;它只返回标题。
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 中,除非另有声明,否则所有连接都被认为是持久的(keep-alive)。通过将选项设置为 false
,我们通知我们希望在请求/响应周期后完成连接。
以下示例使用 ServerSocket
创建了一个非常简单的服务器。 ServerSocket
创建一个服务器套接字,绑定到指定的端口。
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()); }
我们将当前日期写入套接字输出流。
#!/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
在下面的示例中,我们有一个服务器和一个客户端。服务器反转从客户端发送的文本。该示例很简单且阻塞。为了改进它,我们需要包括线程。
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
将反向字符串发送回客户端。它一次只与一个客户端通信。它必须首先断开与客户端的连接才能与另一个客户端通信。它从客户端接收到关闭连接的再见命令。
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
循环是为单个客户端创建的。我们从客户端读取数据并将修改后的内容发回。循环在收到来自客户端的 bye 命令后结束。在此之前,没有其他客户端可以连接到服务器。
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 循环结束,它通知服务器可以关闭连接。
UDP 是一种通过网络传输独立数据包的通信协议,不保证到达且不保证传递顺序。使用 UDP 的一项服务是每日报价 (QOTD)。
以下示例创建一个连接到 QOTD 服务的客户端程序。
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 程序。
List 所有 Java 教程.
地址:https://www.cundage.com/article/java-socket.html