这个在面试的时候经常会有人问,然而网上大多数的回答基本上是在说:TCP 在连接前需要进行三次握手,而 UDP 不会,从而 TCP 是“安全的”,UDP 是“不安全的”,那究竟为啥可靠为啥不可靠?莫非 UDP 不可靠就不可取吗?在这里详细的总结一下。
首先来说,TCP 和 UDP 都是传输层中的协议。他们都为应用程序提供端到端的通信服务。
TCP连接包括三个状态:创建、传送和终止:
在传输过程中,有很多重要的机制来保证传输的可靠性和强壮性:
序列号
和内容数据
的报文段给接收方,接收方会以一个没有数据内容的报文段来回复,用一个确认号来表示已经完全接收并请求下一个报文段。序列号
和内容数据
的报文段,接收方同样会回复一个确认号,发送接收就这样继续下去。在传输过程中,发送方会将 TCP 报文段的头部和数据部分的和计算出来,再求其反码,就得到了校验和,然后将校验和装入报文中传输,接受者在收到报文后再按相同的算法计算一次校验和,然后和报文中的校验和相比较,这样可以保证报文的完整性和正确性。
UDP则和 TCP 不一样,在传输前,并不需要进行连接,不管对方能不能接收到,想发就发,所以说 UDP 是一种面向非连接的协议,正式因为这个原因,没有建立连接,使得 UDP 的通信效率很高,也正是因为如此,使得它的可靠性要低于 TCP。
TCP和UDP的区别大致可以归纳为:
Java 提供了 DatagramSocket 来帮助我们创建 UDP 协议的 Socket,DatagramSocket 的作用是发送和接收数据报文,DatagramSocket 接收和发送数据都是通过 DatagramPacket 对象完成的。
通俗一点来讲,DatagramSocket 是用来“接收发送和数据报”的,DatagramPacket 就是数据报,根据他们的构造函数和一些常用方法就可以看出来:
DatagramSocket
构造函数:
DatagramSocket(SocketAddress bindaddr)
创建数据报套接字,将其绑定到指定的本地套接字地址。
常用设置方法:
常用查询方法:
其他方法:
send(DatagramPacket p)
从此套接字发送数据报包。
DatagramPacket
构造函数:
查询函数:
设置函数:
从上面的一系列方法就可以很明显的看出来,DatagramSocket 就是一个发射接收器,DatagramPacket 才是包含一系列数据的数据报,DatagramPacket 包含了将要发送的数据、数据的长度、远程主机的 IP 地址还有远程主机的端口号。
使用 DatagramSocket 发送、接收数据通常需要下面几个步骤:
事实上,当我们使用 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));
所以绑定操作需要尽量靠前做。