java.net.ServerSocket与java.net.Socket建立在TCP的基础上。TCP是网络传输层的一种可靠的数据传输协议。如果数据在传输途中被丢失或损坏,那么TCP会保证再次发送数据;如果数据到达接收方的顺序被打乱,那么TCP会在接收方重新恢复数据的正确顺序。应用层无须担心接收到乱序或错误的数据,应用层只需从Socket中获得输入流和输出流,就可以方便地接收和发送数据。当发送方先发送字符串“hello”,再发送字符串“everyone”时,接收方在应用层必定先接收到字符串“hello”,再接收到字符串“everyone”。接收方不会先接收到字符串“everyone”,再接收到字符串“hello”。
TCP的可靠性是有代价的,这种代价就是传输速度的降低。建立和销毁TCP连接会花费很长的时间。如果通信双方实际上通信的时间很短,要传输的数据很少,那么建立和销毁TCP连接的代价就相对较高。
UDP(User Datagram Protocol,用户数据报协议)是传输层的另一种协议,它比TCP具有更快的传输速度,但是不可靠。UDP发送的数据单元被称为UDP数据报。当网络传输UDP数据报时,无法保证数据报一定到达目的地,也无法保证各个数据报按发送的顺序到达目的地。当发送方先发送包含字符串“hello”的数据报,再发送包含字符串“everyone”的数据报时,接收方有可能先接收到字符串“everyone”,再接收到字符串“hello”;也有可能什么数据也没有接收到,因为发送方发送的数据有可能在传输途中都被丢失了。
1、UDP简介
如果应用层关心数据能否正确到达,那么就应该在传输层使用TCP。例如应用层的FTP和HTTP都建立在TCP的基础上。如果应用层仅关心发送的数据能否快速到达目的地,则可以考虑在传输层使用UDP。例如,用UDP传输实时音频时,如果丢失了数据报或打乱数据报的顺序,则只会导致接收方出现噪音,而适量的噪音是可以容忍的。但是如果用TCP传输实时音频,则由于速度很慢,会导致接收方常常出现停顿,声音时断时续,这是无法容忍的。由此可见,UDP比TCP更适于传输实时音频。
由于UDP传输数据是不可靠的,假如应用层要求接收到正确的数据,那么应用层自身必须保证数据传输的可靠性。应用层的网络文件系统(Network File System,NFS)协议,以及简单文件传输协议(Trivial FTP,TFTP)协议都建立在UDP的基础上。NFS协议和TFTP会保证可靠性,这意味着实现NFS协议或TFTP的应用程序必须处理丢失或乱序的数据报。
建立在UDP上的应用层如何保证数据传输的可靠性呢?这可以用邮局发送邮包来做解释,邮局就相当于UDP的实现者,它提供不可靠的传输。发送者通过邮局先后发送3个邮包,邮包上写明了接收者的地址,邮局把邮包发送给接收者,但是有可能部分邮包在途中被丢失,也有可能邮包到达目的地的先后顺序与发送的先后顺序不一样。发送者与接收者必须事先商量好确保接收者正确接收到邮包的协议。发送者会在邮包上标上序号,接收者收到邮包后就能按照序号排序。接收者会告诉发送者已经收到了哪些邮包,发送者就能重发那些接收者未收到的邮包。发送者与接收者使用的这种协议就相当于应用层的协议。
在Java中,java.net.DatagramSocket负责接收和发送UDP数据报,java.net.DatagramPacket表示UDP数据报。如下图所示,每个DatagramSocket都与一个本地地址(包括本地主机的IP地址和本地UDP端口)绑定,每个DatagramSocket都可以把UDP数据报发送给任意一个远程DatagramSocket,也都可以接收来自任意一个远程DatagramSocket的UDP数据报。在UDP数据报中包含了目的地址的信息,DatagramSocket根据该信息把数据报发送到目的地。
采用TCP通信时,客户端的Socket必须先与服务器建立连接,连接建立成功后,服务器端也会持有与客户连接的Socket,客户端的Socket与服务器端的Socket是一一对应的,它们构成了两个端点之间的虚拟的通信线路。
而UDP是无连接的协议,客户端的DatagramSocket与服务器端的DatagramSocket不存在一一对应关系,两者无须建立连接,就能交换数据报。DatagramSocket提供了接收和发送数据报的方法。
public void receive(DatagramPacket dst)throws IOException //接收数据报
public void send(DatagramPacket src)throws IOException //发送数据报
下面的EchoServer与例EchoClient就利用DatagramSocket来发送和接收数据报。
在同一时刻,可能会有多个EchoClient进程与EchoServer进程通信。EchoServer的DatagramSocket与UDP端口8000绑定,EchoClient则与任意一个可用的UDP端口绑定。EchoServer在一个while循环中不断接收EchoClient的数据报,然后给EchoClient回复一个数据报。
EchoClient从控制台读取用户输入的字符串,把它包装成一个数据报,然后把它发送给EchoServer,接着再接收EchoServer的响应数据报。
值得注意的是,EchoClient与EchoServer之间的通信是不可靠的,EchoClient以及EchoServer发送的数据报有可能丢失,或者各个数据报到达目的地的顺序与发送数据报的顺序不一致。
2、DatagramPacket类
DatagramPacket表示数据报,它的构造方法可以被分为两种:一种构造方法创建的DatagramPacket对象用来接收数据,还有一种构造方法创建的DatagramPacket对象用来发送数据。两种构造方法的主要区别是,用于发送数据的构造方法需要设定数据报到达的目的地的地址,而用于接收数据的构造方法无须设定地址。
用于接收数据的构造方法包括:
- public DatagramPacket(byte[] data,int length)
- public DatagramPacket(byte[] data,int offset,int length)
以上data参数被用来存放接收到的数据,参数length指定要接收的字节数,参数offset指定在data中存放数据的起始位置,即data[offset]。如果没有设定参数offset,那么起始位置为data[0]。参数length必须小于或等于data.length-offset,否则会抛出IllegalArgumentException。
用于发送数据的构造方法包括:
public DatagramPacket(byte[] data,int offset,int length,InetAddress address,int port)
public DatagramPacket(byte[] data,int offset,int length,SocketAddress address)
public DatagramPacket(byte[] data,int length,InetAddress address,int port)
public DatagramPacket(byte[] data,int length,SocketAddress address)
以上data参数中存放了要发送的数据,参数length指定要发送的字节数,参数offset指定要发送的数据在data中的起始位置,即data[offset]。如果没有设定参数offset,那么起始位置为data[0]。参数length必须小于或等于data.length-offset,否则会抛出IllegalArgumentException。参数address和参数port用来指定目的地的地址。以下两段程序代码都创建了一个数据报,它的送达地址为主机“myhost”的UDP端口100。
3、DatagramSocket类
DatagramSocket类负责接收和发送数据报。每个DatagramSocket对象都会与一个本地端口绑定,在此端口监听发送过来的数据报。在客户程序中,一般由操作系统为DatagramSocket类分配本地端口,这种端口也被称为匿名端口。在服务器程序中,一般由程序显式地为DatagramSocket类指定本地端口。
3.1、构造DatagramSocket
DatagramSocket的构造方法有以下几种重载形式:
第1个不带参数的构造方法使DatagramSocket对象与匿名端口绑定(即由操作系统分配的任意的可用端口),其余的构造方法都会显式地指定本地端口。第3和第4个构造方法需要同时指定IP地址和端口,这适用于一个主机有多个IP地址的场合,这两个构造方法能明确指定DatagramSocket对象所绑定的IP地址。
如果想知道一个DatagramSocket对象所绑定的本地地址,那么可以调用它的以下方法:
- int getLocalPort():返回DatagramSocket所绑定的端口。
- InetAddress getLocalAddress():返回DatagramSocket所绑定的IP地址。
- SocketAddress getLocalSocketAddress():返回一个SocketAddress对象,它包含DatagramSocket所绑定的IP地址和端口信息。
3.2、接收和发送数据报
DatagramSocket的send()方法负责发送一个数据报,该方法的定义如下:
值得注意的是,UDP提供不可靠的传输,如果数据报没有到达目的地,那么send()方法不会抛出任何异常,发送方程序就无法知道数据报是否被接收方接收到,除非双方通过应用层的特定协议来确保接收方未收到数据报时,发送方能重发数据报。
send()方法可能会抛出IOException,但是与java.uti.Socket相比,DatagramSocket的send()方法抛出IOException的可能性很小。如果发送的数据报超过了底层网络所支持的数据报的大小,就可能会抛出SocketException,它是IOException的子类。
DatagramSocket的receive()方法负责接收一个数据报,该方法的定义如下:
此方法从网络上接收一个数据报。如果网络上没有数据报,执行该方法的线程就会进入阻塞状态,直到收到数据报为止。
参数datagramPacket用来存放接收到的数据报,datagramPacket的数据缓冲区应当足够大,以存放接收到的数据。否则,如果接收的数据报太大,receive()方法就会在datagramPacket的数据缓冲区内存放尽可能多的数据,其余的数据则被丢失。
如果接收数据时出现问题,receive()方法就会抛出IOException,但这种情况实际上极少出现。
3.3、管理连接
两个TCP Socket之间存在固定的连接关系,而一个DatagramSocket可以与其他任意一个DatagramSocket交换数据报。
在某些场合,一个DatagramSocket可能只希望与固定的另一个远程DatagramSocket通信。例如,NFS客户只接收来自与之通信的服务器的数据报。再例如,在网络游戏中,一个游戏玩家只接收他的游戏搭档的数据报。
从JDK1.2开始,DatagramSocket添加了一些方法,利用这些方法,可以使一个DatagramSocket只能与另一个固定的DatagramSocket交换数据报:
- (1)
public void connect(InetAddress host,int port)
:connect()方法实际上不建立TCP意义上的连接,但它能限制当前DatagramSocket只对参数指定的远程主机和UDP端口收发数据报。如果当前DatagramSocket试图对其他的主机或UDP端口发送数据报,send()方法就会抛出IllegalArgumentException。从参数以外的其他主机或UDP端口发送过来的数据报则被丢弃,程序不会得到任何通知,也不会抛出任何异常。 - (2)
public void disconnect()
:disconnect()中止当前DatagramSocket已经建立的“连接”,这样,DatagramSocket就可以再次对任何其他主机和UDP端口收发数据报。 - (3)
public int getPort()
当且仅当DatagramSocket已经建立连接时,getPort()方法才返回DatagramSocket所连接的远程UDP端口,否则返回“-1”。 - (4)
public InetAddress getInetAddress()
当且仅当DatagramSocket已经建立连接时,getInetAddress()方法才返回DatagramSocket所连接的远程主机的IP地址,否则返回null。 - (5)
public SocketAddress getRemoteSocketAddress()
当且仅当DatagramSocket已经建立连接时,getRemoteSocketAddress()方法才返回一个SocketAddress对象,表示DatagramSocket所连接的远程主机以及端口的地址,否则返回null。
UDP客户程序通常只和特定的UDP服务器通信,因此可在UDP客户程序中把DatagramSocket与远程服务器连接。UDP服务器需要与多个UDP客户程序通信,因此在UDP服务器中一般不用对DatagramSocket建立特定的连接。
3.4、关闭DatagramSocket
DatagramSocket的close()方法会释放所占用的本地UDP端口。在程序中及时关闭不再需要的DatagramSocket,这是好的编程习惯。
3.5、DatagramSocket的选项
DatagramSocket有以下选项:
- SO_TIMEOUT:表示接收数据报时的等待超时时间。
- SO_RCVBUF:表示接收数据的缓冲区的大小。
- SO_SNDBUF:表示发送数据的缓冲区的大小。
- SO_REUSEADDR:表示是否允许重用DatagramSocket所绑定的本地地址。
- SO_BROADCAST:表示是否允许对网络广播地址发送广播数据报。
3.6、IP服务类型选项
UDP与TCP都建立在IP基础之上,DatagramSocket也提供了设置和读取IP服务类型的方法:
- 设置服务类型:public void setTrafficClass(int trafficClass) throws SocketException
- 读取服务类型:public int getTrafficClass() throws SocketException
4、DatagramChannel类
从JDK1.4开始,添加了一个支持按照非阻塞方式发送和接收数据报的DatagramChannel。DatagramChannel是SelectableChannel的子类,可以被注册到一个Selector。使用DatagramChannel,可以使得UDP服务器只需用单个线程就能同时与多个客户通信。DatagramChannel在默认情况下采用阻塞模式,如果希望该为非阻塞模式,那么可以调用configureBlocking(false)方法。