美文网首页Java技术文章
java 网络编程 UDP协议

java 网络编程 UDP协议

作者: 肉团先生 | 来源:发表于2015-09-01 09:45 被阅读1187次

    UDP协议

    英文:User Datagram Protocol即:用户数据报协议。
    不可靠,传输少量的数据(限制在64KB下),效率高,在两端建立socket(负责发送和接收,无服务端和客户端的概念),位于传输层,而IP为网络层

    使用场景:网络游戏,视频会议,实时性高的情况。
    主要作用:完成网络数据流和数据报之间的转换。在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去,接收端为逆过程。

    传输层和网络层有什么区别呢?

    网络层(IP层)提供点到点的连接即提供主机之间的逻辑通信,传输层提供端到端的连接——提供进程之间的逻辑通信。

    那上面的端对端,即:什么是端口呢

    为了使运行不同操作系统的计算机的应用进程能够互相通信,就必须用统一的方法对TCP/IP体系的应用进程进行标志。
    解决这个问题的方法就是在运输层使用协议端口号(protocol port number),或通常简称为端口(port)

    简单理解,IP的地址负责的是点到点,而仅仅通过IP来连接到对方的电脑是不够的,因为电脑中要很多的应用程序,到底是将数据传输给那个应用程序呢?我们还要需要端口(TCP/IP)来区分是哪一个应用程序。两者结合达到网络通信。

    与TCP协议的比较

    TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

    UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

    java是怎么使用UDP协议达到网络通信的呢?

    java使用DaatagramSocket代表UDP协议的socket,它本身只是码头,只负责接收和发送数据报,使用DatagramPacket来代表数据报。发送的数据通过这个对象进行完成。
    DatagramPacket来决定数据报的目的地
    实际上,还是可以区分出服务端和客户端的,下面展示服务端与客户端和客户端与客户端的传输。

    服务端与客户端

    TCPsocket中,我们需要建立连接后再进行传输,所以有明显的服务端和客户端之分。而UDP则不同,不需要建立连接,直接往目标进行发送数据。但还是可以人为的指定好客户端和服务端的。服务端的特性是有固定的IP和端口
    下面展示用法:
    服务端

    public class UDPServer {
        public static final int PORT=30000;
        //定义每个数据报的最大的大小为4KB
        public static final int DATA_LEN=4096;
        //定义接受网络数据的字节数组
        byte[] inBuff=new byte[DATA_LEN];
        //已指定字节数组创建准备接受数据的DatagramPacket对象
        private DatagramPacket inPacket=new DatagramPacket(inBuff, inBuff.length);
        //定义发送的DatagramPacket对象
        private DatagramPacket outPacket;
        //定义一个字符串数组,服务器发送该数组的元素
        String[] book=new String[]{"I","am","Stu"};
        public void init(){
            try {
                //创建datagramsocket对象
                DatagramSocket socket=new DatagramSocket(PORT);
                {
                    //采用循环接受数据
                    for(int i=0;i<1000;i++){
                        //读取inPacket的数据
                        socket.receive(inPacket);
                        //判断getData()和inbuf是否为同一个数组
                        System.out.println(inPacket.getData()==inBuff);
                        System.out.println(socket.getSoTimeout());
                        //将接受后的内容转化为字符串进行输出
                        System.out.println(new String(inBuff,0,inPacket.getLength()));
                        //从字符串中取出一个元素作为发送数据
                        byte[] sendData=book[i%4].getBytes();
                        //已指定的字符数组作为发送数据,以刚接手到的datagramPacket的
                        //源socketAddress作为目标socketAddress创建DatagramPacket
                        outPacket=new DatagramPacket(sendData,sendData.length, inPacket.getSocketAddress());//通过这个getSocketAddress就可以得到相应的IP地址和端口了
                        //发送数据
                        socket.send(outPacket);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args) {
            new UDPServer().init();
        }
    }
    

    相应的客户端:

    public class UdpClient {
        // 定义数据报的目的地
        public static final int DEST_PORT = 30000;
        public static final String DEST_IP = "127.0.0.1";
        // 定义每个数据报的大小,最大为4kb
        ...//更UdpServer一样相应成员的声明
        public void init(){
            try {
                //创建一个客户端DatagramSocket使用随机端口
                DatagramSocket socket=new DatagramSocket();
                outPacket=new DatagramPacket(new byte[0], 0,InetAddress.getByName(DEST_IP),DEST_PORT);
                //创建键盘输入流
                Scanner scan=new Scanner(System.in);
                //不断读取键盘输入
                while(scan.hasNextLine()){
                    //将键盘输入的一行字符串转换字节数组
                    byte[] buff=scan.nextLine().getBytes();
                    //设置发送用到的DatagramPacket中的字节数据
                    outPacket.setData(buff);
                    //发送数据报
                    socket.send(outPacket);
                    //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
                    socket.receive(inPacket);
                    System.out.println(new String(inBuff,0,inPacket.getLength()));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args) {
            new UdpClient().init();
        }
    }
    

    通过以上的代码,我们可以总结以下规律,即:使用步骤

    1. 定义好IP地址和端口,以及字节数组的大小(最终以此形式传输)并初始化输入的DatagramPacket
    2. 创建DatagramSocket实例(监听端口,S/C都各自不同)
    3. S和C都有所不同
      3.1 S则先进行socket.receive(inPacket);
      3.2 而C则创建outPacket(IP地址和端口,字节数组),并使用outPacket.setData(buff);将需要发送的数据进行设置,再使用socket.send(outPacket);进行发送
    4. 当发送后的就进行等待接受,接受完的进行处理并发送的一个循环里面,这优点像进程间通信的情况

    使用当中的注意点

    1. 一定要注意DatagramPacket的设置,因为它指定发送给哪一个应用程序,通过接受的数据报可等到相应的ip地址和端口
    2. socket.receive(inPacket);是阻塞的。必要时需要对应启动一个线程。

    针对注意的第一点,我们看客户端对客户端的传输

    其实很简单,我们只要对每个客户端的DatagramSocket监听不同的端口,而使用DatagramPacket指定要传输的端口和IP地址即可。我们只需要对上面的类UdpClient进行修改如下:
    第一个客户端

    DatagramSocket socket=new  DatagramSocket(30001);//添加端口,而不是随机端口,
    outPacket=new  DatagramPacket(new  byte[0],0,InetAddress.getByName(DEST_IP),30000);//将要传输放的端口和ip设置
    

    第二个客户端则相反即可

    DatagramSocket socket=new  DatagramSocket(30000);//添加端口,而不是随机端口,
    outPacket=new  DatagramPacket(new  byte[0],0,InetAddress.getByName(DEST_IP),30001);//将要传输放的端口和ip设置
    

    针对注意第二点,当socket.receive(inPacket);如何不阻塞主线程

    下面我对相应的代码进行了封装和优化,并让类实现Runnable接口

    public class UdpClient implements Runnable {
        // 定义数据报的目的地
        public static final int DEST_PORT = 30001;
        public static final String DEST_IP = "127.0.0.1";
        ...//相应成员的声明
    
        public UdpClient() {
            try {
                // 创建一个客户端DatagramSocket使用随机端口
                socket = new DatagramSocket(DEST_PORT);
                // 初始化发送数据报,包含一个长度为0的字节数组
                outPacket = new DatagramPacket(new byte[0], 0,
                        InetAddress.getByName(DEST_IP), 30000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 发送字符串坐标strxy,用于使用ai先手的情况
         *
         * @param strXY
         */
        public void sendPointXy(String strXY) {
            byte[] buff = strXY.getBytes();
            outPacket.setData(buff);// 设置数据报的字节数据
            try {
                // 发送数据报
                socket.send(outPacket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        /**
         * 进行接受坐标strXY,并进行处理进行下棋,最后进行发送
         * 
         * @param strXY
         */
        public void receiverPointXy(String content) {
            String[] strXY = content.split(",");
            ...//对数据进行处理
            sendPointXy(chComputer.getX() + "," + chComputer.getY());
        }
    
        /**
         * 成为被动接受方
         */
        public void acceptPointXy() {
            try {
                socket.receive(inPacket);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            receiverPointXy(new String(inBuff, 0, inPacket.getLength()));
        }
    
        @Override
        public void run() {
            //死循环不断地进行监听
            while(true){
                acceptPointXy();
            }
        }
    
    }
    

    从上面的代码可以看到,当我们发送后,再也不进行receive,而是交给线程的run中进行死循环地receive监听。并把send方法和receive方法封装出来,是为了区分谁先发送,谁监听的情况。否则发送方就无法将数据到达目的地。

    如何让receive跳出?

    也就是告诉等待的对方,我已经退出了,不需要继续等待呢?
    这里分两种情况的方法,一种是正常退出,另一种是非正常退出。

    正常退出:

    传入特定的字符进行判断对方已经退出了,并主动停止等待。我根据上一个代码块进行修改如下:

    public static boolean isRunning=true;//默认true;
    public void receiverPointXy(String content) {
            //首先进行判断是否关闭的情况
            if(content.isEmpty()){//当正常关闭的时候,对方发送空字符来标示关闭
                isRunning=false;//不再循环等待
                if(socket!=null)
                    socket.close();
                return ;
            }
            ....
    }
    public void run() {
            //isRunning进行标识是否等待
            while(isRunning){
                acceptPointXy();
            }
        }
    

    非正常退出:

    程序遇到异常,网络不正常的时候,我们可以设置timeout来停止等待:

    socket.setSoTimeout(60000);// 设置等待时间为1分钟
    

    那么你可能会好奇,默认的Timeout是多久呢?我们通过查看源码的注释

    /**
    * ...
    * A timeout of zero is interpreted as an infinite timeout.
    */
    public synchronized void setSoTimeout(int timeout) throws SocketException {
            if (isClosed())
                throw new SocketException("Socket is closed");
            getImpl().setOption(SocketOptions.SO_TIMEOUT, new Integer(timeout));
        }
     DatagramSocketImpl getImpl() throws SocketException {
            if (!created)
                createImpl();
            return impl;
        }
     /**
         * Retrieve setting for SO_TIMEOUT.  0 returns implies that the
         * option is disabled (i.e., timeout of infinity).
    */
    public synchronized int getSoTimeout() throws SocketException {
            if (isClosed())
                throw new SocketException("Socket is closed");
            if (getImpl() == null)
                return 0;
                ...
    }
    

    通过查看源码可以发现,当我们执行setSoTimeout的时候,将调用getImpl()方法创建DatagramSocketImpl,使得getImpl()!=null,返回不为零的数字。所以,默认的timeout0。但是为零的情况,我们通过看方法的注释可以发现,为零意味着无限等待

    使用MulticastSocket实现多点广播

    使用MulticastSocket可以将数据报以广播方式发送到数量不等的多个客户端

    原理

    若要使用多点广播时,则需要让一个数据报有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。
    IP多点广播(或多点发送)实现了将单一信息发送到多个接收者的广播
    其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端需要发送、接收广播信息时,加入到该组即可。

    IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255

    多点广播的示意图多点广播的示意图

    注意MulticastSocket重要的属性:

    使用jionGroup()方法来加入指定组;使用leaveGroup()方法脱离一个组。
    使用setTimeToLive(int ttl)方法,通过参数ttl来指定最多可以跨过多少个网络。(默认情况ttl=1

    • ttl=0时,指定数据报应停留在本地主机
    • ttl=1时,指定数据报发送到本地局域网
    • ttl=32时,指定数据报只能发送到本站点的网络上
    • ttl=64时,指定数据报应保留在本地区
    • ttl=128时,指定数据报应保留在本大洲
    • ttl=64时,指定数据报可发送到所有地方

    当在某些系统中,可能有多个网络接口:(将会对多点广播带来问题)

    通过调用setInterface可选择MulticastSocket使用的网络接口;也可以使用getInterface方法查询MulticastSocket监听的网络接口。

    示例代码:

    public class MulticastSocketTest implements Runnable {
        // 使用常量作为本程序的多点广播IP地址
        private static final String BROADCAST_IP = "230.0.0.1";
        // 使用常量作为本程序的多点广播目的的端口
        public static final int BROADCAST_PORT = 30000;
        // 定义每个数据报的最大大小为4K
        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 {
                // 创建用于发送、接收数据的MulticastSocket对象
                // 因为该MulticastSocket对象需要接收,所以有指定端口
                socket = new MulticastSocket(BROADCAST_PORT);
                broadcastAddress = InetAddress.getByName(BROADCAST_IP);
                // 将该socket加入指定的多点广播地址
                socket.joinGroup(broadcastAddress);
                // 设置本MulticastSocket发送的数据报被回送到自身
                socket.setLoopbackMode(false);
                // 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
                outPacket = new DatagramPacket(new byte[0], 0, broadcastAddress,
                        BROADCAST_PORT);
                // 启动以本实例的run()方法作为线程体的线程
                new Thread(this).start();
                // 创建键盘输入流
                scan = new Scanner(System.in);
                // 不断读取键盘输入
                while (scan.hasNextLine()) {
                    // 将键盘输入的一行字符串转换字节数组
                    byte[] buff = scan.nextLine().getBytes();
                    // 设置发送用的DatagramPacket里的字节数据
                    outPacket.setData(buff);
                    // 发送数据报
                    socket.send(outPacket);
                }
            } finally {
                socket.close();
            }
        }
    
        public void run() {
            try {
                while (true) {
                    // 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                    socket.receive(inPacket);
                    // 打印输出从socket中读取的内容
                    System.out.println("聊天信息:"
                            + new String(inBuff, 0, inPacket.getLength()));
                }
            }
            // 捕捉异常
            catch (IOException ex) {
                ex.printStackTrace();
                try {
                    if (socket != null) {
                        // 让该Socket离开该多点IP广播地址
                        socket.leaveGroup(broadcastAddress);
                        // 关闭该Socket对象
                        socket.close();
                    }
                    System.exit(1);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) throws IOException {
            new MulticastSocketTest().init();
        }
    }
    

    多次按ctrl+f11生成多个示例进行测试。可以发现,使用MulticastSocket监听统一端口,多个示例并不会产生端口被占用的错误。再者监听的IP地址要为IP协议上的广播地址

    扩展:(聊天室的实现)
    使用MulticastSocket实现多点广播(2)

    参考资料

    TCP、UDP详解
    17.4.3 使用MulticastSocket实现多点广播(1),这个居然是《java疯狂讲义第二部》的全教程。
    java.net.MulticastSocket Example

    相关文章

      网友评论

        本文标题:java 网络编程 UDP协议

        本文链接:https://www.haomeiwen.com/subject/pyclcttx.html