• 14.6 基于UDP协议的网络编程



    UDP协议是一种不可靠的网络协议,它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只负是发送、接受数据报的对象。Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表发送、接受的数据。

    一、UDP协议基础

    1、UDP协议是英文User Datagram Protocol的缩写,即用户数据报协议,主要用于支持那些需要在计算机之间传输数据的网络连接。
    2、UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方法先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说UDP协议是一种不可靠的协议。UDP协议适用于依一次只传送少量的数据、对可靠性要求不高的环境。
    3、UDP协议直接位于IP协议之上,实际上IP协议属于OSI参考模型的网络层协议,而UDP,TCP属于网络传输层。
    4、因为UDP协议是面向非连接的协议,没有建立连接过程,因此它的效率很高;但是可靠性不如TCP协议。
    5、UDP协议主要作用是完成网络数据流和数据报之间的转换——在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去;在信息接收端,UDP协议将数据报转换成实际的数据内容。
    6、UDP协议、TCP协议简单对比
    (1)TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
    (2)UDP协议:不可靠,差错控制开销小,传输大小控制在64K一下,不需要建立连接。

    二、DatagramSocket发送、接受数据

    DatagramSocket本身只是码头,不维护状态,不能产生IO流,它唯一的作用就是接受和发送数据报。

    2.1 DatagramSocket构造器

    (1)DatagramSocket(): 创建一个DatagramSocket实例,并将对象绑定到本地计算机默认的IP地址,本机所有可用端口中随机选择某个端口。
    (2)DatagramSocket(int port) :创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、本机指定端口。
    (3)DatagramSocket(int port,InetAddress laddr):创建一个DatagramSocket实例,并将该对象绑定到指定IP地址、指定端口。
    通常在创建服务器时,创建指定端口的DatagramSocket实例——这样保证其他客户端可以将数据发送到该服务器。

    2.2 发送、接受数据的方法

    (1)receive(DatagramPacket p):从该DatagramSocket对象接收数据报
    (2)send(DatagramPacket p):以该DatagramSocket对象向外发送数据报
    使用DatagramSocket发送数据时,DatagramSocket并不知道将该数据发送到哪里,二十有DatagramPacket自身决定数据报的目的地。就像码头不知道每个集装箱的目的地,码头只是将这些集装箱发送出去,而集装箱本身包含了该集装箱的目的地。

    2.3 DatagramPacket的构造器

    (1)DatagramPakcet(byte[] buf,int length):以一个空数组来创建DatagramPacket对象,该对象的作用是接收DatagramSocket中的数据放入buf中,最多放入length个字节。
    (2)DatagramPacket(byte[] buf,int length,InetAddress addr,int port): 以一个包含数据的数组来创建DatagramPacket发送对象,创建该DatagramPacket对象时还指定了IP指定和端口---这就决定了该数据包的目的地。
    (3)DatagramPacket(byte[] buf,int offset,int length): 以一个空数组来创建DatagramPacket对象,并指定接收到的数据放入buf数组中从offset开始,最多放length个字节。
    (4)DatagramPacket(byte[] buf,int offset,int length,InetAddress addr,int port):创建DatagramPacket发送对象,指定发送buf数组中从offset开始,总共length字节的数组。
    当Client/Server程序使用UDP协议时,实际上并没有明显的服务器端和客户端,因为双发都需要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket对象作为传输数据的载体。通常固定IP地址、固定端口的DatagramSocket对象所在的程序被称为服务器,因为该DatagramSocket可以主动接受客户端数据。
    在接受数据之前,应该采用上面(1)/(3)构造器生成一个DatagramPacket对象,给出接受数据的字节数组和长度。然后调用DatagramSocket的receive()方法等待数据报到来,receive()将一直等待(该方法会阻塞调用该方法的线程),直到收到一个数据报为止。代码如下:

    //创建一个接受数据的DatagramPacket对象
    var packet=new DatagramPacket(buf,256);
    //接受数据报
    socket.receive();
    

    在发送数据之前,调用(2)/(4)构造器,此时的字节数据里存放了想发送的数据。除此之外,话需要给出完整的目的地地址,包括IP地址和端口号。发送数据通过DatagramSocket的send()方法实现,send()方法更具数据报的目的地地址来寻径以传送数据。代码如下:

    //创建一个发送数据的DatagramPacket对象
    var packet=new DatagramPacket(buf,length,address,port);//此时buf已经装好数据
    //发送数据
    socket.send(packet);
    

    注意:DatagramPacket还有一个getData()方法,该方法可以返回DatagramPacket对象里封装的字节数组。但是我们可以直接访问传给DatagramPacket构造器的字节数组,无需调用该方法,这样显得这个设计有点多余。

    2.4 获取数据发送者的ip地址和端口

    当服务器(或客户端)接收到了一个DatagramPacket对象之后,如果想向该数据报的发送者“反馈”一些信息,但由于UDP是相面非链接的,所以接收者并不知道每个数据报由谁发送过来,但程序可以调用DatagramPacket的如下三个方法来获取发送者的IP地址和端口

    1. InetAddress getAddress(): 当程序准备发送此数据报时,该方法返回此数据报对应目标机器的IP地址;当程序刚收到一份数据报时,该该方法返回该数据报发送者的IP地址。
    2. int getPort(): 与getAddress类似,不过getAddress返回的是IP地址,而getPort返回的是端口.
    3. SocketAddress getSocketAddress(): 当程序准备发送此数据报时,该方法返回此数据报的目标SocketAddress(ip+port);当程序该接收到一个数据报时,该方法返回该数据报的发送主机的SocketAddress.

    2.5 程序实例

    本程序的服务器端使用了循环1000次来读取DatagramSocket中的数据报,每当读取到内容之后便向该数据包的发送者送回一条消息。服务器端的程序代码如下:

    package UDP_NET;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    
    public class UdpServer
    {
        public static final int PORT=30000;
        //定义每个数据报的大小最大为4kB
        private static final int DATA_LEN=4096;
        //定义接受网络端数据的字节数组
        byte[] inBuff=new byte[DATA_LEN];
        //以指定字节数组创建准备接受数据的DatagramPacket对象
        private DatagramPacket inPacket=new DatagramPacket(inBuff,DATA_LEN);
        //定义一个用于发送的DatagramPacket对象
        private  DatagramPacket outPacket;
        //定义一个字符串数组,服务器端发送该数组的元素
        String[] books=new String[]{
                "疯狂Java讲义",
                "轻量级Java EE企业应用实战",
                "疯狂Android讲义",
                "疯狂Ajax讲义"
        };
        public void init() throws IOException
        {
            try(
                    //创建DtatgramSocket对象
                    var socket=new DatagramSocket(PORT))
            {
                //采用循环接受数据
                for(var i=0;i<1000;i++)
                {
                    //读取socket中的数据,将读取到的数据放入inPacket
                    socket.receive(inPacket);
                    //判断inPacket.getData()和inbuff是否为同一个数组
                    System.out.println(inBuff==inPacket.getData());
                    //将接收到的内容装欢为字符串输出
                    System.out.println(new String(inBuff,0,inPacket.getLength()));
                    //从字符串数组中取出一个元素作为发送数据
                    byte[] sendData=books[i%4].getBytes();
                    //以指定字节数组作为发送数据,以刚接收到的inPacket的源作为目标SocketAddress创建DatagramPacket
                    outPacket=new DatagramPacket(sendData,sendData.length,inPacket.getSocketAddress());
                    //发送数据
                    socket.send(outPacket);
                }
            }
        }
        public static void main(String[] args) throws IOException
        {
            new UdpServer().init();
        }
    }
    

    该程序可以接受1000个客户端发送过来的消息。
    客户端代码采用不断循环读取用户键盘输入,每当读到用户输入的内容后就将该内容封装成DatagramPacket数据报,再将该数据报发送出去;接着把DatagramSocket中的数据读入接收用的DatagramPacket中(实际上是读入该DatagramPacket所封装的字节数组中)。客户端的程序代码如下:

    package UDP_NET;
    
            import java.io.IOException;
            import java.net.DatagramPacket;
            import java.net.DatagramSocket;
    
    public class UdpServer
    {
        public static final int PORT=30000;
        //定义每个数据报的大小最大为4kB
        private static final int DATA_LEN=4096;
        //定义接受网络端数据的字节数组
        byte[] inBuff=new byte[DATA_LEN];
        //以指定字节数组创建准备接受数据的DatagramPacket对象
        private DatagramPacket inPacket=new DatagramPacket(inBuff,DATA_LEN);
        //定义一个用于发送的DatagramPacket对象
        private  DatagramPacket outPacket;
        //定义一个字符串数组,服务器端发送该数组的元素
        String[] books=new String[]{
                "疯狂Java讲义",
                "轻量级Java EE企业应用实战",
                "疯狂Android讲义",
                "疯狂Ajax讲义"
        };
        public void init() throws IOException
        {
            try(
                    //创建DtatgramSocket对象
                    var socket=new DatagramSocket(PORT))
            {
                //采用循环接受数据
                for(var i=0;i<1000;i++)
                {
                    //读取socket中的数据,将读取到的数据放入inPacket
                    socket.receive(inPacket);
                    //判断inPacket.getData()和inbuff是否为同一个数组
                    System.out.println(inBuff==inPacket.getData());
                    //将接收到的内容装欢为字符串输出
                    System.out.println(new String(inBuff,0,inPacket.getLength()));
                    //从字符串数组中取出一个元素作为发送数据
                    byte[] sendData=books[i%4].getBytes();
                    //以指定字节数组作为发送数据,以刚接收到的inPacket的源作为目标SocketAddress创建DatagramPacket
                    outPacket=new DatagramPacket(sendData,sendData.length,inPacket.getSocketAddress());
                    //发送数据
                    socket.send(outPacket);
                }
            }
        }
        public static void main(String[] args) throws IOException
        {
            new UdpServer().init();
        }
    }
    

    上面的程序同样使用DatagramSocket发送、接受DatagramPacket,这些代码与服务器端代码基本相似。而客户端于服务器端的唯一区别在于:服务器端的IP地址、端口号是固定的,所以客户端可以直接将该数据发送给服务端,而服务器择需要根据接收到的数据报来决定“反馈”数据报的目的地。
    使用DatagramSocket进行网络通信时,服务器端也无需知道每个客户端的状态,客户端把数据报发送到服务器端后,完全有可能立即退出。但不管客户端是否退出,服务器端都无法直到客户端的状态。
    上面程序运行结果:

    三、使用MulticastSocket实现多点广播(或多点发送)

    3.1 多点广播介绍

    DatagramSocket只允许数据报发送给指定目标地址,而MulticastSocket可以将数据报以广播的方式发送到多个客户端。

    MulticastSocket:该类是DatagramSocket的子类,作用是可以将数据报以广播方式发送到多个通信实体。若要使用多点发送(多点广播),则需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报,不仅如此,MulticastSocket既可以接收广播也可以发送广播。

     IP多点发送(多点广播)实现了将单一信息发送到多个通信实体(接收者),其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,通信实体需要在发送或者接收广播信息之前,加入到该组即可。同时IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0 至 239.255.255.255。

    从上图可以看出,MulticastSocket类是实现多点广播的关键,当MulticastSocket把一个DatagramPacket发送到多点广播的IP地址时,该数据将自动广播到加入该地址的所有MulticastSocket,MulticastSocket既可以将数据发送到多点广播的地址,也可以接受其他主机的广播信息。

    3.2 MulticastSocket的构造器

    MulticastSocket是DatagramSocket大的子类,即MulticastSocket是特殊的DatagramSocket。当要发送一个数据报时,可以使用随机端口创建一个MulticastSocket,也可以在指定端口创建MulticastSocket。MulticastSocket提供了如下三个构造器:

    1. MulticastSocket() :使用本机默认IP地址,随机端口来创建MulticastSocket对象
    2. MulticastSocket(int port):使用本机默认IP地址以及指定端口来创建MulticastSocket对象
    3. MulticastSocket(SocketAddress socketAddress):使用指定IP地址以及指定端接口来创建MulticastSocket对象
      **Ps:若创建仅用于发送数据报的MulticastSocket对象,则使用默认IP地址,随机端口即可;反之,若创建用于接收数据报的MulticastSocket对象,则必须指定端口,否则发送方无法确定发送数据报的目标端口 **

    3.2 MulticastSocket对象加入/脱离指定的多点广播的地址

    创建了MulticastSocket对象后,还需要将MulticastSocket加入到指定的多点广播的地址:
     1.joinGroup(InetAddress multicastAddr):将该MulticastSocket加入指定的多点广播地址
     2.leaveGroup(InetAddress multicastAddr):让MulticastSocket离开指定的多点广播地址

    3.3 使用指定网络接口/查询监听的网络接口

    在某些系统上可能有多个网络接口:
    1.setInterface():强制MulticastSocket使用指定的网络端口
    2.getInterface():查询MulticastSocket监听的网络端口

    3.4 发送/接受数据和设置广播信息的ttl

    MulticastSocket是DatagramSocket的子类,因此继承了DatagramSocket发送和接受数据的方法:
    (1)receive(DatagramPacket p):从该MulticastSocket对象接收数据报
    (2)send(DatagramPacket p):以该MulticastSockett对象向外发送数据报
    支持多点广播的MulticastSocketh还多一个seTimeToLive(int ttl)方法,可设置广播信息的ttl(Time-To-Live),该ttl参数用于设置数据报最多可以跨过多少个网络:
    当TTL的值为 0 时,指定数据报应停留在本地主机中;
    当TTL的值为 1 时,指定将数据报发送到本地局域网中;
    当TTL 的值为 32 时,意味着只能将数据报发送到本站点的网络上;
    当TTL 的值为 64 时,意味着数据报应被保留在本地区;
    当TTL 的值为 128 时,意味着数据报应被保留在本大洲;
    当TTL 的值为 255 时,意味着数据报可被发送到所有地方;
    在默认情况下,TTL 的值为1。

    3.5 程序实例

    使用MulticastSocket进行多点广播时所有的通信实体都是平等的,他们将自己的数据报发送到多点广播IP地址,并使用MulticastSocket接受其他其他人发送的广播数据报。下面程序使用MulticastSocket实现了一个基于广播的多人聊天室。程序只需要一个MulticastSocket,两个线程,其中MulticastSocket既用于发送也用于接受;一个线程负责接受用户的键盘输入,并向MulticastSocket发送数据,另一个线程负责从MulticastSocket中读取数据。

    package Multicast;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.net.MulticastSocket;
    import java.util.Scanner;
    
    //让该类实现Runnable接口,该类的实例可以作为线程的target
    public class MulticastSocketTest implements Runnable
    {
        //使用常量作为本程序的多点广播IP地址
        private static final String BROADCAST_IP="230.0.0.1";
        //使用常量作为本程序多点广播目的地的端口
        public static final int BRAODCAST_PORT=30000;
        //定义每个数据报大小最大为4KB
        private static final int DATA_LEN=4096;
        //定义本程序的MulticastSocket实例
        private MulticastSocket socket=null;
        private InetAddress broadcastAddress=null;
        private Scanner scan=null;
        //定义接收网络数据的字节数组
        byte[] inBuff=new byte[DATA_LEN];
        //以指定字节数组创建准备接收数据的DatagramPacket对象
        private DatagramPacket inPacket=new DatagramPacket(inBuff,inBuff.length);
        //定义一个用于发送的DatagramPacket
        private DatagramPacket outPacket=null;
    
        public void init() throws IOException
        {
            try {
                //创建键盘输入流
                scan = new Scanner(System.in);
                //用于接受和发送数据报
                //该对象需要接收数据,所以有指定端口
                socket=new MulticastSocket(BRAODCAST_PORT);//1
                broadcastAddress=InetAddress.getByName(BROADCAST_IP);
                //将该socket加入指定的多点广播地址
                socket.joinGroup(broadcastAddress);//2
                //设置本MulticastSocket发送数据报会被回送到本身
                socket.setLoopbackMode(false);//3
                //初始化发送用的DatagramSocket,包含一个长度为0的字节数组
                outPacket=new DatagramPacket(new byte[0],0,broadcastAddress,BRAODCAST_PORT);
                //启动本实例的run()方法z作为线程的执行体
                new Thread(this).start();
                //不断读取键盘的输入
                while(scan.hasNextLine())
                {
                    //将键盘输入字符串转换为字节数组
                    byte[] buff=scan.nextLine().getBytes();
                    //设置用于发送用的DatagramPacket里的字节数据
                    outPacket.setData(buff);
                    //发送数据报
                    socket.send(outPacket);
                }
            }
            finally {
                scan.close();
                socket.close();
            }
    
        }
        @Override
        public void run()
        {
            try
            {
                while(true)
                {
                    //读取socket中的数据,读到的数据放在inPacket所封装的字节数组中
                    socket.receive(inPacket);
                    //打印输出从socket中读取到的数据
                    System.out.println(new String(inBuff,0,inPacket.getLength()));
                }
            }
            catch (IOException e)
            {
                e.printStackTrace();
                try
                {
                    if(socket!=null)
                    {
                        //让该Socket离开多点广播地址
                        socket.leaveGroup(broadcastAddress);
                        //关闭该Socket对象
                        socket.close();
                    }
                    System.exit(1);
                }
                catch (IOException ex)
                {
                    ex.printStackTrace();
                }
            }
        }
        public  static void main(String[] args) throws IOException
        {
            new MulticastSocketTest().init();
        }
    }
    

    init()方法里的代码1处先创建了一个MulticastSocket对象,由于需要使用该接口接受数据,所以未该MulticastSocket对象设置使用固定端口;代码2处将该MulticastSocket对象添加到指定的多点广播IP地址,代码3设置本MulticastSocket发送数据报会被回送到本身(技改MulticastSocket可以接收自己发送的数据报),其他发送/接收数据报和DatagramSocket没有什么区别。
    可以看到运行效果:

  • 相关阅读:
    hdu 6214 : Smallest Minimum Cut 【网络流】
    hdu 6205: card card card【输入挂】
    UVa 10054 : The Necklace 【欧拉回路】
    hdu 6127 : Hard challenge (2017 多校第七场 1008)(计算几何)
    hdu 6143: Killer Names (2017 多校第八场 1011)
    hdu 6134: Battlestation Operational (2017 多校第八场 1002)【莫比乌斯】
    hdu 4992 Primitive Roots 【求原根模板】
    poj 1284 : Primitive Roots
    codevs 2804 最大最小数质因数
    codevs 2370 小机房的树
  • 原文地址:https://www.cnblogs.com/weststar/p/12964472.html
Copyright © 2020-2023  润新知