访问网络(特别是互联网)正成为应用程序的一个重要且经常是必要的功能。应用程序经常需要访问和提供服务。随着物联网(物联网)连接越来越多的设备,了解如何接入网络变得至关重要。
是推动更多网络应用的重要因素,包括更快网络和更大带宽的可用性。这使得传输更大范围的数据成为可能,例如视频流。近年来,无论是针对新服务、更广泛的社交互动还是游戏,我们都看到了连通性的增加。了解如何开发网络应用程序是一项重要的开发技能。
在本章中,我们将介绍网络编程的基础知识:
- 为什么网络很重要
- Java 提供的支持
- 解决基本网络操作的简单程序
- 基本网络术语
- 一个简单的服务器/客户端应用程序
- 使用线程支持服务器
在本书中,您将接触到许多使用旧 Java 技术和新 Java 技术的网络概念、想法、模式和实现策略。网络连接使用套接字在较低级别上进行,使用多种协议在更高级别上进行。通信可以是同步的,需要仔细协调请求和响应,也可以是异步的,在提交响应之前执行其他活动。
这些概念和其他概念通过一系列章节加以阐述,每个章节都侧重于一个特定的主题。各章通过尽可能详细阐述先前介绍的概念相互补充。尽可能使用大量代码示例来加深您对主题的理解。
访问服务的核心是知道或发现其地址。该地址可以是人类可读的,例如www.packtpub.com,或者以IP地址的形式,例如83.166.169.231
。互联网协议(IP)是一种用于访问互联网上信息的低级寻址方案。寻址长期以来一直使用 IPv4 来访问资源。然而,这些地址几乎都不见了。较新的 IPv6 可提供更大范围的地址。第 2 章网络寻址的重点是网络寻址的基础知识以及如何在 Java 中进行管理。
网络通信的目的是在其他应用程序之间传输信息。这通过使用缓冲区和通道来实现。缓冲区暂时保存信息,直到应用程序可以处理它为止。通道是简化应用程序之间通信的抽象。NIO 和 NIO.2 包提供了对缓冲区和通道的大部分支持。我们将在第 3 章网络 NIO 支持中探讨这些技术以及其他技术,如阻塞和非阻塞 IO。
服务由服务器提供。这方面的一个例子是简单的 echo 服务器,它重新传输发送的内容。更复杂的服务器,如 HTTP 服务器,可以支持广泛的服务以满足广泛的需求。客户端/服务器模型及其 Java 支持见第 3 章、网络 NIO 支持。
另一种服务模式是对等(P2P模式)。在这种体系结构中,没有中央服务器,而是一个应用程序网络,通过通信提供服务。该模型由应用程序表示,如 BitTorrent、Skype 和 BBC 的 iPlayer。虽然开发这些类型的应用程序所需的许多支持超出了本书的范围,第 4 章客户端/服务器开发探讨了 P2P 问题以及 Java 和 JXTA 提供的支持。
IP 在较低级别上用于通过网络发送和接收信息包。我们还将演示用户数据报协议****UDP和传输控制协议****TCP通信协议的使用。这些协议是在 IP 之上分层的。UDP 用于广播短数据包或消息,但不保证可靠传递。TCP 比 UDP 更常用,并提供更高级别的服务。我们将在第 5 章、对等网络中介绍这些相关技术的使用。
由于许多因素,服务通常会面临不同程度的需求。它的负载可能随一天中的时间而变化。随着它越来越受欢迎,其总体需求也将增加。服务器需要扩展以满足负载的增减。线程和线程池被用来支持这项工作。这些技术和其他技术是第 6 章、UDP 和多播的重点。
应用程序越来越需要防止黑客的攻击。当它连接到网络时,这种威胁会增加。在第 7 章、网络可扩展性中,我们将探讨许多可用于支持安全 Java 应用程序的技术。其中包括安全套接字级别(SSL),以及 Java 如何支持它。
应用程序很少单独工作。因此,他们需要使用网络访问其他应用程序。然而,并非所有的应用程序都是用 Java 编写的。与这些应用程序联网可能会带来一些特殊问题,从数据类型的字节如何组织到应用程序支持的接口。使用专用协议(如 HTTP 和 WSDL)是很常见的。本书的最后一章从 Java 的角度研究了这些问题。
我们将演示旧的和新的 Java 技术。为了维护较旧的代码,了解较旧的技术可能是必要的,它可以让我们深入了解为什么要开发较新的技术。我们还将使用许多 Java8 函数编程技术来补充我们的示例。通过使用 Java8 示例以及 Java8 之前的实现,我们可以学习如何使用 Java8,并更好地了解何时可以使用 Java8 以及何时应该使用 Java8。
本文并不打算全面解释较新的 Java8 技术,例如 lambda 表达式和流。但是,使用 Java8 示例将深入了解如何使用它们来支持网络应用程序。
本章的其余部分涉及本书中探讨的许多网络技术。我们将向您介绍这些技术的基础知识,您会发现它们很容易理解。然而,在一些地方,时间不允许我们充分探索和解释这些概念。这些问题将在后续章节中讨论。因此,让我们从网络寻址开始探索。
IP 地址由InetAddress
类表示。地址可以是单播,用于标识特定地址,也可以是多播,用于将消息发送到多个地址。
InetAddress
类没有公共构造函数。要获取实例,请使用几个静态 get-type 方法之一。例如,getByName
方法采用表示地址的字符串,如下所示。本例中的字符串为统一资源定位器(URL:
InetAddress address =
InetAddress.getByName("www.packtpub.com");
System.out.println(address);
下载示例代码
您可以下载您在账户购买的所有 Packt 书籍的示例代码文件 http://www.packtpub.com 。如果您在其他地方购买了本书,您可以访问http://www.packtpub.com/support 并注册,将文件直接通过电子邮件发送给您。
这将显示以下结果:
www.packtpub.com/83.166.169.231
附加在名称末尾的数字是 IP 地址。此地址唯一标识 Internet 上的实体。
如果我们需要关于地址的其他信息,我们可以使用以下几种方法之一,如下所示:
System.out.println("CanonicalHostName: "
+ address.getCanonicalHostName());
System.out.println("HostAddress: " +
address.getHostAddress());
System.out.println("HostName: " + address.getHostName());
执行时,将生成以下输出:
规范主机名:83.166.169.231
主机地址:83.166.169.231
主机名:www.packtpub.com
要测试此地址是否可访问,请使用isReachable
方法,如下所示。它的参数指定在决定无法到达地址之前要等待多长时间。参数是等待的毫秒数:
address.isReachable(10000);
还有分别支持 IPv4 和 IPv6 地址的Inet4Address
和Inet6Address
类。我们将在第 2 章、网络寻址中解释它们的用法。
一旦我们获得了一个地址,我们就可以使用它来支持网络访问,比如服务器。在本文中演示它的使用之前,让我们先看看如何从连接中获取和处理数据。
java.io
、java.nio
和java.nio
子包为 IO 处理提供了大部分 Java 支持。我们将在第 3 章NIO 网络支持中检查这些包对网络访问的支持。在这里,我们将重点介绍java.nio
套餐的基本方面。
NIO 包中使用了三个关键概念:
- 通道:表示应用程序之间的数据流
- 缓冲区:与一起工作,通过一个通道处理数据
- 选择器:这是一种允许单个线程处理多个通道的技术
通道和缓冲区通常相互关联。数据可以从通道传输到缓冲器,也可以从缓冲器传输到通道。顾名思义,缓冲区是信息的临时存储库。选择器在支持应用程序可伸缩性方面很有用,这将在第 7 章、网络可伸缩性中讨论。
有四个主通道:
FileChannel
:这对文件有效DatagramChannel
:此支持 UDP 通信SocketChannel
:用于 TCP 客户端ServerSocketChannel
:这是与 TCP 服务器一起使用的
有几个缓冲区类支持基本数据类型,例如 character、integer 和 float。
访问服务器的一种简单方法是使用URLConnection
类。此类表示应用程序和URL
实例之间的连接。URL
实例表示 Internet 上的一个资源。
在下一个示例中,将为 Google 网站创建一个 URL 实例。使用URL
类的openConnection
方法创建URLConnection
实例。BufferedReader
实例用于从连接中读取线路,然后显示:
try {
URL url = new URL("http://www.google.com");
URLConnection urlConnection = url.openConnection();
BufferedReader br = new BufferedReader(
new InputStreamReader(
urlConnection.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
} catch (IOException ex) {
// Handle exceptions
}
输出比较长,所以这里只显示了第一行的一部分:
<!doctype html><html itemscope=”“itemtype=”http://schema.org/WebPage" ...
URLConnection
类隐藏了访问 HTTP 服务器的一些复杂性。
我们可以修改前面的示例来说明通道和缓冲区的使用。URLConnection
实例与前面一样创建。我们将创建一个ReadableByteChannel
实例,然后创建一个ByteBuffer
实例,如下一个示例所示。ReadableByteChannel
实例允许我们使用read
方法读取站点。一个ByteBuffer
实例从通道接收数据,并用作read
方法的参数。创建的缓冲区一次可容纳 64 个字节。
read
方法返回读取的字节数。ByteBuffer
类的array
方法返回一个字节数组,用作String
类构造函数的参数。这用于显示读取的数据。clear
方法用于复位缓冲器,以便再次使用:
try {
URL url = new URL("http://www.google.com");
URLConnection urlConnection = url.openConnection();
InputStream inputStream = urlConnection.getInputStream();
ReadableByteChannel channel =
Channels.newChannel(inputStream);
ByteBuffer buffer = ByteBuffer.allocate(64);
String line = null;
while (channel.read(buffer) > 0) {
System.out.println(new String(buffer.array()));
buffer.clear();
}
channel.close();
} catch (IOException ex) {
// Handle exceptions
}
下面显示输出的第一行。此产生与之前相同的输出,但限制为一次显示 64 字节:
<!doctype html><html itemscope=”“itemtype=”http://schema.org/We
Channel
类及其派生类为提供了一种改进的技术来访问网络上发现的数据,而不是旧技术提供的数据。我们将看到更多这门课。
有几种使用 Java 创建服务器的方法。我们将演示一些简单的方法,并将这些技术的详细讨论推迟到第 4 章、客户端/服务器开发之后。将同时创建客户端和服务器。
在具有 IP 地址的机器上安装了服务器。在任何给定时间,一台机器上都可能运行多台服务器。当操作系统收到机器上的服务请求时,它还将收到端口号。端口号将标识请求应转发到的服务器。因此,服务器由其 IP 地址和端口号的组合来标识。
通常,客户端会向服务器发出请求。服务器将接收请求并发送回响应。请求/响应的性质以及用于通信的协议取决于客户端/服务器。有时会使用一个记录良好的协议,例如超文本传输协议(HTTP)。对于更简单的体系结构,会来回发送一系列文本消息。
为了使服务器与发出请求的应用程序通信,使用专用软件发送和接收消息。这个软件叫做套接字。一个套接字位于客户端,另一个套接字位于服务器端。当他们连接时,通信是可能的。有几种不同类型的插座。这些包括数据报套接字;流套接字,经常使用 TCP;和原始套接字,它们通常在 IP 级别工作。我们将重点介绍客户端/服务器应用程序的 TCP 套接字。
具体来说,我们将创建一个简单的 echo 服务器。此服务器将从客户端接收文本消息,并立即将其发送回该客户端。该服务器的简单性使我们能够专注于客户端-服务器基础。
我们将从开始定义SimpleEchoServer
类,如下所示。在main
方法中,将显示初始服务器消息:
public class SimpleEchoServer {
public static void main(String[] args) {
System.out.println("Simple Echo Server");
...
}
}
方法主体的其余部分由一系列处理异常的 try 块组成。在第一个 try 块中,使用6000
作为参数创建ServerSocket
实例。ServerSocket
类是一个专用套接字,服务器使用它来侦听客户端请求。它的参数是它的端口号。服务器所在机器的 IP 不一定是服务器感兴趣的,但客户端最终需要知道该 IP 地址。
在下一个代码序列中,创建ServerSocket
类的一个实例并调用其accept
方法。ServerSocket
将阻止此呼叫,直到收到客户端的请求。阻塞意味着程序被挂起,直到方法返回。当收到请求时,accept
方法将返回一个Socket
类实例,该实例表示该客户端和服务器之间的连接。他们现在可以发送和接收消息:
try (ServerSocket serverSocket = new ServerSocket(6000)){
System.out.println("Waiting for connection.....");
Socket clientSocket = serverSocket.accept();
System.out.println("Connected to client");
...
} catch (IOException ex) {
// Handle exceptions
}
创建此客户端套接字后,我们可以处理发送到服务器的消息。当我们处理文本时,我们将使用BufferedReader
实例从客户端读取消息。这是使用客户端套接字的getInputStream
方法创建的。我们将使用一个PrintWriter
实例来回复客户端。这是使用客户端套接字的getOutputStream
方法创建的,如下所示:
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true)) {
...
}
}
PrintWriter
构造函数的第二个参数设置为true
。这意味着使用out
对象发送的文本将在每次使用后自动刷新。
当文本写入套接字时,它将位于缓冲区中,直到缓冲区已满或调用刷新方法为止。执行自动刷新可以避免我们必须记住刷新缓冲区,但这可能会导致过度刷新,而在执行最后一次写入后发出的单个刷新也可以。
下一个代码段完成了服务器。readLine
方法从客户端一次读取一行。显示此文本,然后使用out
对象将其发送回客户端:
String inputLine;
while ((inputLine = br.readLine()) != null) {
System.out.println("Server: " + inputLine);
out.println(inputLine);
}
在演示服务器的实际操作之前,我们需要创建一个客户端应用程序来使用它。
我们从一个SimpleEchoClient
类的声明开始,在main
方法中,会显示一条消息,指示应用程序的启动,如下所示:
public class SimpleEchoClient {
public static void main(String args[]) {
System.out.println("Simple Echo Client");
...
}
}
需要创建一个Socket
实例来连接服务器。在下面的示例中,假定服务器和客户端在同一台机器上运行。InetAddress
类的静态getLocalHost
方法返回此地址,然后与端口6000
一起在Socket
类的构造函数中使用。如果它们位于不同的机器上,则需要使用服务器的地址。与服务器一样,创建了PrintWriter
和BufferedReader
类的实例,以允许向服务器发送文本和从服务器发送文本:
try {
System.out.println("Waiting for connection.....");
InetAddress localAddress = InetAddress.getLocalHost();
try (Socket clientSocket = new Socket(localAddress, 6000);
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true);
BufferedReader br = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()))) {
...
}
} catch (IOException ex) {
// Handle exceptions
}
Localhost 是指当前机器。它有一个特定的 IP 地址:127.0.0.1
。虽然一台机器可能与一个额外的 IP 地址相关联,但每台机器都可以使用这个本地主机地址访问自己。
然后,提示用户输入文本。如果文本是 quit 命令,则无限循环终止,应用程序关闭。否则,文本将使用out
对象发送到服务器。返回回复后,将显示如下所示:
System.out.println("Connected to server");
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter text: ");
String inputLine = scanner.nextLine();
if ("quit".equalsIgnoreCase(inputLine)) {
break;
}
out.println(inputLine);
String response = br.readLine();
System.out.println("Server response: " + response);
}
这些计划可以作为两个单独的项目或在单个项目中实施。无论哪种方式,首先启动服务器,然后启动客户端。服务器启动时,将显示以下内容:
简单回音服务器
正在等待连接。。。。。
当客户端启动时,您将看到以下内容:
简单回音客户端
正在等待连接。。。。。
已连接到服务器
输入文本:
输入消息,并观察客户端和服务器如何交互。从客户的角度来看,以下是一系列可能的输入:
输入文本:你好服务器
服务器响应:你好服务器
输入文字:回显!
服务器响应:回应!
输入文本:退出
客户端输入quit
命令后,服务器的输出如图所示:
简单回音服务器
正在等待连接。。。。。
已连接到客户端
客户端请求:你好服务器
客户请求:回显!
这是实现客户端和服务器的一种方法。我们将在后面的章节中增强此实现。
在本书中,我们将提供使用许多较新的 Java8 特性的示例。这里,我们将向您展示以前 echo 服务器和客户端应用程序的替代实现。
服务器使用 while 循环处理客户端的请求,如下所示:
String inputLine;
while ((inputLine = br.readLine()) != null) {
System.out.println("Client request: " + inputLine);
out.println(inputLine);
}
我们可以将Supplier
接口与Stream
对象结合使用来执行相同的操作。下一条语句使用 lambda 表达式从客户端返回字符串:
Supplier<String> socketInput = () -> {
try {
return br.readLine();
} catch (IOException ex) {
return null;
}
};
Supplier
实例生成无限流。下面的map
方法从用户获取输入,然后将其发送到服务器。当输入quit
时,流将终止。allMatch
方法是一种短路方法,当其参数计算为false
时,流终止:
Stream<String> stream = Stream.generate(socketInput);
stream
.map(s -> {
System.out.println("Client request: " + s);
out.println(s);
return s;
})
.allMatch(s -> s != null);
虽然此实现比传统实现更长,但它可以为更复杂的问题提供更简洁和简单的解决方案。
在客户端,我们可以用功能实现替换此处重复的 while 循环:
while (true) {
System.out.print("Enter text: ");
String inputLine = scanner.nextLine();
if ("quit".equalsIgnoreCase(inputLine)) {
break;
}
out.println(inputLine);
String response = br.readLine();
System.out.println("Server response: " + response);
}
功能解决方案还使用Supplier
实例捕获控制台输入,如下所示:
Supplier<String> scannerInput = () -> scanner.next();
生成无限流,如下图所示,使用map
方法提供等效功能:
System.out.print("Enter text: ");
Stream.generate(scannerInput)
.map(s -> {
out.println(s);
System.out.println("Server response: " + s);
System.out.print("Enter text: ");
return s;
})
.allMatch(s -> !"quit".equalsIgnoreCase(s));
功能方法通常是解决许多问题的更好方法。
请注意,在输入了quit
命令后,客户端会显示一个额外的提示输入文本:。如果输入了quit
命令,则不显示提示,这很容易纠正。此更正留作读者练习。
如果您需要定期向组发送消息,则可以使用多播技术。它使用一个 UDP 服务器和一个或多个 UDP 客户端。为了演示此功能,我们将创建一个简单的时间服务器。服务器将每秒向客户端发送一个日期和时间字符串。
多播将向组中的每个成员发送相同的消息。组由多播地址标识。多播地址必须使用以下 IP 地址范围:224.0.0.0
到239.255.255.255
。服务器将发送带有此地址的消息标记。客户端必须加入组才能接收任何多播消息。
接下来声明一个MulticastServer
类,在这里创建一个DatagramSocket
实例。try-catch 块将在异常发生时处理异常:
public class MulticastServer {
public static void main(String args[]) {
System.out.println("Multicast Time Server");
DatagramSocket serverSocket = null;
try {
serverSocket = new DatagramSocket();
...
}
} catch (SocketException ex) {
// Handle exception
} catch (IOException ex) {
// Handle exception
}
}
}
try 块的主体使用无限循环创建一个字节数组来保存当前日期和时间。接下来,创建表示多播组的InetAddress
实例。使用数组和组地址,实例化一个DatagramPacket
并用作类的send
方法的参数。然后显示发送的数据和时间。然后服务器暂停一秒钟:
while (true) {
String dateText = new Date().toString();
byte[] buffer = new byte[256];
buffer = dateText.getBytes();
InetAddress group = InetAddress.getByName("224.0.0.0");
DatagramPacket packet;
packet = new DatagramPacket(buffer, buffer.length,
group, 8888);
serverSocket.send(packet);
System.out.println("Time sent: " + dateText);
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
// Handle exception
}
}
此服务器仅广播消息。它从不从客户端接收消息。
客户端是使用以下MulticastClient
类创建的。为了接收消息,客户端必须使用相同的组地址和端口号。在接收消息之前,它必须使用joinGroup
方法加入组。在这个实现中,它接收 5 条日期和时间消息,显示它们,然后终止。trim
方法从字符串中删除前导空格和尾随空格。否则,将显示消息的所有 256 字节:
public class MulticastClient {
public static void main(String args[]) {
System.out.println("Multicast Time Client");
try (MulticastSocket socket = new MulticastSocket(8888)) {
InetAddress group =
InetAddress.getByName("224.0.0.0");
socket.joinGroup(group);
System.out.println("Multicast Group Joined");
byte[] buffer = new byte[256];
DatagramPacket packet =
new DatagramPacket(buffer, buffer.length);
for (int i = 0; i < 5; i++) {
socket.receive(packet);
String received = new String(packet.getData());
System.out.println(received.trim());
}
socket.leaveGroup(group);
} catch (IOException ex) {
// Handle exception
}
System.out.println("Multicast Time Client Terminated");
}
}
服务器启动时,发送的消息如下所示:
多播时间服务器
发送时间:2015 年 7 月 9 日星期四 13:19:49 CDT
发送时间:2015 年 7 月 9 日星期四 13:19:50 CDT
发送时间:CDT 2015 年 7 月 9 日星期四 13:19:51
发送时间:2015 年 7 月 9 日星期四 13:19:52 CDT
发送时间:2015 年 7 月 9 日星期四 13:19:53 CDT
发送时间:2015 年 7 月 9 日星期四 13:19:54 CDT
发送时间:CDT 2015 年 7 月 9 日星期四 13:19:55
。。。
客户端输出将类似于以下内容:
多播时间客户端
多播组加入
周四 7 月 9 日 13:19:50 CDT 2015
周四 7 月 9 日 13:19:51 CDT 2015
周四 7 月 9 日 13:19:52 CDT 2015
周四 7 月 9 日 13:19:53 CDT 2015
周四 7 月 9 日 13:19:54 CDT 2015
多播时间客户端终止
如果该示例是在 Mac 上执行的,您可能会收到一个异常,表明它无法分配请求的地址。这可以通过使用 JVM 选项-Djava.net.preferIPv4Stack=true
来修复。
还有许多其他多播功能,将在第 6 章、UDP 和多播中探讨。
当服务器上的需求增加或减少时,最好更改专用于服务器的资源。可用的选项范围从使用手动线程来允许并发行为到嵌入在专用类中以处理线程池和 NIO 通道。
在本节中,我们将使用线程来扩充我们的简单 echo 服务器。ThreadedEchoServer
类的定义如下。它实现了Runnable
接口,为每个连接创建一个新线程。privateSocket
变量将保存特定线程的客户端套接字:
public class ThreadedEchoServer implements Runnable {
private static Socket clientSocket;
public ThreadedEchoServer(Socket clientSocket) {
this.clientSocket = clientSocket;
}
...
}
线程是与应用程序中的其他代码块并行执行的代码块。Thread
类支持 Java 中的线程。虽然有几种创建线程的方法,但其中一种方法是将实现Runnable
接口的对象传递给其构造函数。调用Thread
类的start
方法时,创建线程并执行Runnable
接口的run
方法。当run
方法终止时,线程也会终止。
添加线程的另一种方法是为线程使用单独的类。它可以声明为独立于ThreadedEchoServer
类,也可以声明为ThreadedEchoServer
类的内部类。使用单独的类,可以更好地拆分应用程序的功能。
main
方法像以前一样创建服务器套接字,但是当创建客户端套接字时,客户端套接字用于创建线程,如下所示:
public static void main(String[] args) {
System.out.println("Threaded Echo Server");
try (ServerSocket serverSocket = new ServerSocket(6000)) {
while (true) {
System.out.println("Waiting for connection.....");
clientSocket = serverSocket.accept();
ThreadedEchoServer tes =
new ThreadedEchoServer(clientSocket);
new Thread(tes).start();
}
} catch (IOException ex) {
// Handle exceptions
}
System.out.println("Threaded Echo Server Terminating");
}
实际工作按run
方法执行,如下所示。它本质上与原始 echo 服务器的实现相同,只是显示当前线程以澄清正在使用的线程:
@Override
public void run() {
System.out.println("Connected to client using ["
+ Thread.currentThread() + "]");
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = br.readLine()) != null) {
System.out.println("Client request ["
+ Thread.currentThread() + "]: " + inputLine);
out.println(inputLine);
}
System.out.println("Client [" + Thread.currentThread()
+ " connection terminated");
} catch (IOException ex) {
// Handle exceptions
}
}
以下输出显示了服务器和两个客户端之间的交互。最初的 echo 客户端被启动了两次。如您所见,每个客户端交互都是使用不同的线程执行的:
线程化回音服务器
正在等待连接。。。。。
正在等待连接。。。。。
使用[Thread[Thread-0,5,main]]连接到客户端
客户端请求[Thread[Thread-0,5,main]]:来自客户端 1 的你好
客户端请求【线程[Thread-0,5,main】]:这边好
正在等待连接。。。。。
使用[Thread[Thread-1,5,main]]连接到客户端
客户端请求【线程[Thread-1,5,main]]:来自客户端 2 的你好
客户端请求[Thread[Thread-1,5,main]]:你好!
客户端请求【线程【线程-1,5,主】】:退出
客户端【线程【线程-1,5,主】连接终止
客户端请求[线程[Thread-0,5,main]]:太长了
客户端请求【线程【线程-0,5,主】】:退出
以下交互是从第一个客户的角度进行的:
简单回音客户端
正在等待连接。。。。。
已连接到服务器
输入文本:来自客户 1 的你好
服务器响应:来自客户端 1的你好
输入文字:这边好
服务器响应:这边不错
输入文字:这么长
服务器响应:这么长
输入文本:退出
服务器响应:退出
从第二个客户的角度来看,以下互动是:
简单回音客户端
正在等待连接。。。。。
已连接到服务器
输入文本:来自客户 2 的你好
服务器响应:来自客户端 2的你好
输入文字:你好!
服务器响应:你好!
输入文本:退出
服务器响应:退出
此实现允许一次处理多个客户端。客户端未被阻止,因为另一个客户端正在使用服务器。但是,它也允许创建大量线程。如果存在太多线程,则服务器性能可能会降低。我们将在第 7 章、网络可扩展性中讨论这些问题。
安全是一个复杂的话题。在本节中,我们将演示本主题与网络相关的几个简单方面。具体来说,我们将创建一个安全的 echo 服务器。创建安全的 echo 服务器与我们先前开发的非安全 echo 服务器没有太大区别。然而,在幕后还有很多事情要做。我们现在可以忽略这些细节,但我们将在第 8 章、网络安全中更深入地探讨。
我们将使用SSLServerSocketFactory
类来实例化安全服务器套接字。此外,有必要创建底层 SSL 机制可用于加密通信的密钥。
在下面的示例中,SSLServerSocket
类被声明作为 echo 服务器。由于它与之前的 echo 服务器类似,我们将不解释它的实现,除了它与使用SSLServerSocketFactory
类的关系。它的静态getDefault
方法返回一个ServerSocketFactory
实例。其createServerSocket
方法返回绑定到端口8000
的ServerSocket
实例,该端口能够支持安全通信。否则,它的组织和功能与以前的 echo 服务器类似:
public class SSLServerSocket {
public static void main(String[] args) {
try {
SSLServerSocketFactory ssf = (SSLServerSocketFactory)
SSLServerSocketFactory.getDefault();
ServerSocket serverSocket =
ssf.createServerSocket(8000);
System.out.println("SSLServerSocket Started");
try (Socket socket = serverSocket.accept();
PrintWriter out = new PrintWriter(
socket.getOutputStream(), true);
BufferedReader br = new BufferedReader(
new InputStreamReader(
socket.getInputStream()))) {
System.out.println("Client socket created");
String line = null;
while (((line = br.readLine()) != null)) {
System.out.println(line);
out.println(line);
}
br.close();
System.out.println("SSLServerSocket Terminated");
} catch (IOException ex) {
// Handle exceptions
}
} catch (IOException ex) {
// Handle exceptions
}
}
}
安全 echo 客户端也类似于之前的非安全 echo 客户端。SSLSocketFactory
类“getDefault
返回一个SSLSocketFactory
实例,该实例的createSocket
创建了一个连接到安全 echo 服务器的套接字。申请如下:
public class SSLClientSocket {
public static void main(String[] args) throws Exception {
System.out.println("SSLClientSocket Started");
SSLSocketFactory sf =
(SSLSocketFactory) SSLSocketFactory.getDefault();
try (Socket socket = sf.createSocket("localhost", 8000);
PrintWriter out = new PrintWriter(
socket.getOutputStream(), true);
BufferedReader br = new BufferedReader(
new InputStreamReader(
socket.getInputStream()))) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter text: ");
String inputLine = scanner.nextLine();
if ("quit".equalsIgnoreCase(inputLine)) {
break;
}
out.println(inputLine);
System.out.println("Server response: " +
br.readLine());
}
System.out.println("SSLServerSocket Terminated");
}
}
}
如果我们先执行此服务器,然后执行客户端,则它们将因连接错误而中止。这是因为我们没有提供应用程序可以共享和使用的一组密钥来保护它们之间传递的数据。
为了提供必要的密钥,我们需要创建一个密钥库来保存密钥。当应用程序执行时,密钥库必须对应用程序可用。首先,我们将演示如何创建密钥库,然后我们将向您展示必须提供哪些运行时参数。
Java SE SDK 的bin
目录中有一个名为keytool
的程序。这是一个命令级程序,将生成必要的密钥并将其存储在密钥文件中。在 Windows 中,您需要打开一个命令窗口并导航到源文件的根目录。此目录将包含保存应用程序包的目录。
在 Mac 上,您可能无法生成密钥对。有关在 Mac 上使用此工具的更多信息,请参见https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/keytool.1.html 。
您还需要使用类似于以下命令的命令设置bin
目录的路径。查找并执行keytool
应用程序需要此命令:
set path= C:\Program Files\Java\jdk1.8.0_25\bin;%path%
接下来,输入keytool
命令。系统将提示您输入用于创建密钥的密码和其他信息。此处显示了此过程,其中使用了123456
密码,但在输入时未显示该密码:
Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]: First Last
What is the name of your organizational unit?
[Unknown]: packt
What is the name of your organization?
[Unknown]: publishing
What is the name of your City or Locality?
[Unknown]: home
What is the name of your State or Province?
[Unknown]: calm
What is the two-letter country code for this unit?
[Unknown]: me
Is CN=First Last, OU=packt, O=publishing, L=home, ST=calm, C=me correct?
[no]: y
Enter key password for <mykey>
(RETURN if same as keystore password):
创建密钥库后,您可以运行服务器和客户端应用程序。这些应用程序的启动方式取决于项目的创建方式。您可以从 IDE 执行它,或者您可能需要从命令窗口启动它们。
接下来是可以从命令窗口使用的命令。java
命令的两个参数是密钥库的位置和密码。它们需要从包目录的根目录执行:
java -Djavax.net.ssl.keyStore=keystore.jks -Djavax.net.ssl.keyStorePassword=123456 packt.SSLServerSocket
java -Djavax.net.ssl.trustStore=keystore.jks -Djavax.net.ssl.trustStorePassword=123456 packt.SSLClientSocket
如果要使用 IDE,请对运行时命令参数使用等效设置。下面的示例说明了客户端和服务器之间的一种可能的交换。首先显示服务器窗口的输出,然后显示客户端的输出:
SSLServerSocket 启动
客户端套接字已创建
你好回声服务器
安全可靠
SSLServerSocket 端接
SSLClientSocket 启动
输入文本:Hello echo server
服务器响应:Hello echo 服务器
输入文本:安全可靠
服务器响应:安全可靠
输入文本:退出
SSLServerSocket 端接
关于 SSL,需要了解的内容比这里显示的更多。然而,这提供了流程的概述,更多细节见第 8 章、网络安全。
网络应用在当今社会中扮演着越来越重要的角色。随着越来越多的设备连接到 Internet,了解如何构建能够与其他应用程序通信的应用程序非常重要。
我们简要地识别并解释了 Java 用于连接网络的几种技术。我们演示了InetAddress
类如何表示 IP 地址,并在几个示例中使用了该类。使用 UDP、TCP 和 SSL 技术演示了客户端/服务器体系结构的基本元素。它们提供不同类型的支持。UDP 速度快,但不如 TCP 可靠或功能强大。TCP 是一种可靠且方便的通信方式,但除非与 SSL 一起使用,否则不安全。
说明了 NIO 对缓冲区和通道的支持。这些技术可以提高通信效率。应用程序的可伸缩性对于许多应用程序来说是至关重要的,特别是客户端/服务器模型。我们还了解了线程如何支持可伸缩性。
这些主题中的每一个都将在后面的章节中进行更详细的讨论。这包括 NIO 对可伸缩性的支持、P2P 应用程序的工作方式以及可用于 Java 的各种互操作性技术。
在下一章中,我们将首先详细检查网络,尤其是网络寻址。