网络编程 05:UDP 连接,UDP 与 TCP 的区别,实现 UDP 消息发送和接收,通过 URL 下载资源

发布于:2025-09-03 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、概述

记录时间 [2025-09-02]

前置文章

网络编程 01:计算机网络概述,网络的作用,网络通信的要素,以及网络通信协议与分层模型

网络编程 02:IP 地址,IP 地址的作用、分类,通过 Java 实现 IP 地址的信息获取

网络编程 03:端口的定义、分类,端口映射,通过 Java 实现了 IP 和端口的信息获取

网络编程 04:TCP连接,客户端与服务器的区别,实现 TCP 聊天及文件上传,Tomcat 的简单使用


本文讲述网络编程相关知识——UDP 连接,包括 UDP 的核心特点,UDP 与 TCP 的区别,以及在 Java 中实现 UDP 消息发送和接收,通过 URL 下载资源等。



二、UDP

1. UDP 的核心特点

UDP(User Datagram Protocol,用户数据报协议)是一种简单的、无连接的、不可靠的传输层协议。

  • 无连接
    • UDP 发送数据之前不需要先建立连接,减少了通信的延迟。
  • 不可靠交付
    • UDP 不提供任何机制来确认数据是否成功到达目的地,也不保证数据包的送达顺序。
  • 无拥塞控制
    • UDP 以恒定的速率发送数据,而不管网络是否拥堵,容易丢包。这对于网络整体稳定性可能是个缺点,但对于需要恒定速率的应用却是优点。
  • 数据报结构
    • UDP 保留了应用程序定义的消息边界。如果发送方发送了 5 个 UDP 数据报,接收方将会收到 5 个独立的数据报。
    • 而 TCP 则是一个字节流,应用程序需要自己解析消息的开始和结束。

2. UDP 与 TCP 的区别

通过将 UDP 与 TCP 对比来更好地理解它:

特性 TCP (传输控制协议) UDP (用户数据报协议)
连接 面向连接的 无连接的
通信前必须先建立连接(三次握手) 无需建立连接,直接发送数据
可靠性 可靠的 不可靠的
确保数据按序、完整地送达,有重传机制 不保证数据送达,也不保证顺序
传输速度 相对较慢(由于握手、确认、重传等开销) 非常快(开销极小)
数据流 字节流,无消息边界 数据报,有消息边界
拥塞控制 有复杂的拥塞控制算法 无拥塞控制
应用场景 网页浏览(HTTP)、电子邮件(SMTP)、文件传输(FTP) 视频流、语音通话、在线游戏、DNS查询

3. UDP 在 Java 的关键类

在 Java 中,使用 UDP 协议进行网络通信主要涉及两个类:DatagramPacket 和 DatagramSocket。

  • DatagramPacket
    • 用于发送和接收数据报包的套接字;
    • 表示一个数据报包,用于存储要发送或接收的数据;
    • 包含了数据本身以及目标地址(IP 地址和端口号)。
  • DatagramSocket:用于发送和接收 DatagramPacket 的套接字。
    • 表示数据报包,包含数据和目标地址信息;
    • 用于发送时指定数据和目标地址;
    • 用于接收时提供缓冲区存储接收到的数据。

在这里插入图片描述



在这里插入图片描述



三、UDP 消息发送和接收

注意:UDP 中没有明确的客户端、服务端的概念,也不需要建立双向连接。我们在这里把发消息的称为发送端,接收消息的称为接收端。


1. 简单发送和接收

发送端

数据包 package 中包含:数据(字节 byte 类型),数据的长度(起始,结束), 对方 ip,对方端口。


import java.net.*;

// 发送端
public class UdpSendDemo01 {
    public static void main(String[] args) throws Exception {

        // 1. 建立一个 socket, 开放端口
        DatagramSocket socket = new DatagramSocket();

        // 2. 准备一个数据包
        String msg = "这是一个数据包";
        InetAddress inetAddress = InetAddress.getByName("127.0.0.1");

        // 数据包, 数据的长度起始, 结束, 对方ip, 对方端口
        DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.getBytes().length, inetAddress, 9000);

        // 3. 发送数据包
        socket.send(packet);
        System.out.println("Message sent to the server.");

        // 4. 关闭资源
        socket.close();
    }
}

接收端

接收包的程序要先打开,只有开着才能收到消息。

因为 UDP 只管发,不会去管接收端有没有准备好的。


import java.net.*;

// 接收端
public class UdpReceiveDemo01 {
    public static void main(String[] args) throws Exception {
        // 1. 开放端口
        DatagramSocket socket = new DatagramSocket(9000);

        // 2. 接收数据包
        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);

        // 阻塞接收
        socket.receive(packet);

        // 3. 查看数据包
        System.out.println(packet.getAddress().getHostAddress());
        System.out.println(new String(packet.getData(), 0, packet.getLength()));

        // 4. 关闭资源
        socket.close();

    }
}

2. 循环发送和接收

在简单 UDP 消息发送的基础上,给程序增加循环,实现 UDP 消息循环发送和接收。

并增加判断条件:当发送过来的内容是 bye 时,程序结束。


发送端


import java.io.*;
import java.net.*;

public class UdpSender {
    public static void main(String[] args) throws Exception {
        // 1. 开放端口
        DatagramSocket socket = new DatagramSocket(8888);

        // 2. 装包
        // 从键盘输入到控制台 System.in, 控制台读取
        // 用 BufferedReader 去读键盘输入到控制台的内容
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        while (true) {
            // 读一整行
            String data = reader.readLine();
            // 转成字节流, socket 发的是字节流
            byte[] dataBytes = data.getBytes();

            DatagramPacket packet = new DatagramPacket(dataBytes, 0, dataBytes.length, new InetSocketAddress("localhost", 6666));

            // 3. 发包
            socket.send(packet);

            // 本地退出
            if (data.equals("bye")) {
                break;
            }
        }

        // 4. 关闭资源
        socket.close();
    }
}

接收端

发过来的内容是字节 byte 类型的,要转换成字符串 String 类型。


import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UdpReceiver {
    public static void main(String[] args) throws Exception {
        // 1. 开放端口
        DatagramSocket socket = new DatagramSocket(6666);

        // 准备一个容器
        byte[] container = new byte[1024];

        while (true) {
            // 2. 读包
            DatagramPacket packet = new DatagramPacket(container, 0, container.length);

            // 阻塞接收包
            socket.receive(packet);

            // 3. 输出包
            // 包是字节流, 转成 string
            byte[] data = packet.getData();
            String receiveData = new String(data, 0, packet.getLength());

            // 输出内容
            System.out.println(receiveData);

            // 远程退出
            if (receiveData.equals("bye")) {
                break;
            }
        }

        // 4. 关闭资源
        socket.close();
    }
}


四、UDP 多线程在线咨询

特点:互发消息。

在了解 UDP 发送、接收消息的逻辑后,我们来实现如下程序功能。

  • 相当于一个咨询平台:学生向老师咨询问题,老师给出答复。
  • 接收端、发送端是两个多线程。
  • 老师端、学生端是两个用户,他们既可以发消息,也可以接收消息。

更多多线程相关的知识,请参考 - 这篇文章

这里,通过实现 Runnable 接口来创建线程。


1. 接收端

接收端用于接收 UDP 消息。


import java.io.IOException;
import java.net.*;

public class TalkReceive implements Runnable {

    DatagramSocket socket = null;

    // 接收端的 port
    private int port;

    // 消息从哪里来
    private String msgFrom;

    public TalkReceive(int port, String msgFrom) {
        this.port = port;
        this.msgFrom = msgFrom;

        try {
            // 1. 开放端口
            this.socket = new DatagramSocket(this.port);


        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // 准备一个容器
        byte[] container = new byte[1024];

        while (true) {
            try {
                // 2. 读包
                DatagramPacket packet = new DatagramPacket(container, 0, container.length);

                // 阻塞接收包
                socket.receive(packet);

                // 3. 输出包
                // 包是字节流, 转成 String
                byte[] data = packet.getData();
                String receiveData = new String(data, 0, packet.getLength());

                // 输出内容
                System.out.println(msgFrom + ": " + receiveData);

                // 断开连接, 远程退出
                if (receiveData.equals("bye")) {
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 4. 关闭资源
        socket.close();
    }
}

2. 发送端

发送端用于发送 UDP 消息。


import java.io.*;
import java.net.*;

public class TalkSend implements Runnable {

    DatagramSocket socket = null;
    BufferedReader reader = null;

    // 接收的地址 (接收 ip, 接收 port)
    private String toIP;
    private int toPort;

    // 从哪里来
    private int fromPort;

    public TalkSend(String toIP, int toPort, int fromPort) {

        this.fromPort = fromPort;
        this.toIP = toIP;
        this.toPort = toPort;

        try {
            // 1. 开放端口
            this.socket = new DatagramSocket(this.fromPort);

            // 从键盘输入到控制台 System.in, 控制台读取
            // 用 BufferedReader 去读键盘输入到控制台的内容
            reader = new BufferedReader(new InputStreamReader(System.in));

        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {

        try {
            while (true) {
                // 2. 装包
                // 读一整行
                String data = reader.readLine();
                // 转成字节流, socket 发的是字节流
                byte[] dataBytes = data.getBytes();

                DatagramPacket packet = new DatagramPacket(dataBytes, 0, dataBytes.length, new InetSocketAddress(toIP, toPort));

                // 3. 发包
                socket.send(packet);

                // 本地退出
                if (data.equals("bye")) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 4. 关闭资源
        socket.close();

    }
}

3. 老师端

模拟老师的操作:

  • 给学生发消息(创建发送端的多线程)
  • 接收来自学生的消息(创建接收端的多线程)

public class TalkTeacher {
    public static void main(String[] args) {
        // 启动多线程

        // 把消息发送到 localhost 的 8888 端口
        // 8888 是学生的 Receive 开放端口
        // Send 方开放的端口用不上,Receive 方开放的端口才有用
        new Thread(new TalkSend("localhost", 8888, 5555)).start();

        // 开放 9999 端口,接收来自学生的消息
        new Thread(new TalkReceive(9999, "student")).start();
    }
}

4. 学生端

模拟学生的操作:

  • 给老师发消息(创建发送端的多线程)
  • 接收来自老师的消息(创建接收端的多线程)

public class TalkStudent {
    public static void main(String[] args) {
        // 启动多线程

        // 把消息发送到 localhost 的 9999 端口
        // 9999 是老师的 Receive 开放端口
        new Thread(new TalkSend("localhost", 9999, 7777)).start();

        // 开放 8888 端口,接收来自老师的消息
        new Thread(new TalkReceive(8888, "teacher")).start();
    }
}


五、URL 下载网络资源

1. URL 概述

URL 格式

URL(Uniform Resource Locator,统一资源定位符) 是用于指定互联网上资源(如网页、图像、文件等)位置和访问方式的一种字符串。

通俗地说,它就是我们在浏览器地址栏里输入的 “网址”。


一个完整的 URL 由多个部分组成,通常遵循以下格式:

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

// example
https://www.example.com:8080/products/index.html?category=electronics&id=42#specs

具体的部分,内容解释如下:

其中,www.example.com 是域名,可以通过 DNS 域名解析服务解析成对应的 IP 地址

部分 例子 说明
Scheme(方案) https:// 指定用于访问资源的协议。常见的有 httphttpsftpmailtofile。它告诉浏览器或应用程序使用哪种规则来获取资源。
Authority(授权部分) www.example.com:8080 通常包含主机名 Host ** 和端口 Port**。
Host(主机) www.example.com 资源所在服务器的域名或 IP 地址。
Port(端口) :8080 HTTP 默认端口是 80,HTTPS 是 443。如果使用默认端口,通常在 URL 中省略
Path(路径) /products/index.html 指定服务器上资源的具体位置,类似于文件系统中的文件路径。
Query(查询字符串) ?category=electronics&id=42 用于向服务器传递额外的参数。以 ? 开头,包含多个键值对(key=value),键值对之间用 & 分隔。
Fragment(片段) #specs 也称为 “锚点”,它指向资源内部的某个特定部分,如 HTML 页面中的一个标题。片段不会发送到服务器,仅在浏览器内部使用。

URL 编码

URL 只能使用有限的 ASCII 字符集,任何包含非 ASCII 字符(如中文)或特殊字符(如空格、&=)的 URL 都需要进行编码。

URL 编码也称为 “百分号编码”。

例如,空格被编码为 %20,中文 “中国” 被编码为 %E4%B8%AD%E5%9B%BD


通过 Java 查看 URL

在 Java 中,java.net.URL 类用于表示和解析 URL。它提供了许多有用的方法来分解和操作 URL 的各个部分。

通过 Java 来查看 URL 的各个部分。


import java.net.MalformedURLException;
import java.net.URL;

public class URLDemo01 {
    public static void main(String[] args) throws MalformedURLException {
        
        // example
        URL url = new URL("https://www.example.com:8080/products/index.html?category=electronics&id=42#specs");

        // 协议
        System.out.println(url.getProtocol());

        // 主机ip、域名
        System.out.println(url.getHost());

        // 端口
        System.out.println(url.getPort());

        // 文件路径
        System.out.println(url.getPath());

        // 全路径: 路径+参数
        System.out.println(url.getFile());

        // 参数
        System.out.println(url.getQuery());
    }
}

对应的输出结果:

https
www.example.com
8080
/products/index.html
/products/index.html?category=electronics&id=42
category=electronics&id=42

2. 下载 URL 资源

上一篇 中,讲述了如何启动 Tomcat 并访问部署的资源。

例如,访问自定义资源:webapps 目录下的 test 中的 hello.txt 文件。

用到的其实就是一个 URL:

http://localhost:8080/test/hello.txt

接下来,我们来下载这个 URL 指向的网络资源。

  • 给出资源下载地址;
  • 连接到这个资源;
  • 通过流下载;
  • 通过文件管道处理资源,保存资源。

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

public class URLDown {
    /*
        本地 tomcat 中有这样一个文件
        http://localhost:8080/test/hello.txt
        通过 URL 下载下来
     */

    public static void main(String[] args) throws Exception {

        // 1. 下载地址
        URL url = new URL("http://localhost:8080/test/hello.txt");

        // 2. 连接到这个资源 HTTP
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // 通过流下载
        InputStream is = connection.getInputStream();

        // 文件管道处理下载下来的数据
        FileOutputStream fos = new FileOutputStream(new File("NetStudy/hello.txt"));

        byte[] buffer = new byte[1024];
        int len;
        while ((len = is.read(buffer)) != -1) {
            // 写出这个数据
            fos.write(buffer, 0, len);
        }

        // 3. 关闭资源, 断开连接
        fos.close();
        is.close();
        connection.disconnect();

    }

}

同理可得,网络上的资源也可以这么下载,输入 URL 即可。

无论是文本、图片、视频、音频,还是其他类型的文件。

可以尝试一下:

// 下载图片
URL url = new URL("https://i-blog.csdnimg.cn/direct/728b14801d3f4400bad0905bfdba34be.jpeg");

// 文件管道处理下载下来的数据
FileOutputStream fos = new FileOutputStream(new File("NetStudy/bfdba34be.jpeg"));


参考资料

狂神说 - 网络编程:https://www.bilibili.com/video/BV1LJ411z7vY

Java 8 帮助文档:https://docs.oracle.com/javase/8/docs/api/

多线程 02:线程实现,创建线程的三种方式,通过多线程下载图片案例分析异同(Thread,Runnable,Callable):https://blog.csdn.net/Sareur_1879/article/details/141029891


网站公告

今日签到

点亮在社区的每一天
去签到