本文目的是大概了解 Java 网络编程体系,需要一点点 Java IO 基础,推荐教程 系统学习 Java IO。主要参考 JavaDoc 和 Jakob Jenkov 的英文教程《Java Networking》 http://tutorials.jenkov.com/java-networking/index.html
Java 网络编程概览
Java 有一个相当容易使用的内置网络 API,可以很容易地通过互联网上的 TCP / IP 套接字或 UDP 套接字进行通信。 TCP 通常比 UDP 使用得更频繁。
即使 Java Networking API 允许通过套接字打开和关闭网络连接,但所有通信都通过 Java IO 类 InputStream 和 OutputStream 实现的。
或者,我们可以使用 Java NIO API 中的网络类。 用法类似于 Java Networking API 中的类,但 Java NIO API 可以在非阻塞模式下工作。 在某些情况下,非阻塞模式可提升性能。
Java TCP 网络基础
通常,客户端会打开与服务器的 TCP / IP 连接,然后开始与服务器通信,当通信结束后客户端关闭连接。如下图:
客户端可以通过一个已打开的连接发送多个请求,实际上,客户端可以向服务器发送尽可能多的数据。 当然,如果需要,服务器也可以关闭连接。
Java 中 Socket 类和 ServerSocket 类
当客户端想要打开到服务器的 TCP / IP 连接时,它使用 Java Socket 类来实现。 套接字被告知连接到哪个 IP 地址和 TCP 端口,其余部分由 Java 完成。
如果要启动服务器以侦听来自某个 TCP 端口上的客户端的传入连接,则必须使用 Java ServerSocket 类。 当客户端通过客户端套接字连接到服务器的 ServerSocket 时,服务器上会为该连接分配一个 Socket 。 客户端和服务器的通信就是 Socket 到 Socket 的通信了。
Socket和ServerSocket在后面的文本中有更详细的介绍。
Java UDP 网络基础
UDP 的工作方式与 TCP 略有不同。 使用 UDP ,客户端和服务器之间没有连接。 客户端可以向服务器发送数据,并且服务器可以(或可以不)接收该数据。 客户端永远不会知道数据是否在另一端收到。 从服务器到客户端发送的数据也是如此。
由于无法保证数据传输,因此 UDP 协议的协议开销较小。
在一些情况下,无连接 UDP 模型优于 TCP ,比如传输视频等多媒体文件,缺少一些数据是不影响观看的。
TCP Socket(套接字)
为了通过 Internet 连接到服务器(通过TCP / IP),需要创建一个 Socket 并将其连接到服务器。 或者,如果您更喜欢使用 Java NIO ,则可以使用 Java NIO SocketChannel 。
创建一个Socket
Socket socket = new Socket("baidu.com", 80);
第一个参数是地址,可以是 ip 或者域名字符串,第二个参数是端口,端口80是Web服务器端口。
写入 Socket
要写入 Socket,必须获取其 OutputStream :
Socket socket = new Socket("baidu.com", 80);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("some data".getBytes());
outputStream.flush();
outputStream.close();
socket.close();
当真的希望通过互联网向服务器发送数据时,不要忘记调用 flush() 。操作系统中的底层 TCP / IP 实现会先缓冲数据,缓冲块的大小是与 TCP / IP 数据包的大小相适应的,这就是说,调用 flush() 只是通知系统发送,但系统并不是立即就帮忙发出去。
从 Socket 读取
要从 Socket 读取,需要获取其 InputStream :
Socket socket = new Socket("baidu.com", 80);
InputStream in = socket.getInputStream();
int data = in.read();
//... read more data...
in.close();
socket.close();
记住,在读取时我们不能使用读取 InputStream 返回 -1 来判断数据读取结束 ,因为只有在服务器关闭连接时才返回 -1 。 但是服务器可能并不总是关闭连接,比如通过同一连接发送多个请求。 在这种情况下,关闭连接将是非常愚蠢的。
相反,必须知道从 Socket 的 InputStream 中读取多少字节。 服务器会告知 Socket 它发送的字节数,或者通过查找特殊的数据结束字符来完成。
使用 Socket 后,必须关闭它以关闭与服务器的连接,这可以通过调用 Socket 对象的 close() 方法完成。
ServerSocket
可以使用 ServerSocket 来实现 Java 服务器,这样就可以通过 TCP / IP 侦听来自客户端的传入连接。如果更喜欢使用 Java NIO 而不是 Java Networking(标准API),那么也可以使用 ServerSocketChannel 。
创建一个 ServerSocket
这是一个简单的代码示例,它创建一个侦听端口 9000 的 ServerSocket:
ServerSocket serverSocket = new ServerSocket(9000);
监听传入的连接
要接受传入连接,必须调用 ServerSocket.accept() 方法。 accept() 方法返回一个 Socket ,其行为类似于普通的 Socket ,示例:
ServerSocket serverSocket = new ServerSocket(9000);
boolean isStopped = false;
while(!isStopped){
Socket clientSocket = serverSocket.accept();
//do something with clientSocket
}
每次调用 accept() 方法时只打开一个传入连接。
此外,只有在运行服务器的线程调用 accept() 时才能接受传入连接。 线程在此方法之外执行的所有时间都没有客户端可以连接。 因此,“accept”线程通常将传入连接(Socket)传递给工作线程池,然后工作线程与客户端进行通信。 有关多线程服务器设计的更多信息,请参阅教程跟踪 Java 多线程服务器。
关闭客户端 Sockets
一旦客户端请求完成,并且不会从该客户端收到进一步的请求,必须关闭该Socket,就像关闭普通客户端Socket一样。调用:socket.close();
关闭服务端 Sockets
一旦服务器关闭,就需要关闭 ServerSocket 。 调用:serverSocket.close();
UDP DatagramSocket(UDP数据报套接字)
DatagramSocket 是 Java 通过 UDP 而不是 TCP 进行网络通信的机制。 UDP 也是 IP 协议的上层。 可以使用 DatagramSocket 来发送和接收 UPD 数据报。
UDP 对比 TCP
通过 TCP 发送数据时,首先要创建连接。 建立 TCP 连接后,TCP 保证数据到达另一端,或者它会告诉你发生了错误。
使用 UDP,只需将数据包(数据报)发送到网络上的某个 IP 地址。 无法保证数据会到达,也无法保证 UDP 数据包到达的顺序。 这意味着 UDP 比 TCP 具有更少的协议开销(没有流完整性检查)。
UDP 适用于数据传输,如果数据包在转换过程中丢失则无关紧要。 例如,想象一下通过互联网传输直播电视信号,如果一两帧丢失,这是无关紧要的。我们更不希望直播延迟只是为了确保所有帧都显示出来。 宁愿跳过错过的帧,并直接查看最新的帧。
还有实时监控视频,宁愿丢失一两帧,也不想延迟于现实 30 秒。与摄像机录像的存储有点不同,将图像从相机录制到磁盘时, 为了保证完整性,可能不希望丢失单帧,而是更愿意稍微延迟。
DatagramPacket 类
此类表示数据报包。数据报包用来实现无连接包投递服务。
Java 使用 DatagramSocket 代表 UDP 协议的 Socket ,DatagramSocket 本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报,使用 DatagramPacket 来代表数据报,DatagramSocket 接收和发送的数据都是通过 DatagramPacket 对象完成的。
每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
引用自 李刚《疯狂Java讲义(第2版)》
其所有构造器如下:
方法 | 描述 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造 DatagramPacket,用来接收长度为 length 的数据包。 |
DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。 |
DatagramPacket(byte[] buf, int length, SocketAddress address) | 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。 |
以上3个在 byte[] buf 参数后面追加 int offset | 为长度为 length 的包设置偏移量为 offset |
其中
- InetAddress 类表示互联网协议 (IP) 地址,可以通过静态方法 getByName(String host) 获得其对象。
- SocketAddress 类里面什么都没有。其子类 InetSocketAddress是(IP地址+端口号)类型,也就是端口地址类型,同样可以使用静态方法 createUnresolved(String host, int port) 获取对象,另外也能由构造函数 InetSocketAddress(InetAddress addr, int port) 创建,其中 InetAddress 对象可省略,也可用字符串代替。
通过 DatagramSocket 发送数据(DatagramPacket )
要通过 DatagramSocket 发送数据,必须首先创建一个 DatagramPacket :
byte[] buffer = new byte[65508];
InetAddress address = InetAddress.getByName("baidu.com");
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, 9000);
字节缓冲区(字节数组)是要在 UDP 数据报中发送的数据。 上述缓冲区的长度(65508字节)是可以在单个 UDP 数据包中发送的最大数据量。
DatagramPacket 构造函数中的 buffer.length 是要发送的缓冲区中数据的长度,忽略该数据量之后缓冲区中的所有数据。
InetAddress 实例包含发送 UDP 数据包的节点(例如服务器)的地址。 InetAddress 类表示 IP 地址(Internet地址)。 getByName() 方法返回一个 InetAddress 实例,其 IP 地址与给定的主机名匹配。
port 参数是服务器接收数据正在侦听的 UDP 端口,UDP 和 TCP 端口是不一样的。同一台计算机可以有不同的线程同时监听 UDP 的 80 端口和 TCP 中的 80 端口。不同协议下,端口号互不干扰,端口只是应用程序的标识。
创建一个 DatagramSocket :
DatagramSocket datagramSocket = new DatagramSocket();
要发送数据,请调用 send() 方法,如下所示:
datagramSocket.send(packet);
这是一个完整的例子:
public class DatagramExample {
public static void main(String[] args) throws Exception {
DatagramSocket datagramSocket = new DatagramSocket();
byte[] buffer = "123456789".getBytes();
InetAddress receiverAddress = InetAddress.getLocalHost();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, receiverAddress, 80);
datagramSocket.send(packet);
}
}
通过 DatagramSocket 接收数据 (DatagramPacket )
通过 DatagramSocket 接收数据是通过首先创建 DatagramPacket 然后通过 DatagramSocket 的 receive() 方法接收数据来完成的。 这是一个例子:
DatagramSocket socket = new DatagramSocket(80);
byte[] buffer = new byte[10];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
使用传递给构造函数的参数值 80 来实例化 DatagramSocket , 此参数是 DatagramSocket 接收 UDP 数据包的 UDP 端口。 如前所述,TCP 和 UDP 端口不相同,因此不重叠。 可以在 TCP 和 UDP 80 端口上侦听两个不同的进程,而不会发生任何冲突。
其次,创建字节缓冲区和 DatagramPacket 。 注意 DatagramPacket 没有关于要发送数据的节点的信息,就像创建 DatagramPacket 用于发送数据时一样。 这是因为我们将使用 DatagramPacket 接收数据而不是发送数据,因此,不需要目标地址。
最后调用 DatagramSocket 的 receive() 方法。 此方法将一直阻塞,直到收到 DatagramPacket 。
收到的数据位于 DatagramPacket 的字节缓冲区中。 这个缓冲区可以通过调用如下代码获取:
byte[] buffer = packet.getData();
缓冲区会接收多少数据应该由你找到答案。 正在使用的协议应指定每个 UDP 数据包发送的数据量,或指定可以查找到的数据结束标记。真正的服务器程序可能会在循环中调用 receive() 方法,并将所有收到的 DatagramPacket 传递给工作线程池,就像 TCP 服务器对传入连接一样。
URL + URLConnection
java.net 包中两个有趣的类:URL 类和 URLConnection 类,这些类可用于创建与 Web 服务器(HTTP 服务器)的客户端连接。 这是一个简单的代码示例:
public class URLExample {
public static void main(String[] args) throws IOException {
URL url = new URL("http://baidu.com");
URLConnection urlConnection = url.openConnection();
InputStream inputStream = urlConnection.getInputStream();
int data = inputStream.read();
while (data != -1) {
System.out.print((char) data);
data = inputStream.read();
}
inputStream.close();
}
}
将会输出
HTTP GET 和 POST
URLConnection 类的作用是构造一个到指定 URL 的 URL 连接。它只有一个构造函数:URLConnection(URL url)
。
默认情况下,URLConnection 向 Web 服务器发送 HTTP GET 请求,即查询数据。如果要发送 HTTP POST 请求提交数据,请调用URLConnection.setDoOutput(true) 方法,如下所示:
URL url = new URL("http://baidu.com");
URLConnection urlConnection = url.openConnection();
urlConnection.setDoOutput(true);
一旦设置了 setDoOutput(true) ,因为要提交数据,所以需要输出流。可以打开 URLConnection 的 OutputStream ,如下所示:
OutputStream output = urlConnection.getOutputStream();
使用此 OutputStream ,可以在 HTTP 请求的正文中编写所需的任何数据。 请记住对其进行 URL 编码(参考 【基础进阶】URL详解与URL编码 ,并记得在完成向其写入数据后关闭 OutputStream 。
本地文件的URL
URL 类还可用于访问本地文件系统中的文件。 因此,如果需要代码处理来源不明的文件,比如是来自网络还是本地文件系统,则 URL 类是打开文件的便捷方式。
以下是使用 URL 类在本地文件系统中打开文件的示例:
URL url = new URL("file:/D:/test/test.txt");
URLConnection urlConnection = url.openConnection();
InputStream input = urlConnection.getInputStream();
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
input.close();
请注意,这和通过 HTTP 访问 Web 服务器上的文件的唯一区别是 URL :"file:/D:/test/test.txt"
和 "http://baidu.com"
JarURLConnection
JarURLConnection 类用于连接 Java Jar 文件。 连接后可以获取有关 Jar 文件内容的信息。 这是一个简单的例子:
String urlString = "http://butterfly.jenkov.com/"
+ "container/download/"
+ "jenkov-butterfly-container-2.9.9-beta.jar";
URL jarUrl = new URL(urlString);
JarURLConnection connection = new JarURLConnection(jarUrl);
Manifest manifest = connection.getManifest();
JarFile jarFile = connection.getJarFile();
//do something with Jar file...