10.5 网络

Java 平台支持大量标准的网络协议,因此编写简单的网络应用非常容易。Java 对网络支持的核心 API 在 java.net 包中,其他扩展 API 则由 javax.net 包(尤其是 javax.net.ssl 包)提供。

开发应用时最易于使用的协议是超文本传输协议(HyperText Transmission Protocol,HTTP),这个协议是 Web 的基础通信协议。

10.5.1 HTTP

HTTP 是 Java 原生支持的最高层网络协议。这个协议非常简单,基于文本,在 TCP/IP 标准协议族的基础上实现。HTTP 可以在任何网络端口中使用,不过通常使用 80 端口。

URL 是关键的类——这个类原生支持 http://ftp://file://https:// 形式的 URL。这个类使用起来非常简单,最简单的示例是下载指定 URL 对应页面的内容。在 Java 8 中,使用下面的代码即可:

  1. URL url = new URL("http://www.jclarity.com/");
  2. try (InputStream in = url.openStream()) {
  3. Files.copy(in, Paths.get("output.txt"));
  4. } catch(IOException ex) {
  5. ex.printStackTrace();
  6. }

若想深入低层控制,例如获取请求和响应的元数据,可以使用 URLConnection 类,编写如下代码:

  1. try {
  2. URLConnection conn = url.openConnection();
  3. String type = conn.getContentType();
  4. String encoding = conn.getContentEncoding();
  5. Date lastModified = new Date(conn.getLastModified());
  6. int len = conn.getContentLength();
  7. InputStream in = conn.getInputStream();
  8. } catch (IOException e) {
  9. // 处理异常
  10. }

HTTP 定义了多个“请求方法”,客户端使用这些方法操作远程资源。这些方法是:

GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE

各个方法的用法稍微不同,例如:

  • GET 只能用于取回文档,不能执行任何副作用;

  • HEAD 和 GET 作用一样,但是不返回主体——如果程序只想检查 URL 对应的网页是否有变化,可以使用 HEAD;

  • 如果想把数据发送给服务器处理,要使用 POST。

默认情况下,Java 始终使用 GET 方法,不过也提供了使用其他方法的方式,用于开发更复杂的应用。然而,这需要做一些额外工作。在下面这个示例中,我们使用 BBC 网站提供的搜索功能搜索关于 Java 的新闻:

  1. URL url = new URL("http://www.bbc.co.uk/search");
  2. String rawData = "q=java";
  3. String encodedData = URLEncoder.encode(rawData, "ASCII");
  4. String contentType = "application/x-www-form-urlencoded";
  5. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  6. conn.setInstanceFollowRedirects(false);
  7. conn.setRequestMethod("POST");
  8. conn.setRequestProperty("Content-Type", contentType );
  9. conn.setRequestProperty("Content-Length",
  10. String.valueOf(encodedData.length()));
  11. conn.setDoOutput(true);
  12. OutputStream os = conn.getOutputStream();
  13. os.write( encodedData.getBytes() );
  14. int response = conn.getResponseCode();
  15. if (response == HttpURLConnection.HTTP_MOVED_PERM
  16. || response == HttpURLConnection.HTTP_MOVED_TEMP) {
  17. System.out.println("Moved to: "+ conn.getHeaderField("Location"));
  18. } else {
  19. try (InputStream in = conn.getInputStream()) {
  20. Files.copy(in, Paths.get("bbc.txt"),
  21. StandardCopyOption.REPLACE_EXISTING);
  22. }
  23. }

注意,请求参数要在请求的主体中发送,而且发送前要编码。我们还要禁止跟踪 HTTP 重定向,手动处理服务器返回的每个重定向响应。这是因为 HttpURLConnection 类有个缺陷,不能正确处理 POST 请求的重定向响应。

开发这种高级 HTTP 应用时,多数情况下开发者一般都会使用专门的 HTTP 客户端库,例如 Apache 提供的那个库,而不会使用 JDK 提供的类从零编写所有代码。

下面介绍网络协议栈的下一层,传输控制协议(Transmission Control Protocol,TCP)。

10.5.2 TCP

TCP 是互联网中可靠传输网络数据的基础,确保传输的网页和其他互联网流量完整且易于理解。从网络理论的视角来看,由于 TCP 具有下述特性,才能作为互联网流量的“可靠性层”。

  • 基于连接

数据属于单个逻辑流(连接)。

  • 保证送达

如果未收到数据包,会一直重新发送,直到送达为止。

  • 错误检查

能检测到网络传输导致的损坏,并自动修复。

TCP 是双向通信通道,使用特殊的编号机制(TCP 序号)为数据块指定序号,确保通信流的两端保持同步。为了在同一个网络主机中支持多个不同的服务,TCP 使用端口号识别服务,而且能确保某个端口的流量不会走另一个端口传输。

Java 使用 SocketServerSocket 类表示 TCP。这两个类分别表示连接中的客户端和服务器端。也就是说,Java 既能连接网络服务,也能用来实现新服务。

举个例子,我们来重新实现 HTTP。这个协议基于文本,相对简单。连接的两端都要实现,下面先基于 TCP 套接字实现 HTTP 客户端。为此,其实我们需要实现 HTTP 协议的细节,不过我们有个优势——完全掌控着 TCP 套接字。

我们既要从客户端套接字中读取数据,也要把数据写入客户端套接字,而且构建请求时要遵守 HTTP 标准(RFC 2616)。最终写出的代码如下所示:

  1. String hostname = "www.example.com";
  2. int port = 80;
  3. String filename = "/index.html";
  4. try (Socket sock = new Socket(hostname, port);
  5. BufferedReader from = new BufferedReader(
  6. new InputStreamReader(sock.getInputStream()));
  7. PrintWriter to = new PrintWriter(
  8. new OutputStreamWriter(sock.getOutputStream())); ) {
  9. // HTTP协议
  10. to.print("GET " + filename +
  11. " HTTP/1.1\r\nHost: "+ hostname +"\r\n\r\n");
  12. to.flush();
  13. for(String l = null; (l = from.readLine()) != null; )
  14. System.out.println(l);
  15. }

在服务器端,可能需要处理多个连入连接。因此,需要编写一个服务器主循环,然后使用 accept() 方法从操作系统中接收一个新连接。随后,要迅速把这个新连接传给单独的类处理,好让服务器主循环继续监听新连接。服务器端的代码比客户端复杂:

  1. // 处理连接的类
  2. private static class HttpHandler implements Runnable {
  3. private final Socket sock;
  4. HttpHandler(Socket client) { this.sock = client; }
  5. public void run() {
  6. try (BufferedReader in =
  7. new BufferedReader(
  8. new InputStreamReader(sock.getInputStream()));
  9. PrintWriter out =
  10. new PrintWriter(
  11. new OutputStreamWriter(sock.getOutputStream())); ) {
  12. out.print("HTTP/1.0 200\r\nContent-Type: text/plain\r\n\r\n");
  13. String line;
  14. while((line = in.readLine()) != null) {
  15. if (line.length() == 0) break;
  16. out.println(line);
  17. }
  18. } catch(Exception e) {
  19. // 处理异常
  20. }
  21. }
  22. }
  23. // 服务器主循环
  24. public static void main(String[] args) {
  25. try {
  26. int port = Integer.parseInt(args[0]);
  27. ServerSocket ss = new ServerSocket(port);
  28. for(;;) {
  29. Socket client = ss.accept();
  30. HTTPHandler hndlr = new HTTPHandler(client);
  31. new Thread(hndlr).start();
  32. }
  33. } catch (Exception e) {
  34. // 处理异常
  35. }
  36. }

为通过 TCP 通信的应用设计协议时,要谨记一个简单而意义深远的网络架构原则——Postel 法则(以互联网之父之一 Jon Postel 的名字命名)。这个法则有时表述为:“发送时要保守,接收时要开放。”这个简单的原则表明,网络系统中的通信有太多可能性,即便非常不完善的实现也是如此。

如果开发者遵守 Postel 法则,还遵守尽量保持协议简单这个通用原则(有时也叫 KISS 原则 1),那么,基于 TCP 的通信实现起来要比不遵守时更简单。

1KISS 是“Keep it simple, stupid”的简称。——译者注

TCP 下面是互联网通用的运输协议——互联网协议(Internet Protocol,IP)。

10.5.3 IP

IP 是传输数据的最低层标准,抽象了把字节从 A 设备移动到 B 设备的物理网络技术。

和 TCP 不同,IP 数据包不能保证一定送达,在传输的路径中,任何过载的系统都可能会丢掉数据包。IP 数据包有目的地,但一般没有路由数据——真正传送数据的是沿线的物理传输介质(可能有多种不同的介质)。

在 Java 中可以创建基于单个 IP 数据包(首部除了可以指定使用 TCP 协议,还可以指定使用 UDP2 协议)的数据报服务,不过,除了延迟非常低的应用之外很少需要这么做。Java 使用 DatagramSocket 类实现这种功能,不过很少有开发者需要深入到网络协议栈的这一层。

2UDP 是 User Datagram Protocol 的简称。——译者注

最后,值得注意的是,互联网使用的寻址方案目前正在经历一些变化。目前使用的 IP 版本是 IPv4,可用的网络地址有 32 位存储空间。现在,这个空间严重不足,因此已经开始部署多种缓解技术。

IP 的下一版(IPv6)已经出现,但还没广泛使用。不过,在未来十年,IPv6 应该会更为普及。令人欣慰的是,Java 已经对这种新寻址方案提供了良好支持。