Java技术文章

java 网络编程 UDP协议

2015-09-01  本文已影响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

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

通过调用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

上一篇下一篇

猜你喜欢

热点阅读