Socket网络编程(二):Socket UDP快速入门

博客主页java

UDP是Socket中重要组成部分,下面主要想带你们一块儿了解什么是UDP,以及UDP能够用来作什么。编程

1. UDP是什么?

UDP全称为User Datagram Protocol,缩写为UDP,称为用户数据报协议,也叫用户数据报文协议。它是一个简单的面向数据报的传输层协议,正式规范为RFC 768。在上一篇Socket网络编程理论知识中介绍了UDP是一种面向无链接的协议,所以,在通讯时发送端和接收端不用创建链接。segmentfault

UDP通讯的过程就像是货运公司在两个码头间发送货物同样,在码头发送和接收货物时都须要使用集装箱来装载货物,UDP通讯也是同样,发送和接收的数据也须要使用“集装箱”进行打包。数组

UDP为何不可靠呢?缓存

  1. 它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份
  2. UDP在IP数据报的头部仅仅加入了复用和数据校验
  3. 发送端生产数据,接收端从网络中抓取数据
  4. 结构简单,无检验,速度快,容易丢包,可广播

2. 基于UDP协议的能作什么呢?

2.1 网页或者APP的访问

原来访问网页和手机APP都是基于HTTP协议的。HTTP协议是基于TCP的,创建链接都须要屡次交互,对于时延比较大的目前主流的移动互联网来说,创建一次链接须要的时间会比较长,然而既然是移动中,TCP可能还会断了重连,也是很耗时的。并且目前的HTTP协议,每每采起多个数据通道共享一个链接的状况,这样原本为了加快传输速度,可是TCP的严格顺序策略使得哪怕共享通道,前一个不来,后一个和前一个即使不要紧,也要等着,时延也会加大。服务器

而QUIC(全称Quick UDP Internet Connections,快速UDP互联网链接)是Google提出的一种基于UDP改进的通讯协议,其目的是下降网络通讯的延迟,提供更好的用户互动体验。网络

QUIC在应用层上,会本身实现快速链接创建、减小重传时延,自适应拥塞控制,是应用层“城会玩”的表明。这一节主要是讲UDP,QUIC咱们放到应用层去讲。数据结构

2.2 流媒体的协议

如今直播比较火,直播协议多使用RTMP,这个协议咱们后面的章节也会讲,而这个RTMP协议也是基于TCP的。TCP的严格顺序传输要保证前一个收到了,下一个才能确认,若是前一个收不到,下一个就算包已经收到了,在缓存里面,也须要等着。对于直播来说,这显然是不合适的,由于老的视频帧丢了其实也就丢了,就算再传过来用户也不在乎了,他们要看新的了,若是总是没来就等着,卡顿了,新的也看不了,那就会丢失客户,因此直播,实时性比较比较重要,宁肯丢包,也不要卡顿的。dom

另外,对于丢包,其实对于视频播放来说,有的包能够丢,有的包不能丢,由于视频的连续帧里面,有的帧重要,有的不重要,若是必需要丢包,隔几个帧丢一个,其实看视频的人不会感知,可是若是连续丢帧,就会感知了,于是在网络很差的状况下,应用但愿选择性的丢帧。异步

还有就是当网络很差的时候,TCP协议会主动下降发送速度,这对原本当时就卡的看视频来说是要命的,应该应用层立刻重传,而不是主动让步。于是,不少直播应用,都基于UDP实现了本身的视频传输协议。

2.3 实时游戏

游戏有一个特色,就是实时性比较高。

实时游戏中客户端和服务端要创建长链接,来保证明时传输。可是游戏玩家不少,服务器却很少。因为维护TCP链接须要在内核维护一些数据结构,于是一台机器可以支撑的TCP链接数目是有限的,而后UDP因为是没有链接的,在异步IO机制引入以前,经常是应对海量客户端链接的策略。

另外仍是TCP的强顺序问题,对战的游戏,对网络的要求很简单,玩家经过客户端发送给服务器鼠标和键盘行走的位置,服务器会处理每一个用户发送过来的全部场景,处理完再返回给客户端,客户端解析响应,渲染最新的场景展现给玩家。

若是出现一个数据包丢失,全部事情都须要停下来等待这个数据包重发。客户端会出现等待接收数据,然而玩家并不关心过时的数据,激战中卡1秒,等能动了都已经死了。

游戏对实时要求较为严格的状况下,采用自定义的可靠UDP协议,自定义重传策略,可以把丢包产生的延迟降到最低,尽可能减小网络问题对游戏性形成的影响。

2.4 IoT物联网

一方面,物联网领域终端资源少,极可能只是个内存很是小的嵌入式系统,而维护TCP协议代价太大;另外一方面,物联网对实时性要求也很高,而TCP时延大。Google旗下的Nest创建Thread Group,推出了物联网通讯协议Thread,就是基于UDP协议的。

2.5 移动通讯领域

在4G网络里,移动流量上网的数据面对的协议GTP-U是基于UDP的。由于移动网络协议比较复杂,而GTP协议自己就包含复杂的手机上线下线的通讯协议。若是基于TCP,TCP的机制就显得很是多余。

3. UDP核心API

在UDP通讯中有2个经常使用的类:一个是数据包类DatagramPacket,一个是数据包发送接收器类DatagramSocket

根据API文档的内容,对UDP两个经常使用类进行分析:

3.1 DatagramPacket

在java中,提供了一个DatagramPacket类,该类的实例对象就至关于一个集装箱,用来封装UDP通讯中发送或者接收的数据。

首先须要了解下DatagramPacket的构造方法。在建立发送端和接收端的DatagramPacket对象时,使用的构造方法有所不一样,接收端的构造方法只须要接收一个字节数组来存放接收到的数据,而发送端的构造方法不但要存放发送数据的字节数组,还须要指定发送端的IP地址和端口号。

先来了解下DatagramPacket的构造方法:

  1. 使用该构造方法建立DatagramPacket对象时,指定了封装数据的字节数组和数据的大小,没有指定IP地址和端口号。说明只能用于接收端,不能用于发送端,由于发送端必定要明确指出数据的目的地(IP地址和端口号),而接收端不须要明确知道数据的来源,只须要接收便可


  1. 使用该构造方法建立DatagramPacket对象时,不只指定了封装数据的字节数组和数据大小,还指定了数据包的目标IP地址(address)和端口号(port)。该对象一般用于发送端,由于在发送数据时必须指定接收端的IP地址和端口号,就好像发送货物的集装箱上面必须标明接收人的地址同样。

其中SocketAddress对象封装了IP地址+端口号,至关于InetAddress+端口号port。

// 从SocketAddress子类的构造方法能够看出
InetSocketAddress(InetAddress addr, int port)


了解了DatagramPacket构造方法,接下来对DatagramPacket类中的经常使用方法进行说明:



3.2 DatagramSocket

DatagramPacket数据包的做用就如同是“集装箱”,能够将发送端或者接受端的数据封装起来。然而运输货物只有“集装箱”是不够的,还须要有码头。在程序中须要实现通讯只有DatagramPacket数据包也一样不行,为此JDK中提供一个DatagramSocket类。DatagramSocket类的做用就相似于码头,使用这个类的实例对象就能够发送和接收DatagramPacket数据包。

在建立发送端和接收端的DatagramSocket对象时,使用的构造方法有所不一样。

先来了解下DatagramSocket构造方法:

  1. 该构造用于建立发送端的DatagramSocket对象,在建立DatagramSocket对象时,并无指定端口号,此时,系统会分配一个没有被其它网络程序所使用的端口号。

  1. 该构造既可建立接收端的DatagramSocket对象,又能够建立发送端的DatagramSocket对象,在建立接收端DatagramSocket对象时,必需要指定一个端口号,就能够监听指定的端口。

了解了DatagramSocket构造方法,接下来对DatagramSocket类中的经常使用方法进行说明:

  1. receive(DatagramPacket p) 接收数据报包,接收到的数据封装到DatagramPacket,还包含发送者的IP地址和发件人机器上的端口号。该方法阻塞,直到接收到数据报。
  2. send(DatagramPacket p) 发送数据报包,DatagramPacket包括指示要发送的数据,其长度,远程主机的IP地址和远程主机上的端口号的信息。
  3. close() 关闭此数据报套接字。全部当前阻塞的线程在receive(java.net.DatagramPacket)在此套接字将抛出一个SocketException 。

4. 基于UDP协议的Socket程序函数调用过程,实现简单聊天案例

使用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();
    }
}

5.IP地址和端口号

5.1 IP地址

IP地址是指互联网协议地址(Internet Protocol Address)。IP地址用来给一个网络中的计算机设备作一个惟一的编号。在TCP/IP协议中,这个标识号就是IP地址。目前普遍使用的IP地址是IPv4。

IP地址分类:

  1. IPv4

它由4个字节大小的二进制数表示,如:00001010000100000010100100000001。因为二进制形式表示的IP地址很是不便记忆和处理,所以一般会将IP地址写成十进制的形式,每一个字节用一个十进制数字(0-255)表示,数字间用符号“.”分开,如 “192.168.1.100”。

  1. IPv6

随着计算机网络规模的不断扩大,对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的广播和多播相关知识:

  1. 255.255.255.255为受限的广播地址,即全部网段都能收到,但路由并不会去转发该广播,毕竟全部网段都会接受,因此只有本局域网可以接收到。
  2. X.X.X.255 为 C 类广播,只有该网段下的才能收到 ,好比 192.168.33.255,那么 192.168.33.X 下的全部网段都能接收到。
  3. D类IP地址为多播预留。
广播地址的计算方法:
  1. IP地址与子网掩码进行“与”运算,获得网络地址;
  2. 子网掩码“取反”运算,而后与网络地址进行“或”运算,获得广播地址;

如: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。

5.2 端口号

网络通讯,本质上是两个进程(应用程序)的通讯。每台计算机都有不少的进程,那么在网络通讯时,如何区分这些进程呢?
若是说IP地址能够惟一标识网络中的设备,那么端口号就能够惟一惟一标识设备中的进程(应用程序)了。

端口号是用两个字节表示整数,它的取值范围是0~65535。其中,0~1024之间的端口号用于一些知名的网络服务和应用,普通的应用程序须要使用1024以上的端口号,若是端口号被另外一个服务或者程序所占用,会致使当前程序启动失败。

利用协议+IP地址+端口号组合,就能够标识网络中的进程了,那么进程间的通讯能够利用这个标识与其它进程进行交互。

6. 案例实操,局域网搜索案例

实现一个局域网搜索案例:
首先绑定到本地主机上的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();
        }
    }
}

若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)

相关文章
相关标签/搜索