UDP(User Datagram Protocol 用户数据报协议)

  • 传输层协议
  • 无连接:知道ip和端口号就可以直接进行传输,不需要建立连接。
  • 不可靠传输:没有确认机制,重传机制;出现错误,也不会给应用层返回任何信息。
  • 面向数据报:不能够灵活的控制读写数据的次数和数量。

注意:

  • 数据报的一次发送,接收端只能一次全部接收。
  • UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
  • UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报文的顺序和发送UDP报文的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃。
1 UDP socket API

DatagramSocket类:
DatagramSocket(int port,InetAddress laddr) 创建一个数据报套接字,绑定到指定的本地地址

DatagramSocket(SocketAddress bindaddr) 创建一个数据报套接字,绑定到指定的本地套接字地址

void bind(SocketAddress addr) 将此DatagramSocket绑定到特定的地址和端口

void connect(InetAddress address, int port) 将套接字连接到此套接字的远程地址

void receive(DatagramPacket p) 从此套接字接收数据报包

void close() 关闭此数据报套接字

void send(DatagramPacket p) 从此套接字发送数据报包

1 UDP网络程序

1.回显服务(echo):将请求当作响应回送出去。
2.字典服务(数据来自内存)
3.字典服务(数据来自Mysql)

(1)回显服务——简单理解UDP

1)服务端

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

// 把收到的请求内容,作为响应直接发送回去 —— 回显服务
// Server 必须公开出 port,否则客户端找不到
// 端口(port) 可以在 0 - 65535 之间随便选
// 但是不能使用已经被其他进程使用的端口 —— 端口只能属于唯一的一个进程
public class Server {
    static final int PORT = 9527;
    static final String CHARSET = "UTF-8";

    public static void main(String[] args) throws IOException {
        // 创建套接字
        // DatagramSocket 是 UDP 协议专用的 套接字
        System.out.println("DEBUG: 准备开启一个服务端");

        try (DatagramSocket serverSocket = new DatagramSocket(PORT)) {
            System.out.printf("DEBUG: 在 %d 这个端口上开启了服务端%n", PORT);

            // 提前准备好一个字节数组,用来存放接收到的数据(请求)
            // 一次最多可以接收 8192 个字节
            byte[] receiveBuffer = new byte[8192];

            while (true) {
                System.out.println("=======================================================");
                // 一次循环就是 一次 请求-响应 的处理过程
                // 1. 接收对方发送来的请求(数据)
                // 1.1 必须先创建 DatagramPacket 数据报文对象
                DatagramPacket packetFromClient = new DatagramPacket(
                        receiveBuffer, 0, receiveBuffer.length
                );
                System.out.println("DEBUG: 准备好了接收用的 packet");
                // 1.2 接收数据
                serverSocket.receive(packetFromClient); // 这个方法不是立即返回的,和 scanner.nextLine();
                System.out.println("DEBUG: 真正收到了客户端发来的数据");
                // 当走到这里时,数据一定接收到了
                // packetFromClient.getLength(); 一个收到了多少字节的数据
                // 1.3 因为我们收到的是字节格式的数据,所以我们把数据节码成字符格式的
                //     利用 String 的一个构造方法,把字节数组的数据解码(decode)成字符格式的数据 String
                String request = new String(
                        receiveBuffer, 0, packetFromClient.getLength(),
                        CHARSET
                );
                System.out.println("DEBUG: 收到的请求是: " + request);
                // 1.4 我们跳过了理解请求的这一步 —— 我们没有设计应用层协议
                // 1.5 业务处理
                String response = request;
                // 1.6 发送响应
                // 如何获取客户端进程的 ip + port
                InetAddress clientAddress = packetFromClient.getAddress();
                int clientPort = packetFromClient.getPort();
                System.out.printf("DEBUG: 客户端的唯一标识是(%s:%d)%n",
                        clientAddress.getHostAddress(), clientPort);

                byte[] responseBytes = response.getBytes(Server.CHARSET);
                DatagramPacket packetToClient = new DatagramPacket(
                        responseBytes, 0, responseBytes.length,     // 要发送的数据
                        clientAddress, clientPort                          // 要发送给客户端进程
                );
                System.out.println("DEBUG: 准备好了发送用的 packet");
                serverSocket.send(packetToClient);
                System.out.println("DEBUG: 成功把响应发送给客户端了");
                System.out.println("=======================================================");
            }
        }
    }
}

2)客户端

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

// 重构第一步:通过命令行读取用户输入作为请求发送
// 重构第二步:读取服务器发回的响应
public class Client {
    // 这里使用 127.0.0.1 代表本机
    private static final String serverIP = "127.0.0.1";

    public static void main(String[] args) throws IOException {
        // 创建 UDP Socket 的
        // 不需要传入端口,让 OS 自动分配一个
        try (DatagramSocket clientSocket = new DatagramSocket()) {
            Scanner scanner = new Scanner(System.in);

            byte[] receiveBuffer = new byte[8192];

            System.out.print("请输入请求> ");
            while (scanner.hasNextLine()) {
                // 1. 准备好请求,同时,传输的必须是字符格式
                String request = scanner.nextLine();

                // 这个 String 本身的一个方法,可以按照指定字符集,把字符串编码成字节数组
                byte[] requestBytes = request.getBytes(Server.CHARSET);

                // 2. 发送请求
                // 2.1 先准备 DatagramPacket
                //     需要指定服务器的 ip + port
                //     创建 发送用的 Packet 的时候,需要提供两类信息
                //          1) 需要发送的数据信息   requestBytes + 0 + requestBytes.length
                //          2) 接收信息的唯一标识(ip + port)
                //              InetAddress.getByName("127.0.0.1") 会把 ip 地址转成 InetAddress 对象
                DatagramPacket packetToServer = new DatagramPacket(
                        requestBytes, 0, requestBytes.length,     // 要发送的数据
                        InetAddress.getByName(serverIP), Server.PORT    // 要发送到互联网的哪个进程上
                );

                clientSocket.send(packetToServer);

                // 接收响应
                DatagramPacket packetFromServer = new DatagramPacket(
                        receiveBuffer, 0, receiveBuffer.length  // 提供的是用来装数据的容器信息
                );

                clientSocket.receive(packetFromServer);

                String response = new String(receiveBuffer, 0, packetFromServer.getLength(), Server.CHARSET);
                System.out.println("服务器应答: " + response);

                System.out.print("请输入请求> ");
            }
        }
    }
}

注意:
1) DatagramSocket serverSocket = new DatagramSocket(PORT)构造数据报套接字并将其绑定到本地主机上的任何可用端口。不填写ip地址,默认绑定本机。

2) 报文对象中需要一个字节数组来接收字节数据

byte[] receiveBuffer = new byte[8192];
DatagramPacket packetFromClient = new DatagramPacket(
        receiveBuffer, 0, receiveBuffer.length
);

报文对象源码:

public DatagramPacket(byte buf[], int offset, int length) {
    setData(buf, offset, length);
    this.address = null;
    this.port = -1;
}

3) 服务端开启后,如果客户端没有发送数据,服务端将会卡在receive这里,serverSocket.receive(packetFromClient);

4) 报文中的数据都是用字节数组receiveBuffer接收的,利用 String 的一个构造方法,把字节数组的数据解码(decode)成字符格式的数据:

String request = new String(
        receiveBuffer, 0, packetFromClient.getLength(),
        CHARSET
);

5) 报文对象中包含客户端的ip及端口信息,可以通过报文对象的getAddress和getPort()获得。

InetAddress clientAddress = packetFromClient.getAddress();
int clientPort = packetFromClient.getPort();

6) 传输响应给客户端,将字符串编码为字节数组:

byte[] responseBytes = response.getBytes(Server.CHARSET);
(2)字典服务(数据来自内存)

1)服务端:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// 端口(port) 可以在 0 - 65535 之间随便选
// 但是不能使用已经被其他进程使用的端口 —— 端口只能属于唯一的一个进程
public class Server {
    static final int PORT = 9527;
    static final String CHARSET = "UTF-8";
    // Map<英文单词,中文含义>
    private static final Map<String, String> meaningMap = new HashMap<>();
    // Map<英文单词,示例语句>
    private static final Map<String, List<String>> exampleSentencesMap = new HashMap<>();
    static {
        // 在静态代码块中对两个 map 进行初始化
        // give
        meaningMap.put("give", "vt. 给;产生;让步;举办;授予");
        exampleSentencesMap.put("give", new ArrayList<>());
        exampleSentencesMap.get("give").add("She stretched her arms out and gave a great yawn.");
        exampleSentencesMap.get("give").add("He was given mouth-to-mouth resuscitation.");
        // hive
        meaningMap.put("hive", "n. 蜂巢,蜂箱;蜂群;(喻)充满繁忙人群的场所");
        exampleSentencesMap.put("hive", new ArrayList<>());
        exampleSentencesMap.get("hive").add("Remember to destroy every single structure in each Hive complex.");
        exampleSentencesMap.get("hive").add("What people must remember is that one hive quickly become two, two become four and four become eight.");
        exampleSentencesMap.get("hive").add("Once they return to the hive, this can be detected, and a scan of the chip will reveal the appropriate location.");
    }

    public static void main(String[] args) throws IOException {
        // 创建套接字
        // DatagramSocket 是 UDP 协议专用的 套接字
        System.out.println("DEBUG: 准备开一家饭店");
        try (DatagramSocket serverSocket = new DatagramSocket(PORT)) {
            System.out.printf("DEBUG: 服务端在 %d 这个端口开启了%n", PORT);
            // 提前准备好一个字节数组,用来存放接收到的数据(请求)
            // 一次最多可以接收 8192 个字节
            byte[] receiveBuffer = new byte[8192];
            while (true) {      System.out.println("=======================================================");
                // 一次循环就是 一次 请求-响应 的处理过程
                // 1. 接收对方发送来的请求(数据)
                // 1.1 必须先创建 DatagramPacket 数据报文对象
                DatagramPacket packetFromClient = new DatagramPacket(
                        receiveBuffer, 0, receiveBuffer.length
                );
                System.out.println("DEBUG: 准备好了接收用的 packet");
                // 1.2 接收数据
                serverSocket.receive(packetFromClient); // 这个方法不是立即返回的,和 scanner.nextLine();
              System.out.println("DEBUG: 真正收到了客户端发来的数据");
                // 当走到这里时,数据一定接收到了
                // packetFromClient.getLength(); 一个收到了多少字节的数据

                // 1.3 因为我们收到的是字节格式的数据,所以我们把数据节码成字符格式的
                //     利用 String 的一个构造方法,把字节数组的数据解码(decode)成字符格式的数据 String
                String request = new String(
                        receiveBuffer, 0, packetFromClient.getLength(),
                        CHARSET
                );

                System.out.println("DEBUG: 收到的请求是: " + request);

                // 1.4 我们跳过了理解请求的这一步 —— 我们没有设计应用层协议

                // 1.5 业务处理
                //String response = request;
                // 1.5.1 请求就是英文单词
                //       根据英文单词获取含义 + 示例语句
                //       需要考虑,用户属于的请求不是我们支持的单词
                String response = "没有这个单词";
                String template = "\r\n含义:\r\n%s\r\n示例语句:\r\n%s\r\n";
                String exampleTemplate = "%d. %s\r\n";
                if (meaningMap.containsKey(request)) {
                    String meaning = meaningMap.get(request);
                    List<String> sentenceList = exampleSentencesMap.get(request);
                    StringBuilder exampleSB = new StringBuilder();
                    for (int i = 0; i < sentenceList.size(); i++) {
                        exampleSB.append(String.format(exampleTemplate, i + 1, sentenceList.get(i)));
                    }
                    response = String.format(template, meaning, exampleSB.toString());
                }
                // 1.6 发送响应
                // 如何获取客户端进程的 ip + port
                InetAddress clientAddress = packetFromClient.getAddress();
                int clientPort = packetFromClient.getPort();
                System.out.printf("DEBUG: 客户端的唯一标识是(%s:%d)%n",
                        clientAddress.getHostAddress(), clientPort);

                byte[] responseBytes = response.getBytes(Server.CHARSET);
                DatagramPacket packetToClient = new DatagramPacket(
                        responseBytes, 0, responseBytes.length,     // 要发送的数据
                        clientAddress, clientPort                          // 要发送给客户端进程
                );
                System.out.println("DEBUG: 准备好了发送用的 packet");

                serverSocket.send(packetToClient);
                System.out.println("DEBUG: 成功把响应发送给客户端了");
                System.out.println("=======================================================");
            }
        }
    }
}

2)客户端:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

// 重构第一步:通过命令行读取用户输入作为请求发送
// 重构第二步:读取服务器发回的响应
public class Client {
    // 这里使用 127.0.0.1 代表本机
   private static final String serverIP = "127.0.0.1";
    //private static final String serverIP = "49.233.172.121";

    public static void main(String[] args) throws IOException {
        // 创建 UDP Socket 的
        // 不需要传入端口,让 OS 自动分配一个
        try (DatagramSocket clientSocket = new DatagramSocket()) {
            Scanner scanner = new Scanner(System.in);
            // 这个buffer(缓冲区 —— 数据池子)用来放一会准备接收的数据
            byte[] receiveBuffer = new byte[8192];

            System.out.print("请输入请求> ");
            while (scanner.hasNextLine()) {
                // 1. 准备好请求,同时,传输的必须是字符格式
                String request = scanner.nextLine();

                // 这个 String 本身的一个方法,可以按照指定字符集,把字符串编码成字节数组
                byte[] requestBytes = request.getBytes(Server.CHARSET);

                // 2. 发送请求
                // 2.1 先准备 DatagramPacket
                //     需要指定服务器的 ip + port
                //     创建 发送用的 Packet 的时候,需要提供两类信息
                //          1) 需要发送的数据信息   requestBytes + 0 + requestBytes.length
                //          2) 接收信息的唯一标识(ip + port)
                //              InetAddress.getByName("127.0.0.1") 会把 ip 地址转成 InetAddress 对象
                DatagramPacket packetToServer = new DatagramPacket(
                        requestBytes, 0, requestBytes.length,     // 要发送的数据
                        InetAddress.getByName(serverIP), Server.PORT    // 要发送到互联网的哪个进程上
                );

                clientSocket.send(packetToServer);

                // 接收响应
                DatagramPacket packetFromServer = new DatagramPacket(
                        receiveBuffer, 0, receiveBuffer.length  // 提供的是用来装数据的容器信息
                );

                clientSocket.receive(packetFromServer);

                String response = new String(
                        receiveBuffer, 0, packetFromServer.getLength(), // 已经取到的数据
                        Server.CHARSET
                );
                System.out.println("服务器应答: " + response);

                System.out.print("请输入请求> ");
            }
        }
    }
}
(3)字典服务(数据来自Mysql)

1)服务端

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Server {
    static final int PORT = 9527;
    static final String CHARSET = "UTF-8";

    private static final DataSource dataSource;

    static {
        MysqlDataSource mysqlDataSource = new MysqlDataSource();

        // 通过 ip + port 确定网络上的唯一一个进程(mysqld)
        mysqlDataSource.setServerName("127.0.0.1");
        mysqlDataSource.setPort(3306);

        // MySQL 登陆需要用到的信息 —— 应用层使用的
        mysqlDataSource.setUser("root");
        mysqlDataSource.setPassword("mysql");
        mysqlDataSource.setDatabaseName("vocabulary");
        mysqlDataSource.setCharacterEncoding("utf8");

        // 设置一些 MySQL 连接用的属性 —— 不用 SSL 连接(Secure Socket Layer)
        mysqlDataSource.setUseSSL(false);

        dataSource = mysqlDataSource;
    }
    public static void main(String[] args) throws IOException {
        // 创建套接字
        // DatagramSocket 是 UDP 协议专用的 套接字
        // PORT 是我选好的准备开饭店的地址
        System.out.println("DEBUG: 准备开一家饭店");
        try (DatagramSocket serverSocket = new DatagramSocket(PORT)) {
            System.out.printf("DEBUG: 在 %d 这个端口上开好一家饭店了%n", PORT);

            // 提前准备好一个字节数组,用来存放接收到的数据(请求)
            // 一次最多可以接收 8192 个字节
            byte[] receiveBuffer = new byte[8192];

            while (true) {
                System.out.println("=======================================================");
                // 一次循环就是 一次 请求-响应 的处理过程

                // 1. 接收对方发送来的请求(数据)
                // 1.1 必须先创建 DatagramPacket 数据报文对象
                DatagramPacket packetFromClient = new DatagramPacket(
                        receiveBuffer, 0, receiveBuffer.length
                );
                System.out.println("DEBUG: 准备好了接收用的 packet");
                // 1.2 接收数据
                serverSocket.receive(packetFromClient); // 这个方法不是立即返回的,和 scanner.nextLine();
                System.out.println("DEBUG: 真正收到了客户端发来的数据");
                // 当走到这里时,数据一定接收到了
                // packetFromClient.getLength(); 一个收到了多少字节的数据

                // 1.3 因为我们收到的是字节格式的数据,所以我们把数据节码成字符格式的
                //     需要字符集编码的知识
                //     利用 String 的一个构造方法,把字节数组的数据解码(decode)成字符格式的数据 String
                String request = new String(
                        receiveBuffer, 0, packetFromClient.getLength(),
                        CHARSET
                );

                System.out.println("DEBUG: 收到的请求是: " + request);

                String response = "没有这个单词";
                try (Connection con = dataSource.getConnection()) {
                    String sql = "SELECT chinese FROM voc WHERE english = ?";
                    try (PreparedStatement stmt = con.prepareStatement(sql)) {
                        stmt.setString(1, request); // 尤其要注意 SQL 注入的问题
                                                                   // 不要信息你用户发送来的数据
                                                                   // 你的用户中有恶意用户
                        try (ResultSet rs = stmt.executeQuery()) {
                            if (rs.next()) {
                                response = rs.getString("chinese");
                            }
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();        // 打印在 Server 这边
                    response = e.getMessage();  // 准备发送给客户端的响应
                }

                // 1.6 发送响应

                // 如何获取客户端进程的 ip + port
                InetAddress clientAddress = packetFromClient.getAddress();
                int clientPort = packetFromClient.getPort();
                System.out.printf("DEBUG: 客户端的唯一标识是(%s:%d)%n",
                        clientAddress.getHostAddress(), clientPort);

                byte[] responseBytes = response.getBytes(Server.CHARSET);
                DatagramPacket packetToClient = new DatagramPacket(
                        responseBytes, 0, responseBytes.length,     // 要发送的数据
                        clientAddress, clientPort                          // 要发送给客户端进程
                );
                System.out.println("DEBUG: 准备好了发送用的 packet");

                serverSocket.send(packetToClient);
                System.out.println("DEBUG: 成功把响应发送给客户端了");
                System.out.println("=======================================================");
            }
        }
    }
}

2)客户端

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

// 重构第一步:通过命令行读取用户输入作为请求发送
// 重构第二步:读取服务器发回的响应
public class Client {
    // 这里使用 127.0.0.1 代表本机
    private static final String serverIP = "127.0.0.1";
    //private static final String serverIP = "49.233.172.121";

    public static void main(String[] args) throws IOException {
        // 创建 UDP Socket 的
        // 不需要传入端口,让 OS 自动分配一个
        try (DatagramSocket clientSocket = new DatagramSocket()) {
            Scanner scanner = new Scanner(System.in);

            // 这个buffer(缓冲区 —— 数据池子)用来放一会准备接收的数据
            byte[] receiveBuffer = new byte[8192];

            System.out.print("请输入请求> ");
            while (scanner.hasNextLine()) {
                // 1. 准备好请求,同时,传输的必须是字符格式
                String request = scanner.nextLine();

                // 这个 String 本身的一个方法,可以按照指定字符集,把字符串编码成字节数组
                byte[] requestBytes = request.getBytes(Server.CHARSET);

                // 2. 发送请求
                // 2.1 先准备 DatagramPacket
                //     需要指定服务器的 ip + port
                //     创建 发送用的 Packet 的时候,需要提供两类信息
                //          1) 需要发送的数据信息   requestBytes + 0 + requestBytes.length
                //          2) 接收信息的唯一标识(ip + port)
                //              InetAddress.getByName("127.0.0.1") 会把 ip 地址转成 InetAddress 对象
                DatagramPacket packetToServer = new DatagramPacket(
                        requestBytes, 0, requestBytes.length,     // 要发送的数据
                        InetAddress.getByName(serverIP), Server.PORT    // 要发送到互联网的哪个进程上
                );

                clientSocket.send(packetToServer);

                // 接收响应
                DatagramPacket packetFromServer = new DatagramPacket(
                        receiveBuffer, 0, receiveBuffer.length  // 提供的是用来装数据的容器信息
                );

                clientSocket.receive(packetFromServer);

                String response = new String(
                        receiveBuffer, 0, packetFromServer.getLength(), // 已经取到的数据
                        Server.CHARSET
                );
                System.out.println("服务器应答: " + response);

                System.out.print("请输入请求> ");
            }
        }
    }
}
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐