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 中,使用下面的代码即可:
URL url = new URL("http://www.jclarity.com/");try (InputStream in = url.openStream()) {Files.copy(in, Paths.get("output.txt"));} catch(IOException ex) {ex.printStackTrace();}
若想深入低层控制,例如获取请求和响应的元数据,可以使用 URLConnection 类,编写如下代码:
try {URLConnection conn = url.openConnection();String type = conn.getContentType();String encoding = conn.getContentEncoding();Date lastModified = new Date(conn.getLastModified());int len = conn.getContentLength();InputStream in = conn.getInputStream();} catch (IOException e) {// 处理异常}
HTTP 定义了多个“请求方法”,客户端使用这些方法操作远程资源。这些方法是:
GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE
各个方法的用法稍微不同,例如:
GET 只能用于取回文档,不能执行任何副作用;
HEAD 和 GET 作用一样,但是不返回主体——如果程序只想检查 URL 对应的网页是否有变化,可以使用 HEAD;
如果想把数据发送给服务器处理,要使用 POST。
默认情况下,Java 始终使用 GET 方法,不过也提供了使用其他方法的方式,用于开发更复杂的应用。然而,这需要做一些额外工作。在下面这个示例中,我们使用 BBC 网站提供的搜索功能搜索关于 Java 的新闻:
URL url = new URL("http://www.bbc.co.uk/search");String rawData = "q=java";String encodedData = URLEncoder.encode(rawData, "ASCII");String contentType = "application/x-www-form-urlencoded";HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setInstanceFollowRedirects(false);conn.setRequestMethod("POST");conn.setRequestProperty("Content-Type", contentType );conn.setRequestProperty("Content-Length",String.valueOf(encodedData.length()));conn.setDoOutput(true);OutputStream os = conn.getOutputStream();os.write( encodedData.getBytes() );int response = conn.getResponseCode();if (response == HttpURLConnection.HTTP_MOVED_PERM|| response == HttpURLConnection.HTTP_MOVED_TEMP) {System.out.println("Moved to: "+ conn.getHeaderField("Location"));} else {try (InputStream in = conn.getInputStream()) {Files.copy(in, Paths.get("bbc.txt"),StandardCopyOption.REPLACE_EXISTING);}}
注意,请求参数要在请求的主体中发送,而且发送前要编码。我们还要禁止跟踪 HTTP 重定向,手动处理服务器返回的每个重定向响应。这是因为 HttpURLConnection 类有个缺陷,不能正确处理 POST 请求的重定向响应。
开发这种高级 HTTP 应用时,多数情况下开发者一般都会使用专门的 HTTP 客户端库,例如 Apache 提供的那个库,而不会使用 JDK 提供的类从零编写所有代码。
下面介绍网络协议栈的下一层,传输控制协议(Transmission Control Protocol,TCP)。
10.5.2 TCP
TCP 是互联网中可靠传输网络数据的基础,确保传输的网页和其他互联网流量完整且易于理解。从网络理论的视角来看,由于 TCP 具有下述特性,才能作为互联网流量的“可靠性层”。
- 基于连接
数据属于单个逻辑流(连接)。
- 保证送达
如果未收到数据包,会一直重新发送,直到送达为止。
- 错误检查
能检测到网络传输导致的损坏,并自动修复。
TCP 是双向通信通道,使用特殊的编号机制(TCP 序号)为数据块指定序号,确保通信流的两端保持同步。为了在同一个网络主机中支持多个不同的服务,TCP 使用端口号识别服务,而且能确保某个端口的流量不会走另一个端口传输。
Java 使用 Socket 和 ServerSocket 类表示 TCP。这两个类分别表示连接中的客户端和服务器端。也就是说,Java 既能连接网络服务,也能用来实现新服务。
举个例子,我们来重新实现 HTTP。这个协议基于文本,相对简单。连接的两端都要实现,下面先基于 TCP 套接字实现 HTTP 客户端。为此,其实我们需要实现 HTTP 协议的细节,不过我们有个优势——完全掌控着 TCP 套接字。
我们既要从客户端套接字中读取数据,也要把数据写入客户端套接字,而且构建请求时要遵守 HTTP 标准(RFC 2616)。最终写出的代码如下所示:
String hostname = "www.example.com";int port = 80;String filename = "/index.html";try (Socket sock = new Socket(hostname, port);BufferedReader from = new BufferedReader(new InputStreamReader(sock.getInputStream()));PrintWriter to = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); ) {// HTTP协议to.print("GET " + filename +" HTTP/1.1\r\nHost: "+ hostname +"\r\n\r\n");to.flush();for(String l = null; (l = from.readLine()) != null; )System.out.println(l);}
在服务器端,可能需要处理多个连入连接。因此,需要编写一个服务器主循环,然后使用 accept() 方法从操作系统中接收一个新连接。随后,要迅速把这个新连接传给单独的类处理,好让服务器主循环继续监听新连接。服务器端的代码比客户端复杂:
// 处理连接的类private static class HttpHandler implements Runnable {private final Socket sock;HttpHandler(Socket client) { this.sock = client; }public void run() {try (BufferedReader in =new BufferedReader(new InputStreamReader(sock.getInputStream()));PrintWriter out =new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); ) {out.print("HTTP/1.0 200\r\nContent-Type: text/plain\r\n\r\n");String line;while((line = in.readLine()) != null) {if (line.length() == 0) break;out.println(line);}} catch(Exception e) {// 处理异常}}}// 服务器主循环public static void main(String[] args) {try {int port = Integer.parseInt(args[0]);ServerSocket ss = new ServerSocket(port);for(;;) {Socket client = ss.accept();HTTPHandler hndlr = new HTTPHandler(client);new Thread(hndlr).start();}} catch (Exception e) {// 处理异常}}
为通过 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 已经对这种新寻址方案提供了良好支持。
