这个在面试的时候经常会有人问,然而网上大多数的回答基本上是在说:TCP 在连接前需要进行三次握手,而 UDP 不会,从而 TCP 是“安全的”,UDP 是“不安全的”,那究竟为啥可靠为啥不可靠?莫非 UDP 不可靠就不可取吗?在这里详细的总结一下。
首先来说,TCP 和 UDP 都是传输层中的协议。他们都为应用程序提供端到端的通信服务。
TCP连接包括三个状态:创建、传送和终止:
在创建过程中,会进行人们经常说的“三次握手”,在握手的过程中,很多参数会被初始化,从而保证数据的按序传输和连接的强壮性。三次握手完成,连接建立。
在传输过程中,有很多重要的机制来保证传输的可靠性和强壮性:
序列号
和内容数据
的报文段给接收方,接收方会以一个没有数据内容的报文段来回复,用一个确认号来表示已经完全接收并请求下一个报文段。序列号
和内容数据
的报文段,接收方同样会回复一个确认号,发送接收就这样继续下去。在传输过程中,发送方会将 TCP 报文段的头部和数据部分的和计算出来,再求其反码,就得到了校验和,然后将校验和装入报文中传输,接受者在收到报文后再按相同的算法计算一次校验和,然后和报文中的校验和相比较,这样可以保证报文的完整性和正确性。
UDP则和 TCP 不一样,在传输前,并不需要进行连接,不管对方能不能接收到,想发就发,所以说 UDP 是一种面向非连接的协议,正式因为这个原因,没有建立连接,使得 UDP 的通信效率很高,也正是因为如此,使得它的可靠性要低于 TCP。
TCP和UDP的区别大致可以归纳为:
TCP 是有序的数据传输,而 UDP 则是无序的。
在发送过程中,如果数据包丢失,TCP 会重新发送,而 UDP 则不会。
TCP 会舍弃屌重复的数据包,而 UDP 不会。
TCP 是面向连接的,需要先连接成功,才进行传输,而 UDP 则是面向非连接的,不需要先进行连接。
Java 提供了 DatagramSocket 来帮助我们创建 UDP 协议的 Socket,DatagramSocket 的作用是发送和接收数据报文,DatagramSocket 接收和发送数据都是通过 DatagramPacket 对象完成的。
通俗一点来讲,DatagramSocket 是用来“接收发送和数据报”的,DatagramPacket 就是数据报,根据他们的构造函数和一些常用方法就可以看出来:
DatagramSocket
构造函数:
DatagramSocket()
构造数据报套接字并将其绑定到本地主机上任何可用的端口。
DatagramSocket(int port)
创建数据报套接字并将其绑定到本地主机上的指定端口。
DatagramSocket(int port, InetAddress laddr)
创建数据报套接字,将其绑定到指定的本地地址。
DatagramSocket(SocketAddress bindaddr)
创建数据报套接字,将其绑定到指定的本地套接字地址。
常用设置方法:
setBroadcast(boolean on)
启用/禁用 SO_BROADCAST。
setDatagramSocketImplFactory(DatagramSocketImplFactory fac)
为应用程序设置数据报套接字实现工厂。
setReceiveBufferSize(int size)
将此 DatagramSocket 的 SO_RCVBUF 选项设置为指定的值。
setReuseAddress(boolean on)
启用/禁用 SO_REUSEADDR 套接字选项。
setSendBufferSize(int size)
将此 DatagramSocket 的 SO_SNDBUF 选项设置为指定的值。
setSoTimeout(int timeout)
启用/禁用带有指定超时值的 SO_TIMEOUT,以毫秒为单位。
setTrafficClass(int tc)
为从此 DatagramSocket 上发送的数据报在 IP 数据报头中设置流量类别 (traffic class) 或服务类型八位组 (type-of-service octet)。
常用查询方法:
getBroadcast()
检测是否启用了 SO_BROADCAST。
getChannel()
返回与此数据报套接字关联的唯一 DatagramChannel 对象(如果有)。
getInetAddress()
返回此套接字连接的地址。
getLocalAddress()
获取套接字绑定的本地地址。
getLocalPort()
返回此套接字绑定的本地主机上的端口号。
getLocalSocketAddress()
返回此套接字绑定的端点的地址,如果尚未绑定则返回 null。
getPort()
返回此套接字的端口。
getReceiveBufferSize()
获取此 DatagramSocket 的 SO_RCVBUF 选项的值,该值是平台在 DatagramSocket 上输入时使用的缓冲区大小。
getRemoteSocketAddress()
返回此套接字连接的端点的地址,如果未连接则返回 null。
getReuseAddress()
检测是否启用了 SO_REUSEADDR。
getSendBufferSize()
获取此 DatagramSocket 的 SO_SNDBUF 选项的值,该值是平台在 DatagramSocket 上输出时使用的缓冲区大小。
getSoTimeout()
获取 SO_TIMEOUT 的设置。
getTrafficClass()
为从此 DatagramSocket 上发送的包获取 IP 数据报头中的流量类别或服务类型。
isBound()
返回套接字的绑定状态。
isClosed()
返回是否关闭了套接字。
isConnected()
返回套接字的连接状态。
其他方法:
bind(SocketAddress addr)
将此 DatagramSocket 绑定到特定的地址和端口。
close()
关闭此数据报套接字。
connect(InetAddress address, int port)
将套接字连接到此套接字的远程地址。
connect(SocketAddress addr)
将此套接字连接到远程套接字地址(IP 地址 + 端口号)。
disconnect()
断开套接字的连接。
receive(DatagramPacket p)
从此套接字接收数据报包。
send(DatagramPacket p)
从此套接字发送数据报包。
DatagramPacket
构造函数:
DatagramPacket(byte[] buf, int length)
构造 DatagramPacket,用来接收长度为 length 的数据包。
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
DatagramPacket(byte[] buf, int offset, int length)
构造 DatagramPacket,用来接收长度为 length 的包,在缓冲区中指定了偏移量。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。
DatagramPacket(byte[] buf, int length, SocketAddress address)
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
查询函数:
getAddress()
返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。
getData()
返回数据缓冲区。
getLength()
返回将要发送或接收到的数据的长度。
getOffset()
返回将要发送或接收到的数据的偏移量。
getPort()
返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。
getSocketAddress()
获取要将此包发送到的或发出此数据报的远程主机的 SocketAddress(通常为 IP 地址 + 端口号)。
设置函数:
setAddress(InetAddress iaddr)
设置要将此数据报发往的那台机器的 IP 地址。
setData(byte[] buf)
为此包设置数据缓冲区。
setData(byte[] buf, int offset, int length)
为此包设置数据缓冲区。
setLength(int length)
为此包设置长度。
setPort(int iport)
设置要将此数据报发往的远程主机上的端口号。
setSocketAddress(SocketAddress address)
设置要将此数据报发往的远程主机的 SocketAddress(通常为 IP 地址 + 端口号)。
从上面的一系列方法就可以很明显的看出来,DatagramSocket 就是一个发射接收器,DatagramPacket 才是包含一系列数据的数据报,DatagramPacket 包含了将要发送的数据、数据的长度、远程主机的 IP 地址还有远程主机的端口号。
使用 DatagramSocket 发送、接收数据通常需要下面几个步骤:
创建 DatagramSocket 对象。
创建 DatagramPacket 对象,并设置我们要发送的数据,接收端的 IP 地址、端口号等等。
(发送方)调用 DatagramSocket 对象的 send(DatagramPacket p) 方法向外发送数据。
(接收方)调用 DatagramSocket 对象的 receive(DatagramPacket p) 方法接收数据。
事实上,当我们使用 UDP 协议进行 Socket 通信的时候,是没有 Server/Client 的区别的,双方都需要创建 DatagramSocket 对象,接收和发送都需要 DatagramPacket 对象作为数据发送和接收的载体。通常情况下,固定 IP、固定端口的 DatagramSocket 对象所在的程序端被成为服务器端。
一个简单的例子:
接收端
public class UDPReceiver {
public static void main(String[] args) {
try {
byte[] buf = new byte[1024];
DatagramSocket socket = new DatagramSocket(55555);
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
byte[] data = packet.getData();
System.out.println(new String(data, 0, data.length));
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
发送端
public class UDPSender {
public static void main(String[] args) {
try {
byte[] bytes = "苍茫的天涯是我的爱".getBytes();
DatagramSocket socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
packet.setAddress(InetAddress.getByName("127.0.0.1"));
packet.setPort(55555);
socket.send(packet);
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
需要注意的是,我们使用 UDP Socket,DatagramPacket 作为数据包,每条报文仅根据该包中包含的信息从一台机器到另外一台机器,从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
DatagramSocket 的作用是用来发送和接收 DatagramPacket 数据报,为了接收广播包,应该将 DatagramSocket 绑定到通配符地址。在某些情况中,将 DatagramSocket 版定到一个更加具体的地址时广播包也可以被接收。
又有疑问了,UDP 不是面向非连接的么?那为什么还会有 connect 这样的方法呢?
来看一个例子,修改上面发送端的代码:
public class UDPSender {
public static void main(String[] args) {
try {
byte[] bytes = "苍茫的天涯是我的爱".getBytes();
DatagramSocket socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
packet.setAddress(InetAddress.getByName("127.0.0.1"));
packet.setPort(55555);
socket.send(packet);
System.out.println("连接的地址:" + socket.getInetAddress() + "\n连接的端口:" + socket.getPort());
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出一下发送端的地址和端口,我们会发现:
连接的地址:null
连接的端口:-1
连接地址是 null,端口是 -1,这是啥情况呢?查看 API 中对 getInetAddress 和 getPort 方法的解释:
getInetAddress:返回此套接字连接的地址。如果套接字未连接,则返回 null。
getPort:返回此套接字的端口。如果套接字未连接,则返回 -1。
可以看到,在未连接的情况下,才会发生以上的情况,那么我们调用一下 connect 方法呢?
socket.connect(InetAddress.getByName("127.0.0.1"), 55555);
这下有了:
连接的地址:/127.0.0.1
连接的端口:55555
又有疑问了,不是已经在 DatagramPacket 中指定了目的地址和端口了吗?这里为啥又要绑定一个?我们来把绑定的端口修改一下再运行,结果就是:
Exception in thread "main" java.lang.IllegalArgumentException: connected address and packet address differ
可见,我们绑定的地址和端口是要和 DatagramPacket 中指定的地址端口相同的,这不是脱裤子放屁么?
事实上,我们绑定了地址和端口之后, DatagramPacket 不指定地址和端口也同样有效,把发送端的 setAddress 和 setPort 方法注释掉,发现运行效果没差。
和 TCP Scoket 一样,有时候我们也需要将连接和本地地址和本地端口绑定,除了直接在构造方法中绑定端口之外也可以调用 DatagramSocket 提供的 bind 方法。
需要注意的是,bind 方法需要在 connect 方法调用之前调用(如果有调用 connect 的话),因为如果如果没有绑定的话,调用 connect 方法会默认进行一个绑定操作,看 connect 的源码就知道,其中有这样一段:
if (!isBound())
bind(new InetSocketAddress(0));
所以绑定操作需要尽量靠前做。