一、概述
记录时间 [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:// |
指定用于访问资源的协议。常见的有 http 、https ,ftp ,mailto ,file 。它告诉浏览器或应用程序使用哪种规则来获取资源。 |
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