Java 中文官方教程 2022 版(四十五)(2)https://developer.aliyun.com/article/1488409
运行程序
你必须先启动服务器程序。为此,请使用 Java 解释器运行服务器程序,就像运行任何其他 Java 应用程序一样。指定服务器程序监听的端口号作为命令行参数:
java KnockKnockServer 4444
接下来,运行客户端程序。请注意,你可以在网络上的任何计算机上运行客户端;它不必在与服务器相同的计算机上运行。指定运行KnockKnockServer
服务器程序的计算机的主机名和端口号作为命令行参数:
java KnockKnockClient knockknockserver.example.com 4444
如果你太快了,可能会在服务器有机会初始化并开始监听端口之前启动客户端。如果发生这种情况,你将会看到客户端的堆栈跟踪。如果发生这种情况,只需重新启动客户端。
如果在第一个客户端连接到服务器时尝试启动第二个客户端,第二个客户端将会挂起。下一节,支持多个客户端,讨论了支持多个客户端的问题。
当客户端和服务器成功建立连接时,你将在屏幕上看到以下文本显示:
Server: Knock! Knock!
现在,你必须回复:
Who's there?
客户端会回显你输入的内容并将文本发送到服务器。服务器会回复其中一个众多“敲敲”笑话的第一行。现在你的屏幕应该显示如下内容(你输入的文本用粗体表示):
Server: Knock! Knock! Who's there? Client: Who's there? Server: Turnip
现在,你回复:
Turnip who?
再次,客户端会回显你输入的内容并将文本发送到服务器。服务器会回复一个笑话的结尾。现在你的屏幕应该显示如下内容:
Server: Knock! Knock! Who's there? Client: Who's there? Server: Turnip Turnip who? Client: Turnip who? Server: Turnip the heat, it's cold in here! Want another? (y/n)
如果你想听另一个笑话,输入y;如果不想,输入n。如果输入y,服务器会重新开始“敲敲”;如果输入n,服务器会说“再见”,导致客户端和服务器都退出。
如果在任何时候你打错字,KnockKnockServer
对象会捕捉到并回复类似于这样的消息:
Server: You're supposed to say "Who's there?"!
然后服务器会重新开始讲笑话:
Server: Try again. Knock! Knock!
注意,KnockKnockProtocol
对象对拼写和标点符号很在意,但对大小写不在意。
支持多个客户端
为了保持KnockKnockServer
示例简单,我们设计它来监听并处理单个连接请求。然而,多个客户端请求可以进入同一端口,因此也进入同一个ServerSocket
。客户端连接请求在端口处排队,因此服务器必须按顺序接受连接。然而,服务器可以通过使用线程同时为它们提供服务——每个客户端连接一个线程。
这样一个服务器的基本逻辑流程是这样的:
while (true) { accept a connection; create a thread to deal with the client; }
线程根据需要从客户端连接读取和写入。
试一试:
修改KnockKnockServer
以便能够同时为多个客户端提供服务。我们的解决方案由两个类组成:KKMultiServer
和 KKMultiServerThread
。KKMultiServer
永远循环,监听ServerSocket
上的客户端连接请求。当有请求进来时,KKMultiServer
接受连接,创建一个新的KKMultiServerThread
对象来处理它,将从 accept 返回的套接字传递给它,并启动线程。然后服务器继续监听连接请求。KKMultiServerThread
对象通过读取和写入套接字与客户端通信。运行新的“敲敲”服务器KKMultiServer
,然后依次运行几个客户端。
教训:关于数据报的一切
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/index.html
您编写的一些用于在网络上通信的应用程序不需要 TCP 提供的可靠的点对点通道。相反,您的应用程序可能会从一种通信模式中受益,该模式传递独立的信息包,其到达和到达顺序不受保证。
UDP 协议提供了一种网络通信模式,应用程序可以向彼此发送数据包,称为数据报。数据报是通过网络发送的独立、自包含的消息,其到达、到达时间和内容均不受保证。java.net
包中的DatagramPacket
和DatagramSocket
类使用 UDP 实现了系统独立的数据报通信。
什么是数据报?
数据报是通过网络发送的独立、自包含的消息,其到达、到达时间和内容均不受保证。
编写数据报客户端和服务器
本节将通过一个示例引导您,其中包含两个使用数据报进行通信的 Java 程序。服务器端是一个引用服务器,它监听其DatagramSocket
并在客户端请求时向客户端发送引用。客户端是一个简单的程序,只需向服务器发出请求。
向多个接收方广播
本节修改引用服务器,使其不再在请求时向单个客户端发送引用,而是每分钟向所有正在侦听的客户端广播一条引用。客户端程序必须相应地进行修改。
注意:
许多防火墙和路由器被配置为不允许 UDP 数据包。如果您在防火墙外部连接到服务时遇到问题,或者客户端无法连接到您的服务,请向系统管理员询问是否允许 UDP。
什么是数据报?
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/definition.html
客户端和服务器通过可靠的通道(如 TCP 套接字)进行通信,它们之间有一个专用的点对点通道,或者至少有这种幻觉。为了通信,它们建立连接,传输数据,然后关闭连接。所有通过通道发送的数据都按照发送顺序接收。这由通道保证。
相反,通过数据报通信的应用程序发送和接收完全独立的信息包。这些客户端和服务器没有也不需要专用的点对点通道。数据报的传递到目的地不被保证。它们的到达顺序也不被保证。
定义:
一个数据报是独立的、自包含的消息,通过网络发送,其到达、到达时间和内容都不被保证。
java.net
包含三个类,帮助你编写使用数据报在网络上传输和接收数据包的 Java 程序:DatagramSocket
、DatagramPacket
和MulticastSocket
。一个应用程序可以通过DatagramSocket
发送和接收DatagramPacket
。此外,DatagramPacket
可以广播到多个接收者,所有接收者都监听MulticastSocket
。
编写数据报客户端和服务器
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/clientServer.html
本节中展示的示例由两个应用程序组成:客户端和服务器。服务器持续接收数据报包,每个数据报包都表示客户端请求报价。当服务器接收到数据报时,它会通过数据报包将一行“此刻引用”发送回客户端。
本示例中的客户端应用程序非常简单。它向服务器发送一个数据报包,指示客户端希望接收此刻引用。然后客户端等待服务器发送数据报包作为响应。
两个类实现了服务器应用程序:QuoteServer
和 QuoteServerThread
。一个类实现了客户端应用程序:QuoteClient
。
让我们从包含服务器应用程序的main
方法的类开始研究这些类。使用服务器端应用程序包含QuoteClient
类的小程序版本。
引用服务器类
在这里完整显示的QuoteServer
类包含一个方法:引用服务器应用程序的main
方法。main
方法只是创建一个新的QuoteServerThread
对象并启动它:
import java.io.*; public class QuoteServer { public static void main(String[] args) throws IOException { new QuoteServerThread().start(); } }
QuoteServerThread
类实现了引用服务器的主要逻辑。
QuoteServerThread
类
创建时,QuoteServerThread
在端口 4445 上创建了一个DatagramSocket
(任意选择)。这是服务器与所有客户端通信的DatagramSocket
。
public QuoteServerThread() throws IOException { this("QuoteServer"); } public QuoteServerThread(String name) throws IOException { super(name); socket = new DatagramSocket(4445); try { in = new BufferedReader(new FileReader("one-liners.txt")); } catch (FileNotFoundException e){ System.err.println("Couldn't open quote file. Serving time instead."); } }
请记住,某些端口专用于已知服务,您不能使用它们。如果指定一个正在使用的端口,DatagramSocket
的创建将失败。
构造函数还在名为one-liners.txt
的文件上打开了一个BufferedReader
,其中包含引用列表。文件中的每个引用都在单独的一行上。
现在来看QuoteServerThread
的有趣部分:它的run
方法。run
方法覆盖了Thread
类中的run
方法,并为线程提供了实现。有关线程的信息,请参见定义和启动线程。
run
方法包含一个while
循环,只要文件中还有更多引用,循环就会继续。在循环的每次迭代中,线程会等待一个DatagramPacket
通过DatagramSocket
到达。数据包表示客户端的请求。作为对客户端请求的响应,QuoteServerThread
从文件中获取引用,将其放入DatagramPacket
中,并通过DatagramSocket
将其发送到请求的客户端。
让我们首先看一下接收客户端请求的部分:
byte[] buf = new byte[256]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet);
第一条语句创建了一个字节数组,然后用它创建了一个DatagramPacket
。DatagramPacket
将用于从套接字接收数据报,因为使用了创建它的构造函数。这个构造函数只需要两个参数:一个包含客户端特定数据的字节数组和字节数组的长度。在构造要通过DatagramSocket
发送的DatagramPacket
时,还必须提供数据包目的地的互联网地址和端口号。当我们讨论服务器如何响应客户端请求时,你会看到这一点。
前一个代码片段中的最后一条语句从套接字接收一个数据报(从客户端接收的信息被复制到数据包中)。receive
方法会一直等待,直到接收到一个数据包。如果没有接收到数据包,服务器将不会有进一步的进展,只会等待。
现在假设,服务器收到了客户端请求报价的请求。现在服务器必须做出响应。run
方法中的这部分代码构建了响应:
String dString = null; if (in == null) dString = new Date().toString(); else dString = getNextQuote(); buf = dString.getBytes();
如果由于某种原因未打开报价文件,则in
等于 null。如果是这种情况,报价服务器将提供当天的时间。否则,报价服务器从已打开的文件中获取下一个报价。最后,代码将字符串转换为字节数组。
现在,run
方法使用以下代码向客户端通过DatagramSocket
发送响应:
InetAddress address = packet.getAddress(); int port = packet.getPort(); packet = new DatagramPacket(buf, buf.length, address, port); socket.send(packet);
这段代码中的前两条语句分别从客户端接收的数据报中获取了互联网地址和端口号。互联网地址和端口号指示了数据报包的来源。这是服务器必须发送响应的地方。在这个例子中,数据报包的字节数组不包含相关信息。根据报包的到达本身就表示了来自互联网地址和端口号所指示的客户端的请求。
第三条语句创建了一个新的DatagramPacket
对象,用于通过数据报套接字发送数据报消息。你可以通过创建它的构造函数来判断新的DatagramPacket
是用于通过套接字发送数据的。这个构造函数需要四个参数。前两个参数与用于创建接收数据报的构造函数相同:包含从发送方到接收方的消息的字节数组和该数组的长度。接下来的两个参数不同:一个互联网地址和一个端口号。这两个参数是数据报包的目的地的完整地址,必须由数据报的发送方提供。最后一行代码将DatagramPacket
发送出去。
当服务器从报价文件中读取了所有报价后,while
循环终止,run
方法进行清理:
socket.close();
QuoteClient 类
QuoteClient
类实现了QuoteServer
的客户端应用程序。此应用程序向QuoteServer
发送请求,等待响应,并在接收到响应时将其显示到标准输出。让我们详细看看代码。
QuoteClient
类包含一个方法,即客户端应用程序的main
方法。main
方法的顶部声明了几个局部变量供其使用:
int port; InetAddress address; DatagramSocket socket = null; DatagramPacket packet; byte[] sendBuf = new byte[256];
首先,main
方法处理用于调用QuoteClient
应用程序的命令行参数:
if (args.length != 1) { System.out.println("Usage: java QuoteClient <hostname>"); return; }
QuoteClient
应用程序需要一个命令行参数:运行QuoteServer
的机器的名称。
接下来,main
方法创建一个DatagramSocket
:
DatagramSocket socket = new DatagramSocket();
客户端使用一个不需要端口号的构造函数。此构造函数只是将DatagramSocket
绑定到任何可用的本地端口。客户端绑定到的端口号并不重要,因为DatagramPacket
包含寻址信息。服务器从DatagramPacket
中获取端口号并将其响应发送到该端口。
接下来,QuoteClient
程序向服务器发送一个请求:
byte[] buf = new byte[256]; InetAddress address = InetAddress.getByName(args[0]); DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 4445); socket.send(packet);
代码段获取命令行上命名的主机的 Internet 地址(假设服务器运行的机器的名称)。然后使用此InetAddress
和端口号 4445(服务器用于创建其DatagramSocket
的端口号)创建一个发送到该 Internet 地址和端口号的DatagramPacket
。因此,DatagramPacket
将被传递到引用服务器。
请注意,代码创建了一个带有空字节数组的DatagramPacket
。字节数组为空,因为此数据报包仅仅是向服务器请求信息。服务器发送响应所需的所有信息—地址和端口号—自动包含在数据包中。
接下来,客户端从服务器获取响应并显示出来:
packet = new DatagramPacket(buf, buf.length); socket.receive(packet); String received = new String(packet.getData(), 0, packet.getLength()); System.out.println("Quote of the Moment: " + received);
要从服务器获取响应,客户端创建一个“接收”数据包,并使用DatagramSocket
的接收方法从服务器接收回复。接收方法会等待直到一个发送给客户端的数据包通过套接字传输过来。请注意,如果服务器的回复某种原因丢失,客户端将永远等待,因为数据报模型没有保证策略。通常,客户端会设置一个计时器,以便不会永远等待回复;如果没有收到回复,计时器会触发,客户端会重新发送。
当客户端从服务器接收到回复时,客户端使用getData
方法从数据包中检索数据。然后客户端将数据转换为字符串并显示出来。
运行服务器和客户端
在成功编译服务器和客户端程序之后,您可以运行它们。必须先运行服务器程序。只需使用 Java 解释器并指定QuoteServer
类名。
一旦服务器启动,你可以运行客户端程序。记得用一个命令行参数来运行客户端程序:QuoteServer
运行的主机名。
当客户端发送请求并从服务器接收到响应后,你应该看到类似于以下输出:
Quote of the Moment: Good programming is 99% sweat and 1% coffee.
向多个接收者广播
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/broadcasting.html
除了DatagramSocket
,它让程序之间可以发送数据包,java.net 还包括一个名为MulticastSocket
的类。这种类型的套接字用于客户端监听服务器广播给多个客户端的数据包。
让我们重写引用服务器,使其可以向多个接收者广播DatagramPacket
。而不是向请求的特定客户端发送引用,新服务器现在需要定期广播引用。客户端需要进行修改,以便被动地监听引用,并在MulticastSocket
上这样做。
这个示例由三个类组成,这三个类是前一个示例中三个类的修改版本:MulticastServer
,MulticastServerThread
,和MulticastClient
。本讨论重点介绍了这些类的有趣部分。
这是服务器主程序的新版本。这段代码与之前版本QuoteServer
的不同之处用粗体显示出来:
import java.io.*; public class MulticastServer { public static void main(String[] args) throws IOException { new MulticastServerThread().start(); } }
基本上,服务器得到了一个新的名称,并创建了一个MulticastServerThread
而不是QuoteServerThread
。现在让我们看看MulticastServerThread
,其中包含服务器的核心。这是它的类声明:
public class MulticastServerThread extends QuoteServerThread { // ... }
我们将这个类作为QuoteServerThread
的子类,以便它可以使用构造函数,并继承一些成员变量和getNextQuote
方法。回想一下,QuoteServerThread
创建了一个绑定到端口 4445 的DatagramSocket
并打开引用文件。在这个示例中,DatagramSocket
的端口号实际上并不重要,因为客户端从不向服务器发送任何内容。
MulticastServerThread
中唯一显式实现的方法是其run
方法。这个run
方法与QuoteServerThread
中的方法的不同之处用粗体显示出来:
public void run() { while (moreQuotes) { try { byte[] buf = new byte[256]; // don't wait for request...just send a quote String dString = null; if (in == null) dString = new Date().toString(); else dString = getNextQuote(); buf = dString.getBytes(); InetAddress group = InetAddress.getByName("203.0.113.0"); DatagramPacket packet; packet = new DatagramPacket(buf, buf.length, group, 4446); socket.send(packet); try { sleep((long)Math.random() * FIVE_SECONDS); } catch (InterruptedException e) { } } catch (IOException e) { e.printStackTrace(); moreQuotes = false; } } socket.close(); }
有趣的变化是如何构造DatagramPacket
,特别是用于构造DatagramPacket
的InetAddress
和端口。回想一下,前一个示例从客户端发送到服务器的数据包中检索了InetAddress
和端口号。这是因为服务器需要直接回复给客户端。现在,服务器需要向多个客户端发送数据。因此,这次InetAddress
和端口号都是硬编码的。
硬编码的端口号是 4446(客户端必须将MulticastSocket
绑定到此端口)。DatagramPacket
的硬编码InetAddress
是"203.0.113.0",是一个组标识符(而不是单个客户端所在机器的互联网地址)。这个特定地址是从保留给此目的的地址中任意选择的。
以这种方式创建的DatagramPacket
将发送到所有监听端口号为 4446 且属于"203.0.113.0"组的客户端。
要监听端口号 4446,新创建的客户端程序只需使用该端口号创建其MulticastSocket
。要成为"203.0.113.0"组的成员,客户端调用MulticastSocket
的joinGroup
方法,并提供标识该组的InetAddress
。现在,客户端已设置好接收发送到指定端口和组的DatagramPacket
。以下是新客户端程序中相关的代码(还对其进行了重写,以 passively 接收引用而不是主动请求)。粗体语句是与MulticastSocket
交互的语句:
MulticastSocket socket = new MulticastSocket(4446); InetAddress group = InetAddress.getByName("203.0.113.0"); socket.joinGroup(group); DatagramPacket packet; for (int i = 0; i < 5; i++) { byte[] buf = new byte[256]; packet = new DatagramPacket(buf, buf.length); socket.receive(packet); String received = new String(packet.getData()); System.out.println("Quote of the Moment: " + received); } socket.leaveGroup(group); socket.close();
注意,服务器使用DatagramSocket
广播客户端接收的DatagramPacket
,而客户端使用MulticastSocket
。或者,它可以使用MulticastSocket
。服务器用于发送DatagramPacket
的套接字并不重要。在广播数据包时重要的是DatagramPacket
中包含的寻址信息,以及客户端用于监听的套接字
试试这个:
运行MulticastServer
和几个客户端。观察客户端如何都收到相同的引用。
课程:程序化访问网络参数
原文:
docs.oracle.com/javase/tutorial/networking/nifs/index.html
系统通常会同时运行多个活动网络连接,比如有线以太网,802.11 b/g
(无线)和蓝牙。一些应用程序可能需要访问这些信息,以便在特定连接上执行特定的网络活动。
java.net.NetworkInterface
类提供了访问这些信息的途径。
本课程将指导您了解此类的一些常见用法,并提供列出机器上所有网络接口及其 IP 地址和状态的示例。
什么是网络接口?
本页面描述了一个网络接口,并解释了为什么您可能想要使用它。
检索网络接口
本页面包含一个示例,演示了客户端程序如何检索机器上的所有网络接口。
列出网络接口地址
本页面向您展示如何列出分配给机器上所有网络接口的 IP 地址。
网络接口参数
本页面向您展示如何确定网络接口是否正在运行,或者网络接口是环回接口、点对点接口还是虚拟接口。您还可以了解如何确定接口是否支持多播。
什么是网络接口?
原文:
docs.oracle.com/javase/tutorial/networking/nifs/definition.html
网络接口是计算机与私有或公共网络之间的连接点。网络接口通常是网络接口卡(NIC),但不一定要有物理形式。相反,网络接口可以在软件中实现。例如,环回接口(IPv4 的127.0.0.1
和 IPv6 的::1
)不是物理设备,而是模拟网络接口的软件部分。环回接口通常用于测试环境。
java.net.NetworkInterface
类代表两种类型的接口。
NetworkInterface
对于具有多个 NIC 的多宿主系统非常有用。使用NetworkInterface
,您可以指定要用于特定网络活动的 NIC。
例如,假设您有一台配置了两个 NIC 的机器,并且您想向服务器发送数据。您可以这样创建一个套接字:
Socket soc = new java.net.Socket(); soc.connect(new InetSocketAddress(address, port));
发送数据时,系统会确定使用哪个接口。但是,如果您有偏好或需要指定要使用的 NIC,您可以查询系统以找到适当的接口并在您想要使用的接口上找到一个地址。当您创建套接字并将其绑定到该地址时,系统将使用相关联的接口。例如:
NetworkInterface nif = NetworkInterface.getByName("bge0"); Enumeration<InetAddress> nifAddresses = nif.getInetAddresses(); Socket soc = new java.net.Socket(); soc.bind(new InetSocketAddress(nifAddresses.nextElement(), 0)); soc.connect(new InetSocketAddress(address, port));
您还可以使用NetworkInterface
来识别要加入多播组的本地接口。例如:
NetworkInterface nif = NetworkInterface.getByName("bge0"); MulticastSocket ms = new MulticastSocket(); ms.joinGroup(new InetSocketAddress(hostname, port), nif);
NetworkInterface
可以与 Java API 一起以许多其他方式使用,超出了此处描述的两种用途。
检索网络接口
原文:
docs.oracle.com/javase/tutorial/networking/nifs/retrieving.html
NetworkInterface
类没有公共构造函数。因此,您不能只使用new
运算符创建此类的新实例。相反,提供了以下静态方法,以便您可以从系统中检索接口详细信息:getByInetAddress()
、getByName()
和 getNetworkInterfaces()
。前两种方法用于当您已经知道特定接口的 IP 地址或名称时。第三种方法,getNetworkInterfaces()
返回计算机上所有接口的完整列表。
网络接口可以按层次结构组织。NetworkInterface
类包括两个方法,getParent()
和 getSubInterfaces()
,这些方法与网络接口层次结构相关。getParent()
方法返回接口的父NetworkInterface
。如果网络接口是子接口,则getParent()
返回一个非空值。getSubInterfaces()
方法返回网络接口的所有子接口。
以下示例程序列出了计算机上所有网络接口和子接口(如果存在)的名称。
import java.io.*; import java.net.*; import java.util.*; import static java.lang.System.out; public class ListNIFs { public static void main(String args[]) throws SocketException { Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); for (NetworkInterface netIf : Collections.list(nets)) { out.printf("Display name: %s\n", netIf.getDisplayName()); out.printf("Name: %s\n", netIf.getName()); displaySubInterfaces(netIf); out.printf("\n"); } } static void displaySubInterfaces(NetworkInterface netIf) throws SocketException { Enumeration<NetworkInterface> subIfs = netIf.getSubInterfaces(); for (NetworkInterface subIf : Collections.list(subIfs)) { out.printf("\tSub Interface Display name: %s\n", subIf.getDisplayName()); out.printf("\tSub Interface Name: %s\n", subIf.getName()); } } }
以下是示例程序的样本输出:
Display name: bge0 Name: bge0 Sub Interface Display name: bge0:3 Sub Interface Name: bge0:3 Sub Interface Display name: bge0:2 Sub Interface Name: bge0:2 Sub Interface Display name: bge0:1 Sub Interface Name: bge0:1 Display name: lo0 Name: lo0
Java 中文官方教程 2022 版(四十五)(4)https://developer.aliyun.com/article/1488414