博客主页java
UDP是Socket中重要组成部分,下面主要想带你们一块儿了解什么是UDP,以及UDP能够用来作什么。编程
UDP全称为User Datagram Protocol,缩写为UDP,称为用户数据报协议,也叫用户数据报文协议。它是一个简单的面向数据报的传输层协议,正式规范为RFC 768。在上一篇Socket网络编程理论知识中介绍了UDP是一种面向无链接的协议,所以,在通讯时发送端和接收端不用创建链接。segmentfault
UDP通讯的过程就像是货运公司在两个码头间发送货物同样,在码头发送和接收货物时都须要使用集装箱来装载货物,UDP通讯也是同样,发送和接收的数据也须要使用“集装箱”进行打包。数组
UDP为何不可靠呢?缓存
原来访问网页和手机APP都是基于HTTP协议的。HTTP协议是基于TCP的,创建链接都须要屡次交互,对于时延比较大的目前主流的移动互联网来说,创建一次链接须要的时间会比较长,然而既然是移动中,TCP可能还会断了重连,也是很耗时的。并且目前的HTTP协议,每每采起多个数据通道共享一个链接的状况,这样原本为了加快传输速度,可是TCP的严格顺序策略使得哪怕共享通道,前一个不来,后一个和前一个即使不要紧,也要等着,时延也会加大。服务器
而QUIC(全称Quick UDP Internet Connections,快速UDP互联网链接)是Google提出的一种基于UDP改进的通讯协议,其目的是下降网络通讯的延迟,提供更好的用户互动体验。网络
QUIC在应用层上,会本身实现快速链接创建、减小重传时延,自适应拥塞控制,是应用层“城会玩”的表明。这一节主要是讲UDP,QUIC咱们放到应用层去讲。数据结构
如今直播比较火,直播协议多使用RTMP,这个协议咱们后面的章节也会讲,而这个RTMP协议也是基于TCP的。TCP的严格顺序传输要保证前一个收到了,下一个才能确认,若是前一个收不到,下一个就算包已经收到了,在缓存里面,也须要等着。对于直播来说,这显然是不合适的,由于老的视频帧丢了其实也就丢了,就算再传过来用户也不在乎了,他们要看新的了,若是总是没来就等着,卡顿了,新的也看不了,那就会丢失客户,因此直播,实时性比较比较重要,宁肯丢包,也不要卡顿的。dom
另外,对于丢包,其实对于视频播放来说,有的包能够丢,有的包不能丢,由于视频的连续帧里面,有的帧重要,有的不重要,若是必需要丢包,隔几个帧丢一个,其实看视频的人不会感知,可是若是连续丢帧,就会感知了,于是在网络很差的状况下,应用但愿选择性的丢帧。异步
还有就是当网络很差的时候,TCP协议会主动下降发送速度,这对原本当时就卡的看视频来说是要命的,应该应用层立刻重传,而不是主动让步。于是,不少直播应用,都基于UDP实现了本身的视频传输协议。
游戏有一个特色,就是实时性比较高。
实时游戏中客户端和服务端要创建长链接,来保证明时传输。可是游戏玩家不少,服务器却很少。因为维护TCP链接须要在内核维护一些数据结构,于是一台机器可以支撑的TCP链接数目是有限的,而后UDP因为是没有链接的,在异步IO机制引入以前,经常是应对海量客户端链接的策略。
另外仍是TCP的强顺序问题,对战的游戏,对网络的要求很简单,玩家经过客户端发送给服务器鼠标和键盘行走的位置,服务器会处理每一个用户发送过来的全部场景,处理完再返回给客户端,客户端解析响应,渲染最新的场景展现给玩家。
若是出现一个数据包丢失,全部事情都须要停下来等待这个数据包重发。客户端会出现等待接收数据,然而玩家并不关心过时的数据,激战中卡1秒,等能动了都已经死了。
游戏对实时要求较为严格的状况下,采用自定义的可靠UDP协议,自定义重传策略,可以把丢包产生的延迟降到最低,尽可能减小网络问题对游戏性形成的影响。
一方面,物联网领域终端资源少,极可能只是个内存很是小的嵌入式系统,而维护TCP协议代价太大;另外一方面,物联网对实时性要求也很高,而TCP时延大。Google旗下的Nest创建Thread Group,推出了物联网通讯协议Thread,就是基于UDP协议的。
在4G网络里,移动流量上网的数据面对的协议GTP-U是基于UDP的。由于移动网络协议比较复杂,而GTP协议自己就包含复杂的手机上线下线的通讯协议。若是基于TCP,TCP的机制就显得很是多余。
在UDP通讯中有2个经常使用的类:一个是数据包类DatagramPacket,一个是数据包发送接收器类DatagramSocket
根据API文档的内容,对UDP两个经常使用类进行分析:
在java中,提供了一个DatagramPacket类,该类的实例对象就至关于一个集装箱,用来封装UDP通讯中发送或者接收的数据。
首先须要了解下DatagramPacket的构造方法。在建立发送端和接收端的DatagramPacket对象时,使用的构造方法有所不一样,接收端的构造方法只须要接收一个字节数组来存放接收到的数据,而发送端的构造方法不但要存放发送数据的字节数组,还须要指定发送端的IP地址和端口号。
先来了解下DatagramPacket的构造方法:
其中SocketAddress对象封装了IP地址+端口号,至关于InetAddress+端口号port。
// 从SocketAddress子类的构造方法能够看出 InetSocketAddress(InetAddress addr, int port)
了解了DatagramPacket构造方法,接下来对DatagramPacket类中的经常使用方法进行说明:
DatagramPacket数据包的做用就如同是“集装箱”,能够将发送端或者接受端的数据封装起来。然而运输货物只有“集装箱”是不够的,还须要有码头。在程序中须要实现通讯只有DatagramPacket数据包也一样不行,为此JDK中提供一个DatagramSocket类。DatagramSocket类的做用就相似于码头,使用这个类的实例对象就能够发送和接收DatagramPacket数据包。
在建立发送端和接收端的DatagramSocket对象时,使用的构造方法有所不一样。
先来了解下DatagramSocket构造方法:
了解了DatagramSocket构造方法,接下来对DatagramSocket类中的经常使用方法进行说明:
使用UDP完成一个简易的聊天程序案例:在发送端控制台中输入要发送的消息,接收端接收发送端发来的消息,并在接收端控制台中输出发送端的IP地址、端口号和消息,当发送端输入886,发送端和接收端都结束。
UDP完成接收端程序:
public class UdpReceive { public static void main(String[] args) throws IOException { // 1. 建立DatagramPacket对象,用于封装一个字节数组,用于接收数据 byte[] data = new byte[1024]; // 最大长度1024*64=64KB DatagramPacket receiverPacket = new DatagramPacket(data, data.length); // 2. 建立DatagramSocket对象,绑定到本地主机上的指定端口 DatagramSocket socket = new DatagramSocket(10002); while (true) { // 3. 使用DatagramSocket对象的receive方法,接收数据包 // 该方法阻塞,直到接收到数据报 socket.receive(receiverPacket); // 4. 拆包 // 返回该数据报发送或接收数据报的计算机的IP地址。 String ip = receiverPacket.getAddress().getHostAddress(); // 返回发送数据报的远程主机上的端口号,或从中接收数据报的端口号。 int port = receiverPacket.getPort(); // 返回要发送的数据的长度或接收到的数据的长度。 int length = receiverPacket.getLength(); String message = new String(data, 0, length); System.out.println("Receive-> receiver data: " + message + " from " + ip + ":" + port); if ("886".equalsIgnoreCase(message)) { // 关闭接受者,不在接收消息 break; } } // 5. 关闭此数据报套接字。 socket.close(); } }
UDP完成发送端程序:
public class UdpSend { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); InetAddress address = InetAddress.getByName("127.0.0.1"); // 2. 建立DatagramSocket对象,系统会分配一个可用的端口号 DatagramSocket socket = new DatagramSocket(); while (true) { String message = scanner.nextLine(); // 读取输入的数据 byte[] data = message.getBytes(); // 1.建立DatagramPacket对象,用于封装长度为length数据报包发送到指定主机上的指定端口号。 DatagramPacket sendPacket = new DatagramPacket(data, data.length, address, 10002); // 3.使用DatagramSocket对象中的send方法,发送数据报包 socket.send(sendPacket); if ("886".equalsIgnoreCase(message)) { // 结束聊天 break; } } // 4. 关闭此数据报套接字 socket.close(); } }
IP地址是指互联网协议地址(Internet Protocol Address)。IP地址用来给一个网络中的计算机设备作一个惟一的编号。在TCP/IP协议中,这个标识号就是IP地址。目前普遍使用的IP地址是IPv4。
IP地址分类:
它由4个字节大小的二进制数表示,如:00001010000100000010100100000001。因为二进制形式表示的IP地址很是不便记忆和处理,所以一般会将IP地址写成十进制的形式,每一个字节用一个十进制数字(0-255)表示,数字间用符号“.”分开,如 “192.168.1.100”。
随着计算机网络规模的不断扩大,对IP地址的需求也愈来愈多,IPV4这种用4个字节表示的IP地址面临枯竭,所以IPv6 便应运而生了,IPv6采用128位地址长度,每16个字节一组,分红8组十六进制数,表示成fd00:EF01:4023:6507:bb92:e153:ef13:6789。
InetAddress
JDK中提供了一个InetAddress类,该类用于封装一个IP地址,并提供了一系列与IP地址相关的方法:
public class InetAddressDemo { public static void main(String[] args) throws UnknownHostException { // 返回本地主机的地址 InetAddress local = InetAddress.getLocalHost(); System.out.println("本机的IP地址:" + local.getHostAddress()); // 172.20.43.73 System.out.println("本机IP地址的主机名:" + local.getHostName()); // YQBMAC-0050 //肯定主机名称的IP地址。 InetAddress remote = InetAddress.getByName("218.98.31.235"); System.out.println("remote的IP地址:" + remote.getHostAddress()); // 218.98.31.235 System.out.println("remote的主机名:" + local.getHostName()); // YQBMAC-0050 } }
IP地址类别:
从上图可知,不一样的类别能够经过子网掩码来区分。咱们经常使用的是B类和C类地址。
先来看下UDP的广播和多播相关知识:
广播地址的计算方法:
如:172.17.24.18/20 ,计算其广播地址;
因为该IP的掩码为20个比特位,所以,其掩码地址为:255.255.240.0
IP地址的二进制表示为:10101100.00010001.00011000.00010010
(1)IP地址与子网掩码按位“与”运算 结果:10101100.00010001.00010000.00000000 即:172.17.16.0
(2)子网掩码按位取反结果:00000000.00000000.00001111.11111111
与网络地址或运算结果:10101100.00010001.00011111.11111111 即:172.17.31.255
IP地址构成,由4个字节二进制数据表示,一般转化成十进制形式:
上面看到了受限广播地址,即 255.255.255.255 ,当使用这个地址做为广播地址时,路由器的其余设备都能监听到,但若是A路由器和B路由器想要之间也能通讯,好比:
A:ip为192.168.134.7 ,子网掩码为 255.255.255.192
B:ip为192.168.134.100 ,子网掩码也是 255.255.255.192
看到A与B的子网掩码是同样,但其实仍是不能通讯,由于A与B的广播地址不同,A广播地址为 192.168.134.63 B广播地址为 192.168.134.127。
网络通讯,本质上是两个进程(应用程序)的通讯。每台计算机都有不少的进程,那么在网络通讯时,如何区分这些进程呢?
若是说IP地址能够惟一标识网络中的设备,那么端口号就能够惟一惟一标识设备中的进程(应用程序)了。
端口号是用两个字节表示整数,它的取值范围是0~65535。其中,0~1024之间的端口号用于一些知名的网络服务和应用,普通的应用程序须要使用1024以上的端口号,若是端口号被另外一个服务或者程序所占用,会致使当前程序启动失败。
利用协议+IP地址+端口号组合,就能够标识网络中的进程了,那么进程间的通讯能够利用这个标识与其它进程进行交互。
实现一个局域网搜索案例:
首先绑定到本地主机上的30000端口,当接收到数据包时,拆数据包解析出要发送的端口号,而后随机生产一个序列号,使用解析的端口号发送该序列号。
public class UdpProvider { public static void main(String[] args) throws IOException { // 建立DatagramSocket对象,并将其绑定到本地主机上的指定30000端口 DatagramSocket socket = new DatagramSocket(30000); // 建立DatagramPacket对象,用于封装接收的数据包 byte[] receiveData = new byte[1024]; DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); // 接收数据包 // 该方法阻塞,直到接收到数据报 socket.receive(receivePacket); // 拆数据包 int length = receivePacket.getLength(); String ip = receivePacket.getAddress().getHostAddress(); int port = receivePacket.getPort(); String receivePort = new String(receiveData, 0, length); System.out.println("Provider-> receive: " + receivePort + " form " + ip + ":" + port); // 随机生产一个序列号 String sn = UUID.randomUUID().toString(); // 根据接收到端口号,发送数据包 byte[] data = sn.getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length); packet.setAddress(receivePacket.getAddress()); // 传入接收过来的IP地址 packet.setPort(Integer.parseInt(receivePort)); // 使用DatagramSocket发送数据包 socket.send(packet); // 释放资源 socket.close(); } }
绑定到本地主机上的20000端口,启动后,给30000端口发送一个数据包,数据封装20000端口数据。
public class UdpSearch { public static void main(String[] args) throws IOException, InterruptedException { CountDownLatch latch = new CountDownLatch(1); new Thread(new SearchListener(latch)).start(); latch.await(); sendBroadcast(); } private static void sendBroadcast() throws IOException { // 建立DatagramSocket对象 DatagramSocket socket = new DatagramSocket(); // 建立DatagramPacket对象,用于封装数据包:数据、IP地址、端口号 byte[] data = "20000".getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length); packet.setPort(30000); // 数据包发给30000端口 packet.setAddress(InetAddress.getByName("255.255.255.255")); //使用DatagramSocket的send方法,发送数据包 socket.send(packet); socket.close(); System.out.println("Search-> 发送广播结束."); } private static class SearchListener implements Runnable { DatagramSocket socket; CountDownLatch latch; boolean isClosed = false; SearchListener(CountDownLatch latch) { this.latch = latch; } @Override public void run() { System.out.println("Search-> 已启动..."); latch.countDown(); try { // 建立DatagramSocket对象,并将其绑定到本地主机上的指定端口。 socket = new DatagramSocket(20000); while (!isClosed) { byte[] receiverData = new byte[1024]; // 建立DatagramPacket对象,用于接收数据包 DatagramPacket receiverPacket = new DatagramPacket(receiverData, receiverData.length); // 使用DatagramSocket对象,接收数据报包。 // 该方法阻塞,直到接收到数据报 socket.receive(receiverPacket); // 拆接收的数据包 // 获取接收数据报的IP地址 String ip = receiverPacket.getAddress().getHostAddress(); // 获取数据报中的远程主机上的端口号 int port = receiverPacket.getPort(); // 获取接收到的数据的长度。 int length = receiverPacket.getLength(); // 数据缓冲区 byte[] buffer = receiverPacket.getData(); String data = new String(buffer, 0, length); System.out.println("Search-> " + new Device(ip, port, data)); } } catch (IOException ignore) { } finally { close(); } } private void close() { if (socket != null) { //关闭数据报套接字。 //全部当前阻塞的线程在receive(java.net.DatagramPacket)在此套接字将抛出一个SocketException 。 socket.close(); } socket = null; } private void exit() { isClosed = true; close(); } } }
若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)