欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

NIO基于UDP协议的网络编程

程序员文章站 2022-07-08 19:44:43
...

 

UDP(User Datagram Protocol)协议是用户数据报协议的简称,也用于网络数据的传输。

UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。

虽然 UDP 协议是一种不太可靠的协议,但有时在需要较快地接收数据并且可以忍受较小错误的情况下,UDP 会比TCP表现出更大的优势。

UDP协议与TCP协议的简单对比:

  • TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大
  • UDP协议:不可靠,差错控制开销小,传输大小有限制,不需要建立连接。

一、DatagramPacket类和 DatagramSocket 类

UDP通信两个核心类:DatagramSocket类和DatagramPacket类。

1、DatagramPacket 类

该类表示一个数据报包。 

数据包是用来实现一个无连接的分组传送服务。每个消息都是从一台机器路由到另一个完全基于包含在该数据包内的信息。从一台机器发送到另一台机器的多个数据包可能会被不同的路由,并可能以任何顺序到达。包交付没有保证。

构造方法如下:

    • DatagramPacket(byte[] buf, int length)

      接收数据包长度 length DatagramPacket构建。

      DatagramPacket(byte[] buf, int length, InetAddress address, int port)

      指定主机上的指定端口发送数据包的长度 length数据报包结构。

      DatagramPacket(byte[] buf, int offset, int length)

      接收数据包长度 length DatagramPacket结构,指定一个偏移量到缓冲区。

      DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)

      发送有偏置 ioffsetto指定端口号指定主机上的数据包长度 length数据报包结构。

      DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)

      发送有偏置 ioffsetto指定端口号指定主机上的数据包长度 length数据报包结构。

      DatagramPacket(byte[] buf, int length, SocketAddress address)

      指定主机上的指定端口发送数据包的长度 length数据报包结构。

常用方法如下:

    • InetAddress getAddress()

      返回的IP地址的机器,这个数据包被发送或从收到的数据报。

      byte[] getData()

      返回数据缓冲区。

      SocketAddress getSocketAddress()

      获取SocketAddress(通常是IP地址+端口号)的远程主机,数据包被发送到或来自。

      void setData(byte[] buf)

      为这个数据包设置数据缓冲区。

      void setPort(int iport)

      设置远端端口号,这个数据包被发送。

2、DatagramSocket 类

该类代表一个发送和接收数据包的Socket。 

数据报套接字发送或者接收点的分组传送服务。每个发送的数据包或数据报套接字上接收单独寻址和路由。从一台机器发送到另一台机器的多个数据包可能会被不同的路由,并可以以任何顺序到达。

在可能的情况下,一个新建的DatagramSocket有SO_BROADCAST套接字选项已启用,以便允许广播数据报传输。为了收到广播包应该将DatagramSocket绑定到通配符地址。在一些实施方案中,广播包,也可以接受当一个DatagramSocket绑定到一个更具体的地址。

构造方法如下:

常用方法如下:

 

二、客户端与服务器端之间交互数据的简单Demo

基于UDP的套接字编程实现流程如下:

1. 发送数据包步骤:

    使用 DatagramSocket() 创建一个数据报套接字。不需要绑定到指定的ip和端口

    使用 DatagramPacket() 创建要发送的数据包,并指定目标SocketAddress

    使用 DatagramSocket 类的 send() 方法发送数据包。

2. 接收数据包步骤:

    使用 DatagramSocket 创建数据报套接字,并将其绑定到指定的ip和端口。

    使用 DatagramPacket 创建字节数组来接收数据包。

    使用 DatagramSocket 类的 receive() 方法接收 UDP 包。

尽管UDP协议对服务器和客户端之间并没有太大区分,事实上,我们通常将固定ip以及固定端口的DatagramSocket对象所在的程序被成为服务端,因为该DatagramSocket可以主动接收客户端数据。

1、服务器端

public class Server {
    public static void main(String[] args) {
        try {
            // 创建一个数据报套接字,并将其绑定到指定的端口
            DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 19999));
            // 定义接收数据的数据包
            byte[] inbuffer = new byte[4096];
            DatagramPacket inPacket = new DatagramPacket(inbuffer, inbuffer.length);

            while (socket.isClosed() == false) {
                // 接收数据
                socket.receive(inPacket);
                System.out.println("服务器端显示信息:" + new String(inbuffer, 0, inPacket.getLength()));

                // 原路把数据返回,发送数据
                SocketAddress clinetAddress = inPacket.getSocketAddress();
                System.out.println(clinetAddress); // /127.0.0.1:60652
                byte[] sendData = "服务器收到了".getBytes();
                DatagramPacket outPacket = new DatagramPacket(sendData, sendData.length, clinetAddress);
                socket.send(outPacket);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2、客户端

public class Client {
    public static void main(String[] args) {
        try {
            // 创建一个数据报套接字
            DatagramSocket socket = new DatagramSocket();
            // 定义发送数据的数据包
            DatagramPacket outPacket = new DatagramPacket(new byte[0], 0, new InetSocketAddress("127.0.0.1", 19999));

            //发送键盘上输入的数据
            Scanner scanner = new Scanner(System.in);
            while(scanner.hasNext()){
                byte[] sendData = scanner.nextLine().getBytes();
                outPacket.setData(sendData);
                socket.send(outPacket);

                // 定义接收数据的数据包,获取服务器端的响应的数据
                byte[] inbuffer = new byte[4096];
                DatagramPacket inPacket = new DatagramPacket(inbuffer, inbuffer.length);
                socket.receive(inPacket);
                System.out.println("客户端显示信息:" + new String(inbuffer, 0, inPacket.getLength()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三、NIO对UDP协议的支持

由于UDP协议是一个无连接的,对服务器和客户端之间并没有太大区分。所以NIO不存在不存在ServerDatagramChannel,存在DatagramChannel类。DatagramChannel类:连接到UDP包的通道。对应UDP的DatagramSocket类。

注意:使用NIO来进行UDP编程时,不要使用read和write方法,可能会报错,正确地使用send和receive方法,这两个方法是抽象的,由 DatagramChannelImpl类具体实现的。

NIO基于UDP协议的网络编程

服务器端和客户端创建一个DatagramChannel。然后bind一个端口,注册Selector之后监听处理,就和之前NIO用法雷同。这里写个简单demo.

1、服务器端

public class Server {
    // 定义发送和接收数据的缓冲区
    private ByteBuffer inbuffer = ByteBuffer.allocate(1024);
    private ByteBuffer outbuffer = ByteBuffer.allocate(1024);

    // 定义一个选择器
    private Selector selector;

    public static void main(String[] args) {
        Server server = new Server();
        server.init();
        server.listen();
    }

    /**
     * 初始化 DatagramChannel
     */
    private void init() {
        try {
            // 获取一个DatagramChannel,并设置为非阻塞模式,绑定ip和port
            DatagramChannel datagramChannel = DatagramChannel.open();
            datagramChannel.configureBlocking(false);
            datagramChannel.bind(new InetSocketAddress("127.0.0.1", 19999));

            // 获取注册 Selector,并将 datagramChannel 的读事件(不需要连接)通道注册到 Selector
            selector = Selector.open();
            datagramChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }

    /**
     * 使用循环不断地监听来自客户端的连接
     */
    private void listen() {
        try {
            while (true) {
                int count = selector.select(5000);
                if (count == 0) {
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.keys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    handlerRead(selectionKey);
//                    it.remove();
                    selector.selectedKeys().remove(selectionKey);
                }
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }

    /**
     * 处理读事件的通道的方法
     *
     * @param key
     */
    private void handlerRead(SelectionKey key) {
        try {
            if (key.isReadable()) {
                DatagramChannel datagramChannel = (DatagramChannel) key.channel();
                inbuffer.clear();
                InetSocketAddress receiveAddr = (InetSocketAddress) datagramChannel.receive(inbuffer);
                inbuffer.flip();
                System.out.println("服务器端显示信息:" + StandardCharsets.UTF_8.decode(inbuffer).toString());

                // 回应该客户端:原路发送数据
                handlerWrite(datagramChannel, receiveAddr);
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }

    /**
     * 回应该客户端
     *
     * @param datagramChannel
     * @param receiveAddr
     */
    private void handlerWrite(DatagramChannel datagramChannel, InetSocketAddress receiveAddr) {
        try {
            outbuffer.clear();
            outbuffer.put("服务器收到了".getBytes("UTF-8"));
            outbuffer.flip();
            System.out.println(receiveAddr);
//            datagramChannel.send(outbuffer, new InetSocketAddress(receiveAddr.getHostName(), receiveAddr.getPort()));
            datagramChannel.send(outbuffer, receiveAddr);
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

2、客户端

public class Client {
    // 定义发送和接收数据的缓冲区
    private ByteBuffer inbuffer = ByteBuffer.allocate(1024);
    private ByteBuffer outbuffer = ByteBuffer.allocate(1024);

    // 定义一个选择器
    private Selector selector;

    public static void main(String[] args) {
        Client client = new Client();
        client.init();
    }

    /**
     * 初始化 DatagramChannel, 并发送数据
     */
    private void init() {
        try {
            // 获取一个DatagramChannel,并设置为非阻塞模式
            DatagramChannel datagramChannel = DatagramChannel.open();
            datagramChannel.configureBlocking(false);
            selector = Selector.open();
            datagramChannel.register(selector, SelectionKey.OP_WRITE);
            while (true) {
                int count = selector.select(5000);
                if (count == 0) {
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    handlerWrite(selectionKey);
                    it.remove();
                }
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }

    }

    /**
     *  向服务器端发送消息
     * @param key
     */
    private void handlerWrite(SelectionKey key) {
        try {
            if(key.isWritable()){
                DatagramChannel datagramChannel = (DatagramChannel) key.channel();
                //发送键盘上输入的数据
                Scanner scanner = new Scanner(System.in);
                while(scanner.hasNext()){
                    byte[] sendData = scanner.nextLine().getBytes("UTF-8");
                    outbuffer.clear();
                    outbuffer.put(sendData);
                    outbuffer.flip();
                    datagramChannel.send(outbuffer, new InetSocketAddress("127.0.0.1", 19999));

                    // 获取服务器端的响应的数据
                    inbuffer.clear();
                    datagramChannel.receive(inbuffer);
                    inbuffer.flip();
                    System.out.println("客户端显示信息:" + Charset.forName("UTF-8").decode(inbuffer).toString());
                }
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

 

—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。