1.md 40.2 KB
Newer Older
W
wizardforcel 已提交
1
# 一、网络编程入门
W
wizardforcel 已提交
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

访问网络(特别是互联网)正成为应用程序的一个重要且经常是必要的功能。应用程序经常需要访问和提供服务。随着**物联网****物联网**)连接越来越多的设备,了解如何接入网络变得至关重要。

是推动更多网络应用的重要因素,包括更快网络和更大带宽的可用性。这使得传输更大范围的数据成为可能,例如视频流。近年来,无论是针对新服务、更广泛的社交互动还是游戏,我们都看到了连通性的增加。了解如何开发网络应用程序是一项重要的开发技能。

在本章中,我们将介绍网络编程的基础知识:

*   为什么网络很重要
*   Java 提供的支持
*   解决基本网络操作的简单程序
*   基本网络术语
*   一个简单的服务器/客户端应用程序
*   使用线程支持服务器

在本书中,您将接触到许多使用旧 Java 技术和新 Java 技术的网络概念、想法、模式和实现策略。网络连接使用套接字在较低级别上进行,使用多种协议在更高级别上进行。通信可以是同步的,需要仔细协调请求和响应,也可以是异步的,在提交响应之前执行其他活动。

这些概念和其他概念通过一系列章节加以阐述,每个章节都侧重于一个特定的主题。各章通过尽可能详细阐述先前介绍的概念相互补充。尽可能使用大量代码示例来加深您对主题的理解。

访问服务的核心是知道或发现其地址。该地址可以是人类可读的,例如[www.packtpub.com](http://www.packtpub.com),或者以**IP**地址的形式,例如`83.166.169.231`**互联网协议****IP**)是一种用于访问互联网上信息的低级寻址方案。寻址长期以来一直使用 IPv4 来访问资源。然而,这些地址几乎都不见了。较新的 IPv6 可提供更大范围的地址。[第 2 章](2.html "Chapter 2. Network Addressing")*网络寻址*的重点是网络寻址的基础知识以及如何在 Java 中进行管理。

网络通信的目的是在其他应用程序之间传输信息。这通过使用缓冲区和通道来实现。缓冲区暂时保存信息,直到应用程序可以处理它为止。通道是简化应用程序之间通信的抽象。NIO 和 NIO.2 包提供了对缓冲区和通道的大部分支持。我们将在[第 3 章](3.html "Chapter 3. NIO Support for Networking")*网络 NIO 支持*中探讨这些技术以及其他技术,如阻塞和非阻塞 IO。

W
wizardforcel 已提交
24
服务由服务器提供。这方面的一个例子是简单的 echo 服务器,它重新传输发送的内容。更复杂的服务器,如 HTTP 服务器,可以支持广泛的服务以满足广泛的需求。客户端/服务器模型及其 Java 支持见[第 3 章](3.html "Chapter 3. NIO Support for Networking")*网络 NIO 支持*
W
wizardforcel 已提交
25

W
wizardforcel 已提交
26
另一种服务模式是**对等****P2P**模式)。在这种体系结构中,没有中央服务器,而是一个应用程序网络,通过通信提供服务。该模型由应用程序表示,如 BitTorrent、Skype 和 BBC 的 iPlayer。虽然开发这些类型的应用程序所需的许多支持超出了本书的范围,[第 4 章](4.html "Chapter 4. Client/Server Development")*客户端/服务器开发*探讨了 P2P 问题以及 Java 和 JXTA 提供的支持。
W
wizardforcel 已提交
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

IP 在较低级别上用于通过网络发送和接收信息包。我们还将演示**用户数据报协议****UDP****传输控制协议****TCP**通信协议的使用。这些协议是在 IP 之上分层的。UDP 用于广播短数据包或消息,但不保证可靠传递。TCP 比 UDP 更常用,并提供更高级别的服务。我们将在[第 5 章](5.html "Chapter 5. Peer-to-Peer Networks")*对等网络*中介绍这些相关技术的使用。

由于许多因素,服务通常会面临不同程度的需求。它的负载可能随一天中的时间而变化。随着它越来越受欢迎,其总体需求也将增加。服务器需要扩展以满足负载的增减。线程和线程池被用来支持这项工作。这些技术和其他技术是[第 6 章](6.html "Chapter 6. UDP and Multicasting")*UDP 和多播*的重点。

应用程序越来越需要防止黑客的攻击。当它连接到网络时,这种威胁会增加。在[第 7 章](7.html "Chapter 7. Network Scalability")*网络可扩展性*中,我们将探讨许多可用于支持安全 Java 应用程序的技术。其中包括**安全套接字级别****SSL**),以及 Java 如何支持它。

应用程序很少单独工作。因此,他们需要使用网络访问其他应用程序。然而,并非所有的应用程序都是用 Java 编写的。与这些应用程序联网可能会带来一些特殊问题,从数据类型的字节如何组织到应用程序支持的接口。使用专用协议(如 HTTP 和 WSDL)是很常见的。本书的最后一章从 Java 的角度研究了这些问题。

我们将演示旧的和新的 Java 技术。为了维护较旧的代码,了解较旧的技术可能是必要的,它可以让我们深入了解为什么要开发较新的技术。我们还将使用许多 Java8 函数编程技术来补充我们的示例。通过使用 Java8 示例以及 Java8 之前的实现,我们可以学习如何使用 Java8,并更好地了解何时可以使用 Java8 以及何时应该使用 Java8。

本文并不打算全面解释较新的 Java8 技术,例如 lambda 表达式和流。但是,使用 Java8 示例将深入了解如何使用它们来支持网络应用程序。

本章的其余部分涉及本书中探讨的许多网络技术。我们将向您介绍这些技术的基础知识,您会发现它们很容易理解。然而,在一些地方,时间不允许我们充分探索和解释这些概念。这些问题将在后续章节中讨论。因此,让我们从网络寻址开始探索。

# 使用 InetAddress 类进行网络寻址

IP 地址由`InetAddress`类表示。地址可以是单播,用于标识特定地址,也可以是多播,用于将消息发送到多个地址。

`InetAddress`类没有公共构造函数。要获取实例,请使用几个静态 get-type 方法之一。例如,`getByName`方法采用表示地址的字符串,如下所示。本例中的字符串为**统一资源定位器****URL**

```java
    InetAddress address = 
        InetAddress.getByName("www.packtpub.com");
    System.out.println(address);
```

### 提示

**下载示例代码**

您可以下载您在[账户购买的所有 Packt 书籍的示例代码文件 http://www.packtpub.com](http://www.packtpub.com) 。如果您在其他地方购买了本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support) 并注册,将文件直接通过电子邮件发送给您。

这将显示以下结果:

**www.packtpub.com/83.166.169.231**

附加在名称末尾的数字是 IP 地址。此地址唯一标识 Internet 上的实体。

如果我们需要关于地址的其他信息,我们可以使用以下几种方法之一,如下所示:

```java
    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`方法,如下所示。它的参数指定在决定无法到达地址之前要等待多长时间。参数是等待的毫秒数:

```java
    address.isReachable(10000);
```

还有分别支持 IPv4 和 IPv6 地址的`Inet4Address``Inet6Address`类。我们将在[第 2 章](2.html "Chapter 2. Network Addressing")*网络寻址*中解释它们的用法。

一旦我们获得了一个地址,我们就可以使用它来支持网络访问,比如服务器。在本文中演示它的使用之前,让我们先看看如何从连接中获取和处理数据。

# NIO 支持

`java.io``java.nio``java.nio`子包为 IO 处理提供了大部分 Java 支持。我们将在[第 3 章](3.html "Chapter 3. NIO Support for Networking")*NIO 网络支持*中检查这些包对网络访问的支持。在这里,我们将重点介绍`java.nio`套餐的基本方面。

NIO 包中使用了三个关键概念:

*   **通道**:表示应用程序之间的数据流
*   **缓冲区**:与一起工作,通过一个通道处理数据
*   **选择器**:这是一种允许单个线程处理多个通道的技术

通道和缓冲区通常相互关联。数据可以从通道传输到缓冲器,也可以从缓冲器传输到通道。顾名思义,缓冲区是信息的临时存储库。选择器在支持应用程序可伸缩性方面很有用,这将在[第 7 章](7.html "Chapter 7. Network Scalability")*网络可伸缩性*中讨论。

有四个主通道:

*   `FileChannel`:这对文件有效
*   `DatagramChannel`:此支持 UDP 通信
*   `SocketChannel`:用于 TCP 客户端
*   `ServerSocketChannel`:这是与 TCP 服务器一起使用的

有几个缓冲区类支持基本数据类型,例如 character、integer 和 float。

## 使用 URLConnection 类

访问服务器的一种简单方法是使用`URLConnection`类。此类表示应用程序和`URL`实例之间的连接。`URL`实例表示 Internet 上的一个资源。

在下一个示例中,将为 Google 网站创建一个 URL 实例。使用`URL`类的`openConnection`方法创建`URLConnection`实例。`BufferedReader`实例用于从连接中读取线路,然后显示:

```java
    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 类

我们可以修改前面的示例来说明通道和缓冲区的使用。`URLConnection`实例与前面一样创建。我们将创建一个`ReadableByteChannel`实例,然后创建一个`ByteBuffer`实例,如下一个示例所示。`ReadableByteChannel`实例允许我们使用`read`方法读取站点。一个`ByteBuffer`实例从通道接收数据,并用作`read`方法的参数。创建的缓冲区一次可容纳 64 个字节。

`read`方法返回读取的字节数。`ByteBuffer`类的`array`方法返回一个字节数组,用作`String`类构造函数的参数。这用于显示读取的数据。`clear`方法用于复位缓冲器,以便再次使用:

```java
    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`类及其派生类为提供了一种改进的技术来访问网络上发现的数据,而不是旧技术提供的数据。我们将看到更多这门课。

W
wizardforcel 已提交
175
# 客户端/服务器架构
W
wizardforcel 已提交
176

W
wizardforcel 已提交
177
有几种使用 Java 创建服务器的方法。我们将演示一些简单的方法,并将这些技术的详细讨论推迟到[第 4 章](4.html "Chapter 4. Client/Server Development")*客户端/服务器开发*之后。将同时创建客户端和服务器。
W
wizardforcel 已提交
178 179 180 181 182

在具有 IP 地址的机器上安装了服务器。在任何给定时间,一台机器上都可能运行多台服务器。当操作系统收到机器上的服务请求时,它还将收到端口号。端口号将标识请求应转发到的服务器。因此,服务器由其 IP 地址和端口号的组合来标识。

通常,客户端会向服务器发出请求。服务器将接收请求并发送回响应。请求/响应的性质以及用于通信的协议取决于客户端/服务器。有时会使用一个记录良好的协议,例如**超文本传输协议****HTTP**)。对于更简单的体系结构,会来回发送一系列文本消息。

W
wizardforcel 已提交
183
为了使服务器与发出请求的应用程序通信,使用专用软件发送和接收消息。这个软件叫做套接字。一个套接字位于客户端,另一个套接字位于服务器端。当他们连接时,通信是可能的。有几种不同类型的插座。这些包括数据报套接字;流套接字,经常使用 TCP;和原始套接字,它们通常在 IP 级别工作。我们将重点介绍客户端/服务器应用程序的 TCP 套接字。
W
wizardforcel 已提交
184

W
wizardforcel 已提交
185
具体来说,我们将创建一个简单的 echo 服务器。此服务器将从客户端接收文本消息,并立即将其发送回该客户端。该服务器的简单性使我们能够专注于客户端-服务器基础。
W
wizardforcel 已提交
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

# 创建一个简单的 echo 服务器

我们将从开始定义`SimpleEchoServer`类,如下所示。在`main`方法中,将显示初始服务器消息:

```java
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`类实例,该实例表示该客户端和服务器之间的连接。他们现在可以发送和接收消息:

```java
    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`方法创建的,如下所示:

```java
    try (BufferedReader br = new BufferedReader(
                new InputStreamReader(
                clientSocket.getInputStream()));
            PrintWriter out = new PrintWriter(
                clientSocket.getOutputStream(), true)) {
        ...
        }
    }
```

`PrintWriter`构造函数的第二个参数设置为`true`。这意味着使用`out`对象发送的文本将在每次使用后自动刷新。

当文本写入套接字时,它将位于缓冲区中,直到缓冲区已满或调用刷新方法为止。执行自动刷新可以避免我们必须记住刷新缓冲区,但这可能会导致过度刷新,而在执行最后一次写入后发出的单个刷新也可以。

下一个代码段完成了服务器。`readLine`方法从客户端一次读取一行。显示此文本,然后使用`out`对象将其发送回客户端:

```java
    String inputLine;
    while ((inputLine = br.readLine()) != null) {
        System.out.println("Server: " + inputLine);
        out.println(inputLine);
    }
```

在演示服务器的实际操作之前,我们需要创建一个客户端应用程序来使用它。

## 创建一个简单的 echo 客户端

我们从一个`SimpleEchoClient`类的声明开始,在`main`方法中,会显示一条消息,指示应用程序的启动,如下所示:

```java
public class SimpleEchoClient {
    public static void main(String args[]) {
        System.out.println("Simple Echo Client");
        ...
    }
}
```

需要创建一个`Socket`实例来连接服务器。在下面的示例中,假定服务器和客户端在同一台机器上运行。`InetAddress`类的静态`getLocalHost`方法返回此地址,然后与端口`6000`一起在`Socket`类的构造函数中使用。如果它们位于不同的机器上,则需要使用服务器的地址。与服务器一样,创建了`PrintWriter``BufferedReader`类的实例,以允许向服务器发送文本和从服务器发送文本:

```java
    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`对象发送到服务器。返回回复后,将显示如下所示:

```java
    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);
    }
```

W
wizardforcel 已提交
298
这些计划可以作为两个单独的项目或在单个项目中实施。无论哪种方式,首先启动服务器,然后启动客户端。服务器启动时,将显示以下内容:
W
wizardforcel 已提交
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337

**简单回音服务器**

**正在等待连接。。。。。**

当客户端启动时,您将看到以下内容:

**简单回音客户端**

**正在等待连接。。。。。**

**已连接到服务器**

**输入文本:**

输入消息,并观察客户端和服务器如何交互。从客户的角度来看,以下是一系列可能的输入:

**输入文本:你好服务器**

**服务器响应:你好服务器**

**输入文字:回显!**

**服务器响应:回应!**

**输入文本:退出**

客户端输入`quit`命令后,服务器的输出如图所示:

**简单回音服务器**

**正在等待连接。。。。。**

**已连接到客户端**

**客户端请求:你好服务器**

**客户请求:回显!**

W
wizardforcel 已提交
338
这是实现客户端和服务器的一种方法。我们将在后面的章节中增强此实现。
W
wizardforcel 已提交
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475

## 使用 Java 8 支持 echo 服务器和客户端

在本书中,我们将提供使用许多较新的 Java8 特性的示例。这里,我们将向您展示以前 echo 服务器和客户端应用程序的替代实现。

服务器使用 while 循环处理客户端的请求,如下所示:

```java
    String inputLine;
    while ((inputLine = br.readLine()) != null) {
        System.out.println("Client request: " + inputLine);
        out.println(inputLine);
    }
```

我们可以将`Supplier`接口与`Stream`对象结合使用来执行相同的操作。下一条语句使用 lambda 表达式从客户端返回字符串:

```java
    Supplier<String> socketInput = () -> {
        try {
            return br.readLine();
        } catch (IOException ex) {
            return null;
        }
    };
```

`Supplier`实例生成无限流。下面的`map`方法从用户获取输入,然后将其发送到服务器。当输入`quit`时,流将终止。`allMatch`方法是一种短路方法,当其参数计算为`false`时,流终止:

```java
    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 循环:

```java
    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`实例捕获控制台输入,如下所示:

```java
    Supplier<String> scannerInput = () -> scanner.next();
```

生成无限流,如下图所示,使用`map`方法提供等效功能:

```java
    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 服务器和一个或多个 UDP 客户端。为了演示此功能,我们将创建一个简单的时间服务器。服务器将每秒向客户端发送一个日期和时间字符串。

多播将向组中的每个成员发送相同的消息。组由多播地址标识。多播地址必须使用以下 IP 地址范围:`224.0.0.0``239.255.255.255`。服务器将发送带有此地址的消息标记。客户端必须加入组才能接收任何多播消息。

## 创建多播服务器

接下来声明一个`MulticastServer`类,在这里创建一个`DatagramSocket`实例。try-catch 块将在异常发生时处理异常:

```java
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`方法的参数。然后显示发送的数据和时间。然后服务器暂停一秒钟:

```java
    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
        }
    }
```

此服务器仅广播消息。它从不从客户端接收消息。

## 创建多播客户端

W
wizardforcel 已提交
476
客户端是使用以下`MulticastClient`类创建的。为了接收消息,客户端必须使用相同的组地址和端口号。在接收消息之前,它必须使用`joinGroup`方法加入组。在这个实现中,它接收 5 条日期和时间消息,显示它们,然后终止。`trim`方法从字符串中删除前导空格和尾随空格。否则,将显示消息的所有 256 字节:
W
wizardforcel 已提交
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863

```java
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 章](6.html "Chapter 6. UDP and Multicasting")*UDP 和多播*中探讨。

# 可伸缩性

当服务器上的需求增加或减少时,最好更改专用于服务器的资源。可用的选项范围从使用手动线程来允许并发行为到嵌入在专用类中以处理线程池和 NIO 通道。

## 创建线程服务器

在本节中,我们将使用线程来扩充我们的简单 echo 服务器。`ThreadedEchoServer`类的定义如下。它实现了`Runnable`接口,为每个连接创建一个新线程。private`Socket`变量将保存特定线程的客户端套接字:

```java
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`方法像以前一样创建服务器套接字,但是当创建客户端套接字时,客户端套接字用于创建线程,如下所示:

```java
    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 服务器的实现相同,只是显示当前线程以澄清正在使用的线程:

```java
    @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 章](7.html "Chapter 7. Network Scalability")*网络可扩展性*中讨论这些问题。

# 安全

安全是一个复杂的话题。在本节中,我们将演示本主题与网络相关的几个简单方面。具体来说,我们将创建一个安全的 echo 服务器。创建安全的 echo 服务器与我们先前开发的非安全 echo 服务器没有太大区别。然而,在幕后还有很多事情要做。我们现在可以忽略这些细节,但我们将在[第 8 章](8.html "Chapter 8. Network Security")*网络安全*中更深入地探讨。

我们将使用`SSLServerSocketFactory`类来实例化安全服务器套接字。此外,有必要创建底层 SSL 机制可用于加密通信的密钥。

## 创建 SSL 服务器

在下面的示例中,`SSLServerSocket`类被声明作为 echo 服务器。由于它与之前的 echo 服务器类似,我们将不解释它的实现,除了它与使用`SSLServerSocketFactory`类的关系。它的静态`getDefault`方法返回一个`ServerSocketFactory`实例。其`createServerSocket`方法返回绑定到端口`8000``ServerSocket`实例,该端口能够支持安全通信。否则,它的组织和功能与以前的 echo 服务器类似:

```java
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
        }
    }
}
```

## 创建 SSL 客户端

安全 echo 客户端也类似于之前的非安全 echo 客户端。`SSLSocketFactory`类“`getDefault`返回一个`SSLSocketFactory`实例,该实例的`createSocket`创建了一个连接到安全 echo 服务器的套接字。申请如下:

```java
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](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/keytool.1.html)

您还需要使用类似于以下命令的命令设置`bin`目录的路径。查找并执行`keytool`应用程序需要此命令:

```java
 set path= C:\Program Files\Java\jdk1.8.0_25\bin;%path%

```

接下来,输入`keytool`命令。系统将提示您输入用于创建密钥的密码和其他信息。此处显示了此过程,其中使用了`123456`密码,但在输入时未显示该密码:

```java
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
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 章](8.html "Chapter 8. Network Security")*网络安全*

# 总结

网络应用在当今社会中扮演着越来越重要的角色。随着越来越多的设备连接到 Internet,了解如何构建能够与其他应用程序通信的应用程序非常重要。

W
wizardforcel 已提交
864
我们简要地识别并解释了 Java 用于连接网络的几种技术。我们演示了`InetAddress`类如何表示 IP 地址,并在几个示例中使用了该类。使用 UDP、TCP 和 SSL 技术演示了客户端/服务器体系结构的基本元素。它们提供不同类型的支持。UDP 速度快,但不如 TCP 可靠或功能强大。TCP 是一种可靠且方便的通信方式,但除非与 SSL 一起使用,否则不安全。
W
wizardforcel 已提交
865

W
wizardforcel 已提交
866
说明了 NIO 对缓冲区和通道的支持。这些技术可以提高通信效率。应用程序的可伸缩性对于许多应用程序来说是至关重要的,特别是客户端/服务器模型。我们还了解了线程如何支持可伸缩性。
W
wizardforcel 已提交
867 868 869 870

这些主题中的每一个都将在后面的章节中进行更详细的讨论。这包括 NIO 对可伸缩性的支持、P2P 应用程序的工作方式以及可用于 Java 的各种互操作性技术。

在下一章中,我们将首先详细检查网络,尤其是网络寻址。