基础概念
Java 网络API 允许我们通过套接字(Socket)打开或关闭网络连接,但所有的网络通信均是基于Java IO 类 InputStream 和OutputStream 实现的。
此外,我们还可以使用Java NIO API中相关的网络类,用法与Java网络API基本类似,Java NIO API可以以非阻塞模式工作,在某些特定的场景中使用非阻塞模式可以获得较大的性能提升。
Java TCP网络基础
通常情况下,客户端打开一个连接到服务器端的TCP/IP 连接,然后客户端开始与服务器之间通信,当通信结束后客户端关闭连接。
客户端通过一个已打开的连接可以发送不止一个请求。事实上在服务器处于接收状态下,客户端可以发送尽可能多的数据,服务器也可以主动关闭连接。
Java中Socket类和ServerSocket类
当客户端想要打开一个连接到服务器的TCP/IP连接时,就要使用到Java Socket类。socket类只需要被告知连接的IP地址和TCP端口,其余的都有Java实现。
假如我们想要打开一个监听服务,来监听客户端连接某些指定TCP端口的连接,那就需要使用Java ServerSocket类。当客户端通过Socket连接服务器端的ServerSocket监听时,服务器端会指定这个连接的一个Socket,此时客户端与服务器端间的通信就变成Socket与Socket之间的通信。
关于Socket类和ServerSocket类会在后面的文章中有详细的介绍。
Java UDP网络基础
UDP的工作方式与TCP相比略有不同。使用UDP通信时,在客户端与服务器之间并没有建立连接的概念,客户端发送到服务器的数据,服务器可能(也可能并没有)收到这些数据,而且客户端也并不知道这些数据是否被服务器成功接收。当服务器向客户端发送数据时也是如此。
正因为是不可靠的数据传输,UDP相比与TCP来说少了很多的协议开销。
在某些场景中,使用无连接的UDP要优于TCP,这些在文章Java UDP DatagramSocket类介绍中会有更多介绍。
Socket、ServerSocket
当我们想要在Java中使用TCP/IP通过网络连接到服务器时,就需要创建java.net.Socket对象并连接到服务器。假如希望使用Java NIO,也可以创建Java NIO中的SocketChannel对象。
创建Socket
1 | Socket socket = new Socket("78.46.84.171", 80); |
Socket发送数据
要通过Socket发送数据,我们需要获取Socket的输出流(OutputStream),示例代码如下:
1 | Socket socket = new Socket("jenkov.com", 80); |
代码非常简单,但是想要通过网络将数据发送到服务器端,一定不要忘记调用flush()方法。操作系统底层的TCP/IP实现会先将数据放入一个更大的数据缓存块中,而缓存块的大小是与TCP/IP的数据包大小相适应的。(译者注:调用flush()方法只是将数据写入操作系统缓存中,并不保证数据会立即发送)
Socket读取数据
从Socket中读取数据,我们就需要获取Socket的输入流(InputStream),代码如下:
1 | Socket socket = new Socket("jenkov.com", 80); |
代码也并不复杂,但需要注意的是,从Socket的输入流中读取数据并不能读取文件那样,一直调用read()方法直到返回-1为止,因为对Socket而言,只有当服务端关闭连接时,Socket的输入流才会返回-1,而是事实上服务器并不会不停地关闭连接。假设我们想要通过一个连接发送多个请求,那么在这种情况下关闭连接就显得非常愚蠢。
因此,从Socket的输入流中读取数据时我们必须要知道需要读取的字节数,这可以通过让服务器在数据中告知发送了多少字节来实现,也可以采用在数据末尾设置特殊字符标记的方式连实现。
关闭Socket
当使用完Socket后我们必须将Socket关闭,断开与服务器之间的连接。关闭Socket只需要调用Socket.close()方法即可,代码如下:
1 | Socket socket = new Socket("vgbhfive.cn", 80); |
Demo
https://github.com/vgbhfive/VChatDemo
UDP DatagramSocket
DatagramSocket 是Java 通过UDP(而非TCP)进行网络通信的机制。UDP 位于IP 之上。
使用Java 的DatagramSocket 来发送和接收UPD 数据包(数据报)。
UDP、TCP
UDP 的工作方式与TCP 有所不同。
通过TCP 发送数据时,首先要创建一个连接,建立TCP 连接后,TCP 会确保您的数据到达另一端,否则它将另一端告诉发生了错误。
使用UDP,只需将数据包(数据报)发送到网络上的某个IP 地址。但是不能保证数据会到达,并且也无法保证UDP 数据包到达接收方的顺序。
这意味着UDP 比TCP 具有更少的协议开销(无流完整性检查)。
UDP 适用于数据传输,在传输过程中是否丢失数据包并不重要。
例如,想象一下通过互联网传输实况电视信号。您希望信号尽可能接近现场。因此,如果丢失了一两帧,则不必担心,最实际的是不希望延迟直播,只是确保所有帧都在客户端显示。宁愿跳过错过的帧,并始终直接移至最新的帧。
DatagramSocket
通过DatagramSocket 发送数据,要通过Java DatagramSocket 发送数据则必须先创建一个DatagramPacket 。
1 | byte[] buffer = new byte[65508]; |
字节缓冲区(字节数组)是在UDP 数据报中发送的数据。
在DatagramPacket 构造函数的长度是要发送的缓冲区中数据的长度。在该数据量之后,缓冲区中的所有数据将被忽略。
在InetAddress 实例中包含要向其发送UDP 数据包的节点(例如服务器)的地址。这个InetAddress 类代表一个IP地址(Internet地址)。
这个getByName() 方法返回一个InetAddress 实例,该实例的IP 地址与给定的主机名匹配。
port 参数是服务器接收数据的UDP 端口。
UDP和TCP端口不相同。一台计算机可以具有不同的进程,例如同时侦听UDP和TCP中的端口80。
缓冲区的长度为65508字节,是您可以在单个UDP数据包中发送的最大数据量。
DatagramSocket.send()
发送数据,DatagramPacket 必须创建一个DatagramSocket 针对发送数据的目标。
1 | DatagramSocket datagramSocket = new DatagramSocket(); |
要发送数据,请调用send()方法,如下所示:
1 | datagramSocket.send(packet); |
Demo:
1 | DatagramSocket datagramSocket = new DatagramSocket(); |
DatagramSocket.receive()
接收数据是通过DatagramSocket 建立一个完成DatagramPacket ,然后通过将数据接收到它DatagramSocket 的receive() 方法。
1 | DatagramSocket datagramSocket = new DatagramSocket(80); |
- 首先,80 是DatagramSocket 用于接收UDP 数据包的UDP 端口。如前所述,TCP 和UDP 端口不相同,因此不会重叠。因此可以有两个不同的进程在TCP 和UDP 端口80 侦听,而不会发生任何冲突。
- 其次,DatagramPacket 创建一个字节缓冲区和一个DatagramPacket。
DatagramPacket 与用于发送数据的节点一样,该节点没有关于要向其发送数据的节点的信息。这是因为我们将使用DatagramPacket来接收数据,而不是发送数据。因此,不需要目的地地址。 - 最后,调用DatagramSocket 的receive() 方法。此方法会阻塞直到DatagramPacket 收到数据为止。收到的数据位于DatagramPacket 的字节缓冲区中。
通过调用以下方法获得此缓冲区:如果需要确定缓冲区中接收了多少数据。那么就需要在使用的协议中指定每个UDP数据包发送多少数据,或者指定数据的结束标记。1
byte[] buffer = packet.getData();
真正的服务器程序可能会receive() 在循环中调用该方法,并将所有接收到的DatagramPacket 传递给工作线程池,就像TCP 服务器处理传入连接一样。
URL + URLConnection
在java.net包中包含两个有趣的类:URL类和URLConnection类。这两个类可以用来创建客户端到web服务器(HTTP服务器)的连接。
下面是一个简单的代码例子:
1 | URL url = new URL("https://vgbhfive.cn"); |
HTTP GET和POST
默认情况下URLConnection发送一个HTTP GET请求到web服务器。如果你想发送一个HTTP POST请求,要调用URLConnection.setDoOutput(true)方法,
如下:
1 | URL url = new URL("https://vgbhfive.cn"); |
一旦你调用了setDoOutput(true),你就可以打开URLConnection的OutputStream,如下:
1 | OutputStream output = urlConnection.getOutputStream(); |
你可以使用这个OutputStream向相应的HTTP请求中写任何数据,但你要记得将其转换成URL编码(关于URL编码的解释,自行Google)(译者注:具体名字是:application/x-www-form-urlencoded MIME 格式编码)。
当你写完数据的时候要记得关闭OutputStream。
从URLs到本地文件
URL也被叫做统一资源定位符。如果你的代码不关心文件是来自网络还是来自本地文件系统,URL类是另外一种打开文件的方式。
下面是一个如何使用URL类打开一个本地文件系统文件的例子:
1 | URL url = new URL("file:/c:/data/test.txt"); |
注意:这和通过HTTP访问一个web服务器上的文件的唯一不同处就是URL:”file:/c:/data/test.txt”。
JarURLConnection
Java的JarURLConnection类用来连接Java Jar文件。一旦连接上,你可以获取Jar文件的信息。
一个简单的例子如下:
1 | String urlString = "http://butterfly.jenkov.com/" |
InetAddress
- 创建一个 InetAddress 实例
- InetAddress 的内部方法
InetAddress 是 Java 对 IP 地址的封装。这个类的实例经常和 UDP DatagramSockets 和 Socket,ServerSocket 类一起使用。
创建一个 InetAddress 实例
InetAddress 没有公开的构造方法,因此你必须通过一系列静态方法中的某一个来获取它的实例。
下面是为一个域名实例化 InetAddres 类的例子:
1 | InetAddress address = InetAddress.getByName("jenkov.com"); |
当然也会有为匹配某个 IP 地址来实例化一个 InetAddress:
1 | InetAddress address = InetAddress.getByName("78.46.84.171"); |
另外,它还有通过获取本地 IP 地址的来获取 InetAddress 的方法(正在运行程序的那台机器)
1 | InetAddress address = InetAddress.getLocalHost(); |
InetAddress 内部方法
InetAddress 类还拥有大量你可以调用的其它方法。例如:你可以通过调用getAddress()方法来获取 IP 地址的 byte 数组。如果要了解更多的方法,最简单的方式就是读 JavaDoc 文档中关于 InetAddress 类的部分。
Protocol Design
如果设计一个客户端到服务器的系统,那么同时也需要设计客户端和服务器之间的通信协议。当然,有时候协议已经为你决定好了,比如HTTP、XML_RPC(http response 的 body 使用xml)、或者SOAP(也是http response 的 body 使用xml)。设计客户端到服务端协议的时候,一旦协议决定开启一会儿,来看一些你必须考虑的地方:
- 客户端到服务端的往返通讯
- 区分请求结束和响应结束。
- 防火墙穿透
客户端-服务端往返
当客户端和服务端通信,执行操作时,他们在交换信息。比如,客户端执行一个服务请求,服务端尝试完成这个请求,发回响应告诉客户端结果。这种客户端和服务端的信息交换就叫做往返。
示意图如下:
当一个计算机(客户端或者服务端)在网络中发送数据到另一个计算机时,从数据发送到另一端接收数据完会花费一定时间。这就是数据在网络间的传送的时间花费。这个时间叫做延迟。
协议中含有越多的往返,协议变得越慢,延迟特别高。HTTP协议只包含一个单独的响应来执行服务。换句话说就是一个单独的往返。另一方面,在一封邮件发送前,SMTP协议包含了几个客户端和服务端的往返。
在协议中有多个往返的原因是:有大量的数据从客户端发送到服务端。
这种情况下你有2个选择:
- 在分开往返中发送头信息;
- 将消息分成更小的数据块。
如果服务端能完成头信息的一些初始验证 ,那么分开发送头信息是很明智的。如果头信息是空白的,发送大量数据本身就是浪费资源。
在传输大量数据时,如果网络连接失败了,得从头开始重新发送数据。数据分割发送时,只需要在网络连接失败处重新发送数据块。已经发送成功的数据块不需要重新发送。
区分请求结束和响应结束
如果协议容许在同一个连接中发送多个请求,需要一个让服务端知道当前请求何时结束、下一个请求何时开始。客户端也需要知道一个响应何时结束了,下一个响应何时开始。
对于请求有2个方法区分结束:
- 在请求的开始处发送请求的字长
- 在请求数据的最后发送一个结束标记。
HTTP用第一个机制。在请求头中 发送了“Content-Length”。请求头会告诉服务端在头文件后有多少字节是属于请求的。
这个模型的优势在于没有请求结束标志的开销。为了避免数据看上去像请求结束标志,也不需要对数据体进行编码。
第一个方法的劣势:在数据传输前,发送者必须知道多少字节数将被传输。如果数据时动态生成的,在发送前,首先你得缓存所有的数据,这样才能计算出数据的字节数。
运用请求结束标志时,不需要知道发送了多少字节数。只需要知道请求结束标志在数据的末尾。当然,必须确认已发送的数据中不包含会导致请求结束标志错误的数据。可以这样做:
可以说请求结束标志是字节值255。当然数据可能包含值255。因此,对数据中包含值255的每一个字节添加一个额外的字节,还有值255。结束请求标志被从字节值255到255之后的值为0。
如下编码:
1 | 255 in data –>255, 255 |
这种255,0的序列永远不会出现在数据中,因为你把所有的值255变成了255,255。同时,255,255,0也不会被错认为255,0。255,255被理解成在一起的,0是单独的。
防火墙穿透
比起HTTP协议,大多数防火墙会拦截所有的其他通信。因此把协议放在HTTP的上层是个好方法,像XML-RPC,SOAP和REST也可以这样做。
协议置于HTTP的上层,在客户端和服务端的HTTP请求和响应中可以来回发送数据。记住,HTTP请求和响应不止包含text或者HTML。也可以在里面发送二进制数据。
将请求放置在HTTP协议上,唯一有点奇怪的是:HTTP请求必须包含一个“主机”头字段。如果你在HTTP协议上设计P2P协议,同样的人最可能不会运行多个“主机”。在这种情况下需要头字段是不必要的开销(但是个小开销)。
引用
http://tutorials.jenkov.com/java-networking/udp-datagram-sockets.html
http://ifeve.com/
个人备注
此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!