Java网络编程

235 阅读42分钟

基本概念

计算机网络
指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接(有线性、无线)起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

网络编程

目的
实现计算机之间的数据交换和通讯

操作

  1. 准确地定位网络上的一台计算机及其端口。 IP地址用于唯一标识网络中的一台计算机,而端口号则是在一台计算机上标识一个特定的应用或资源
  2. 依赖通信协议传输数据。 C/S(客户端/服务器)架构是一种常见的架构,其中客户端应用程序负责与服务器进行通信,以获取或发送数据。TCP/IP 协议族是 C/S 架构中常用的通信协议,它提供了 可靠的、面向连接的传输服务,如 TCP(传输控制协议)和 不可靠的、无连接的服务,如 UDP(用户数据报协议)。这些协议规定了数据在网络中的传输方式,包括数据的打包、传输和接收等过程

要素
地址:设备在网络中的地址,是唯一的标识。IP地址是分配给上网设备的唯一标志,分为IPv4和IPv6两种。
协议:数据在网络中传输的规则,常见的协议有TCP和UDP协议。TCP协议是面向连接的,传输数据之前需要建立连接,是安全可靠协议。而UDP协议是面向无连接的,传输数据之前源端和目的端不需要建立连接,传输速度快,效率高。
端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0~65535。端口类型分为知名端口和注册端口,知名端口被预先定义的知名应用占用,如HTTP占用80端口。
连接:设备间建立连接并完成信息交换。连接的代表是socket,它代表通信的连接。

此外,网络通信还包括数据、操作、会话、时序等要素。

TCP/IP参考模型 c18bc38f09c04c8aa8306523a28488ce.png

IP

java.net.InetAddress类:提供多种方法来处理和解析IP地址字符串

IP地址
在网络中是用来唯一标识一台计算机或设备的。每台计算机在网络上都有一个唯一的IP地址,这样其他计算机或设备就可以通过这个地址与它通信。

127.0.0.1 和 localhost
"127.0.0.1"和"localhost"是本机地址的别名,指向同一IP地址,即本机地址。在本地开发和测试中,它们通常用于访问本地服务,如设置本地Web服务器、开发其他需要本地访问的应用程序。
127.0.0.1:一个特殊的IP地址,被称为回环地址或本地地址。它用于将数据包发送回发送者,通常用于测试和开发目的。任何发送到此地址的数据包都不会离开计算机,而是返回给发送者。
localhost:“本地主机”的缩写,它是一个网络名称,用于指代当前计算机。在大多数情况下,当您在浏览器中输入 “localhost” 时,您实际上是在访问运行在同一台计算机上的Web服务器或其他服务。

ip 地址的分类

IPv4和IPv6是IP协议的两个版本,用于在互联网上为设备分配唯一的地址。

IPv4

  • 地址长度:IPv4使用32位地址长度,这意味着地址由4个字节组成。
  • 地址组成:IPv4地址由4个8比特的整数组成,用一个字节表示一个数字,每个数字(1字节 8比特)的范围是0-255(八比特二进制数的范围)。通常以点(.)分隔的4个十进制数字序列的形式表示,例如192.168.1.1
  • 地址耗尽:随着互联网的发展,到2011年左右,全球的IPv4地址已经分配完毕。
  • 地址分配:北美拥有大量的IPv4地址,而亚洲的地址较少。这是因为早期的互联网发展和地址分配主要集中在北美。

IPv6

  • 地址长度:IPv6使用128位地址长度,这意味着地址由16个字节组成。
  • 地址组成:IPv6地址由8组4个十六进制数字组成,用两个字节表示一组数字,每组数字(2字节 16比特)的范围是0-ffff(十六比特二进制数的范围)。通常以冒号(:)分隔的8组4个十六进制数字序列的形式表示,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334。所以IPv6的地址范围非常庞大,远远超过IPv4。

公网(互联网)-私网(局域网)
公网地址是可以在互联网上公开访问的地址。私网地址则专门为组织内部使用,私网地址在互联网上是不可见的,只能在组织的内部网络中使用。

域名
互联网上的一种标识,由一串英文字母和数字的组合组成,用来表示网站的名称。域名通常由几个部分组成,包括顶级域名和二级域名。顶级域名是域名中的最后一部分,例如“.com”、“.net”、“.org”等,它们表示了网站的性质或行业。二级域名则是域名的第一部分,例如“google.com”、“yahoo.com”等,它们表示了具体的网站名称。

import java.net.InetAddress;
import java.net.UnknownHostException;

public class TextInetAddress {
    public static void main(String[] args) {
        try {
            //查询本机地址
            InetAddress interaddress1 = InetAddress.getByName("127.0.0.1");
            System.out.println(interaddress1);
            InetAddress interaddress3 = InetAddress.getByName("localhost");
            System.out.println(interaddress3);
            InetAddress interaddress4 = InetAddress.getLocalHost();
            System.out.println(interaddress4);
            
            //查询网站IP地址
            InetAddress interaddress2 = InetAddress.getByName("www.baidu.com");
            System.out.println(interaddress2);
            
            //常用方法
            System.out.println(interaddress2.getAddress());
            System.out.println(interaddress2.getCanonicalHostName());  //规范的名字
            System.out.println(interaddress2.getHostAddress());        //ip
            System.out.println(interaddress2.getHostName());           //域名,或者自已电脑的名字
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}

分析

P8
InetAddress: 这是Java的一个类,位于 java.net 包中。提供了多种方法来获取和处理 IP 地址信息
interaddress1: 这是一个变量名,代表一个 InetAddress 对象。使用这个变量来存储查询到的IP地址
getByName(String host): 这是 InetAddress 类中的一个静态方法,用于根据提供的主机名或IP地址获取对应的 InetAddress 对象。这个方法有几个用途:

  1. 解析主机名:如果提供了主机名(如 "www.example.com" ) ,该方法会返回一个表示解析出的 IP 地址 InetAddress 对象
  2. 解析 IP 地址:如果提供了 IP 地址(如 "192.168.1.1"),该方法会直接返回一个表示该 IP 地址的 InetAddress 对象

当执行这行代码时,Java会查找与字符串"127.0.0.1"对应的IP地址,并将结果存储在interaddress1变量中。返回的IP地址就是"127.0.0.1"。

P12
getLocalHost():这是 InetAddress 类中的一个静态方法,用于获取本地主机的名称和地址。该方法返回一个 InetAddress 对象,该对象表示本地计算机的 IP 地址和域名

P20-P23
演示如何使用 InetAddress 类的几个常用方法:

  • getAddress() : 返回IP地址的字节数组表示。
  • getCanonicalHostName() : 返回规范的主机名,通常是域名的完全限定版本。
  • getHostAddress() : 返回字符串形式的IP地址。
  • getHostName() : 返回主机名,这可能是域名或本地主机名。

异常处理:
如果在尝试解析主机名时发生错误(例如,主机名不存在),则将抛出UnknownHostException异常。该异常在catch块中被捕获,并打印堆栈跟踪。

端口

定义:用于标识不同的通信进程。可以将计算机上的程序想象成一个个独立的工作者,而端口则是这些工作者之间进行通信的“电话号码”。这样,当一个程序需要向另一个程序发送数据时,它可以通过接收程序的端口号来指定接收方。

  • 在计算机网络中,每个正在运行的程序或进程都有一个唯一的端口号,以便区分不同的通信会话。例如,当访问一个网页时,浏览器会与服务器上的特定端口(如HTTP的80端口)通信
  • TCP和UDP是两种主要的传输层协议。它们端口号范围都是0-65535。同一个端口号可以被不同的协议使用,表示不同的服务。例如,当端口443用于TCP时,它用于HTTPS服务。但UDP上的443端口可能被用于其他与HTTPS无关的目的

端口分类
公有端口:0-1023是预留给特定服务的公有端口。这些端口由标准化组织定义,并由操作系统管理。例如,HTTP使用80端口,HTTPS使用443端口
程序注册端口:1024-49151是程序注册的端口范围。这是用户或应用程序可以选择的端口范围,以避免与公有端口的冲突。例如,Tomcat通常使用8080端口,MySQL使用3306端口
动态/私有端口 :49152-65535是用于临时目的的端口,例如当一个程序需要创建一个短暂的连接时。这些端口通常不会被防火墙特别处理。

关于端口的DOS命令

系统命令可以在任何位置运行,不需要进入特定的目录来运行

  • netstat -ano

    • netstat 是一个用于显示网络连接、路由表、接口统计等网络相关信息的命令
    • -a 选项表示显示所有活动网络连接和监听端口
    • -n 选项表示以数字形式显示地址和端口号,不尝试解析名称
    • -o 选项表示显示与每个连接相关的进程ID。
    • 因此, netstat -ano 会列出所有活动的网络连接以及与它们相关的进程ID
  • netstat -ano | findstr "n"

    • findstr 是一个用于搜索文本的命令。这里它被用来搜索包含“n”的行,即与端口 n 相关的网络连接( n 为端口号),
    • 因此,这个命令组合会列出与端口 n 相关的所有活动连接
  • tasklist | findstr "n"

    • tasklist 是一个用于显示当前运行的进程的命令
    • 因此,这个命令组合会列出具有进程ID n 的进程

快捷键 ctrl + shift + ESC

用于打开任务管理器。任务管理器是一个用于查看计算机性能、进程、资源占用等的工具。通过任务管理器,你可以结束不响应的程序或查看运行中的进程。

通信协议

定义:双方实体完成通信或服务所必须遵循的规则和约定。这些规则和约定详细规定了数据通信的各个方面,包括数据格式、同步方式、传输速度、传输步骤、检纠错方式以及控制字符定义等。通信双方必须共同遵守这些规定,以确保信息的正确传输和交换。

TCP/IP协议簇是互联网中最常用的协议簇,包含了许多用于实现网络通信的协议。其核心协议主要包括TCP(传输控制协议)、UDP(用户数据报协议)和IP(互联网协议)。其中,TCP(传输控制协议)和UDP(用户数据报协议)是最重要的两个传输层协议。

TCP和UDP 对比

TCP:打电话
TCP(传输控制协议)就像打电话

  1. 首先需要拨号并建立连接,这样你和对方才能开始通话
  2. 通话过程中,你们会保持连接,确保通话的稳定性和连续性
  3. 如果通话过程中出现中断或噪音,通常会重新连接或进行错误纠正
  4. 通话结束后,需要挂断电话,释放连接

TCP协议的特点是可靠、有序和基于连接的。它适用于需要保证数据完整性和可靠性的应用,如网页浏览、文件传输等。但由于需要建立和维护连接,并进行错误控制和流量控制,TCP的效率相对较低。

面试题三次握手和四次挥手

//三次握手,最少需要三次,保证稳定连接!
A:同学!
B:嗯?
A:请和我交往吧,拜托了。

//四次挥手
A:我要走了。
B:你要走了吗?
B:你真的要走了吗?
A:我真的要走了!

UDP:发短信
UDP(用户数据报协议)则像发短信

  • 可以直接发送短信。不需要先建立连接
  • 而对方是否收到、何时收到以及短信内容的顺序都没有保证。这就像UDP协议一样不保证数据的可靠传输和顺序性

UDP协议的特点是简单、快速和不可靠。它适用于对数据传输的可靠性要求不高,但对实时性要求较高的应用,如音频、视频流传输等。由于UDP不需要建立和维护连接,也不进行错误控制和流量控制,因此它的效率非常高。

DDOS:洪水攻击(饱和攻击)
DDOS(分布式拒绝服务攻击)是一种网络攻击方式,其中攻击者通过控制大量计算机或网络僵尸来向目标服务器发送大量请求,使其无法处理正常请求,从而导致服务瘫痪。这种攻击方式类似于洪水攻击或饱和攻击,即通过发送大量无用的数据包来占用目标服务器的带宽和资源,使其无法为正常用户提供服务。

在DDOS攻击中,攻击者可能会利用UDP协议的特点,发送大量无序、无连接、不可靠的UDP数据包来占用目标服务器的资源。由于UDP协议本身不保证数据的可靠传输和顺序性,这使得攻击更加难以防范和检测。

TCP

客户端

  1. 连接服务器 Socket
  2. 发送消息

服务端

  1. 建立服务的端口 Serversocket
  2. 等待用户的链接 accept
  3. 接收用的湖息

发送消息-接收消息的示例

客户端示例

import java.io.IOException;    // 导入Java的IO异常类,用于处理输入输出过程中可能出现的异常
import java.io.OutputStream;   // 导入Java的输出流类,用于向Socket发送数据
import java.net.InetAddress;   // 导入Java的网络地址类,用于表示IP地址
import java.net.Socket;        // 导入Java的Socket类,用于建立客户端与服务器的连接
import java.net.UnknownHostException;  // 导入Java的未知主机异常类,用于处理无法解析主机名的情况

public class TcpClientDemo01 {
    public static void main(String[] args) {
        InetAddress serverIP = null; //声明InetAddress类型的变量serverIP,并初始化为null,用于存储服务器的IP地址
        Socket socket = null;        //声明Socket类型的变量socket,并初始化为null,用于表示与服务器的连接
        OutputStream os = null;      //声明OutputStream类型的变量os,并初始化为null,用于向服务器发送数据 

        try {
            //1.获取服务器的地址。通过InetAddress类的静态方法getByName获取服务器的IP地址,这里使用的是本地回环地址
            serverIP = InetAddress.getByName("127.0.0.1");
            int port = 9999;  //声明整型变量port,并赋值为9999,表示服务器的端口号
            //2.创建一个Socket连接。使用Socket类的构造方法创建一个新的Socket连接,参数为服务器的IP地址和端口号
            socket = new Socket(serverIP,port);
            //3.发送消息IO流。通过Socket对象的getOutputStream方法获取输出流,用于向服务器发送数据
            os = socket.getOutputStream();
            os.write("你好,欢迎学习狂神说Java".getBytes());  //使用输出流的write方法向服务器发送数据
            //这里使用getBytes()方法发送字符串"你好,欢迎学习狂神说Java"的字节表示

        } catch (UnknownHostException e) {  //捕获UnknownHostException异常
            e.printStackTrace();
        } catch (IOException e) {  //捕获IOException异常
            e.printStackTrace();
        }finally {                 //finally块中的代码无论是否发生异常都会执行
            if (os != null) {      //如果输出流os不为 null,则执行
                try {
                    os.close();    //关闭输出流,释放资源
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {  //如果socket不为 null,则执行
                try {
                    socket.close();  //关闭Socket连接,释放资源
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

分析

P20
getOutputStream():这是 String 类的一个方法,用于 TCP 客户端向服务器发送数据。当使用 Socket 类建立一个到远程主机的连接后,可以通过调用 getOutputStream() 方法来获取一个 OutputStream 对象,这个对象可被用于向远程主机发送字节数据。

P21
getBytes():这是 String 类的一个方法,用于将字符串转换为字节数组。这个方法对于处理文本数据特别有用,尤其是当需要通过网络发送数据、写入文件(文件存储通常以字节为单位)或者处理二进制数据时。
网络发送数据:在网络通信中,数据通常是以字节的形式发送和接收的。要将一个字符串发送到网络另一端,需要先使用 getBytes() 方法将字符串转换为字节。

当你调用 getBytes() 方法时,它会根据系统的默认字符集将字符串编码为字节。例如,在大多数现代系统上,默认字符集是UTF-8。这意味着字符串中的每个字符都将被转换为对应的UTF-8编码的字节。

示例:

String str = "你好,欢迎学习狂神说Java";
byte[] bytes = str.getBytes(); // 使用系统默认字符集转换字符串为字节数组

如果想要使用特定的字符集来编码字符串,那么可以将字符集的名称作为参数传递给 getBytes() 方法,示例:

byte[] bytes = str.getBytes("UTF-8"); // 使用UTF-8字符集转换字符串为字节数组
或者
byte[] bytes = str.getBytes("ISO-8859-1"); // 使用ISO-8859-1字符集转换字符串为字节数组

如果不指定字符集,而字符串中包含无法用默认字符集表示的字符,那么 getBytes() 方法可能会抛出 UnsupportedEncodingException 异常。为了避免这种情况,建议总是明确指定字符集。

服务端示例

import java.io.ByteArrayOutputStream;  // 导入Java的ByteArrayOutputStream类,用于将数据写入内存中的字节数组
import java.io.IOException;     // 导入Java的IO异常类,用于处理输入输出过程中可能出现的异常
import java.io.InputStream;     // 导入Java的输入流类,用于读取从客户端发送过来的数据 
import java.net.ServerSocket;   // 导入Java的服务器套接字类,用于创建服务器并监听特定端口
import java.net.Socket;         // 导入Java的Socket类,用于建立客户端与服务器的连接

public class TcpServerDemo01 {
    public static void main(String[] args){
        ServerSocket serverSocket = null;  //声明ServerSocket类型的变量serverSocket,用于监听客户端连接 
        Socket socket = null;    //声明Socket类型的变量socket,并初始化为null,用于与特定客户端通信
        InputStream is = null;   //声明InputStream类型的变量is,并初始化为null,用于从客户端接收数据
        ByteArrayOutputStream baos = null; //声明*类型的变量*,用于存储从输入流中读取的数据

        try {
            ///1.监听客户端连接。创建一个ServerSocket对象,监听端口9999
            serverSocket = new ServerSocket(9999);
            //2.等待客户端连接。开始一个无限循环,使服务器能够持续接受客户端的连接
            while (true){
                //↓ 调用accept方法,等待客户端的连接请求,并返回一个对象Socket,用于与该客户端进行通信
                socket = serverSocket.accept();
                //3.读收客户端的消息。从对象Socket中获取输入流,用于读取客户端发送的数据
                is = socket.getInputStream();

                //管道流
                baos = new ByteArrayOutputStream();  //创建一个ByteArrayOutputStream对象,用于将接收到的字节转换为字节数组
                byte[] buffer = new byte[1024];  //创建一个字节数组作为缓冲区,用于临时存储从输入流中读取的数据
                int len;
//使用循环从输入流中读取数据,直到没有更多数据可读(read方法返回-1)。每次读取的数据都写入ByteArrayOutputStream
                while ((len=is.read(buffer))!=-1){
                    baos.write(buffer,0,len);
                }
                System.out.println(baos.toString());
            }
        } catch (IOException e) {  //捕获IOException异常
            e.printStackTrace();
        } finally {                //finally块中的代码无论是否发生异常都会执行
            if (baos!=null){       //如果baos不为 null,则执行
                try {
                    baos.close();  //关闭字节数组输出流,释放内存资源  
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is != null) {      //如果is不为 null,则执行
                try {
                    is.close();    //关闭输入流,释放相关资源  
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {   //如果socket不为 null,则执行
                try {
                    socket.close(); //关闭Socket连接,释放资源
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {   //如果serverSocket不为 null,则执行
                try {
                    serverSocket.close(); //关闭服务器套接字,释放监听端口和网络资源
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

分析

P20
serverSocket.accept():这是 ServerSocket 类的一个方法,用于接受来自客户端的连接请求。当这个方法被调用时,服务器会阻塞(即暂停执行)当前的线程,等待一个客户端的连接请求。一旦有客户端连接到服务器, accept() 方法就会返回一个新的 Socket 对象,这个对象代表了与客户端的通信通道。
作用:

  1. 等待连接:如果没有客户端连接到服务器, accept()  方法会阻塞当前线程,直到有客户端连接为止。
  2. 接受连接:一旦有客户端连接请求到达, accept() 方法会接受这个连接,并返回一个新的 Socket 对象,该对象代表了服务器与客户端之间的通信通道。

多个连接中的用法:由于 accept() 方法会阻塞,所以在处理多个客户端连接时,通常会创建一个新的线程来执行 accept() 方法,这样主线程可以继续执行其他任务,而新的线程则负责处理客户端的请求。

P22
getInputStream():这是 Socket 类的一个方法。用于从已建立的套接字连接中读取数据。当使用 Socket 类与远程主机建立连接后,可以通过这个方法获取一个 InputStream 对象,用于通过该连接从服务器读取数据(字节流)。

P26
byte[] buffer = new byte[1024];:这行代码创建了一个字节数组并为其分配1024个字节的内存空间。

分部详析
byte[] : 这部分声明了一个字节数组的类型。byte 是Java的基本数据类型之一,用于表示8位有符号整数,其取值范围是 -128 到 127。[] 表示这是一个数组。
buffer : 这是为字节数组自定义的变量名。可以使用这个变量名来访问和操作数组。
new byte[1024] : 这部分代码创建了一个新的字节数组实例,并分配了1024个字节的内存空间。 new 关键字用于创建对象的实例,而 byte[1024] 则指定了要创建的数组的长度,即1024个字节。
作用
在网络编程中,当从套接字(Socket)读取数据时,通常会使用这样的字节数组作为缓冲区来存储接收到的数据。例如,当调用 socket.getInputStream().read(buffer) 时,接收到的数据会被读取到 buffer 数组中。

P29
len=is.read(buffer))!=-1read()是 InputStream 类的一个方法。用于从输入流(InputStream)is 中读取数据到字节数组 buffer 中。该方法返回读取的字节数,如果因为已经到达流的末尾而没有更多的数据可读,那么它会返回 -1。

表达式 len = is.read(buffer) 是将 read 方法返回的读取的字节数赋值给变量 len。因此,len 将包含从输入流中读取的字节数,或者如果到达流的末尾,则包含 -1。

接下来的表达式 len != -1 是一个条件判断,用来检查是否还有更多的数据可以从输入流中读取。如果 len 不是 -1,说明至少读取了一个字节,并且可能还有更多的数据可读。这个条件通常用在循环中,以便持续读取数据,直到没有更多数据为止。

P30
baos.write(buffer, 0, len);write()是 ByteArrayOutputStream 类的一个方法。用于将字节数组(在这里是buffer)的一部分写入到 ByteArrayOutputStream 实例(在这里是baos)中。这个方法有三个参数

  1. buffer:要写入到实例的字节数组。
  2. 0:开始写入的起始偏移量。在这个例子中,从buffer数组的第一个元素(索引为0)开始写入。
  3. len:要写入的字节数。这个值通常是从某个输入流(如网络套接字)读取的字节数,或者是自定义想要写入的buffer数组中的字节数。

这个方法的作用是将buffer数组中从索引0开始、长度为len的字节序列写入到baos中。这通常用于从输入流中读取数据,并将这些数据累积在ByteArrayOutputStream中,直到所有数据都被读取和处理完毕。

在上面的代码示例中,这个write方法被用在一个循环中,循环会一直执行直到is.read(buffer)返回-1,表示没有更多的数据可以读取。在每次循环中,它都会读取最多buffer.length(在这里是1024)个字节的数据到buffer数组中,然后使用write方法将这些字节写入到baos中。这样,当所有数据都被读取后,baos中就包含了从客户端接收到的完整消息。

最后,可以通过调用baos.toString()方法将ByteArrayOutputStream中的内容转换成一个字符串,这样就可以方便地打印出来或进行其他处理。

文件上传的示例

客户端示例

import java.io.*;              // 导入java.io包下的所有类 
import java.net.InetAddress;   // 导入InetAddress类,用于获取主机名对应的IP地址,或者根据IP地址获取对应的主机名
import java.net.Socket;        // 导入Java的Socket类,用于建立客户端与服务器的连接
import java.net.UnknownHostException;  // 导入Java的未知主机异常类,用于处理无法解析主机名的情况

public class TcpClientDemo02 {
    public static void main(String[] args) throws Exception {
        //1.创建一个 Socket 连接。使用Socket类的构造方法创建,传入参数为服务器的IP地址(127.0.0.1)和端口号(9000)
        Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9000);
        //2. 创建一个输出流。获取Socket的输出流,用于向服务器发送数据
        OutputStream os = socket.getOutputStream();
        //3. 创建一个文件输入流。用于读取名为"cat-6723256.jpg"的文件
        FileInputStream fis = new FileInputStream(new File("cat-6723256.jpg"));
        //4.文件输出。创建一个字节数组缓冲区,用于存储从文件中读取的数据
        byte[] buffer = new byte[1024];
        int len;
        //使用循环从文件中读取数据,并写入到Socket的输出流中,直到文件读取完毕
        while ((len=fis.read(buffer))!=-1){
            os.write(buffer,0,len);
        }
        //通知服务器,文件已读取完毕。关闭Socket的输出流,表示客户端已经发送完数据
        socket.shutdownOutput();

        //确定服务器接收完数据,才能够断开连按。
        InputStream inputStream = socket.getInputStream();  //获取Socket的输入流,用于接收来自服务器的数据
        ByteArrayOutputStream baos = new ByteArrayOutputStream(); //创建一个字节数组输出流,用于存储从服务器接收的数
        byte[] buffer2 = new byte[1024];
        int len2;
        //使用循环从Socket的输入流中读取数据,并写入到字节数组输出流中,直到读取完毕
        while ((len2=inputStream.read(buffer2))!=-1){
            baos.write(buffer2,0,len2);
        }
        System.out.println(baos.toString());  //将从服务器接收的数据转换为字符串并打印

        //5.关闭资源
        baos.close();           // 关闭字节数组输出流
        inputStream.close();    // 关闭输入流
        fis.close();            // 关闭文件输入流
        os.close();             // 关闭输出流
        socket.close();         // 关闭Socket连接
    }
}

分析

P22
shutdownOutput():这是 Socket 类中的一个方法。用于在使用 TCP 套接字(Socket)进行网络通信时,关闭此套接字的输出流。换句话说,它告诉远程对等体(即另一端的服务器或客户端):自身不再打算发送任何数据。
调用 shutdownOutput() 后将发生:

  1. 套接字的输出流将被关闭,意味着你不能再向这个套接字写入数据。如果尝试这样做,将会抛出 IOException。
  2. 套接字的输入流(如果有的话)仍然保持打开状态,除非你显式地关闭它或调用 shutdownInput()。
  3. 远程对等体将收到一个 TCP 的正常连接终止信号(FIN),表明对端不再发送数据。然而,这并不意味着连接立即关闭;远程对等体仍然可以发送数据,直到它自己也关闭其输出流或连接。

这个方法通常用于优雅地关闭连接。例如,当客户端完成其数据发送并希望服务器知道它不再发送更多数据时,客户端可能会调用 shutdownOutput()。然后,服务器可以继续读取任何剩余的数据,并在完成后关闭连接。

需要注意的是,仅仅调用 shutdownOutput()不会自动关闭套接字。想要完全关闭套接字,需要调用 Socket.close() 方法。但在调用 close() 之前,可以先调用 shutdownOutput() 和/或 shutdownInput() 来优雅地结束数据传输。

服务端示例

import java.io.*;              // 导入java.io包下的所有类
import java.net.ServerSocket;  // 导入Java的服务器套接字类,用于创建服务器并监听特定端口
import java.net.Socket;        // 导入Java的Socket类,用于建立客户端与服务器的连接

public class TcpServerDemo02 {
    public static void main(String[] args) throws IOException {
        //1.创建服务。创建一个ServerSocket类型的对象serverSocket,用于监听端口9000
        ServerSocket serverSocket = new ServerSocket(9000);
        //2.监听客户端连接。使用ServerSocket的accept方法等待客户端连接,并返回一个Socket对象  
        Socket socket = serverSocket.accept();
        //3.获取输出流。通过Socket对象获取输入流,用于读取客户端发送的数据
        InputStream is = socket.getInputStream();
        //4.文件输出。创建一个FileOutputStream对象,用于将接收到的数据写入到名为"receive.jpg"的文件中
        FileOutputStream fos = new FileOutputStream(new File("receive.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while ((len=is.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }

        //通知服务器,文件已接收完毕
        OutputStream os = socket.getOutputStream(); // 通过Socket对象获取输出流,用于向客户端发送数据  
        os.write("我接收完毕了,你可以断开了".getBytes()); 
        //↑ 将字符串"我接收完毕了,你可以断开了"转换成字节数组,并通过输出流发送给客户端

        //关闭资源
        fos.close();            // 关闭文件输出流,释放与文件关联的资源
        is.close();             // 关闭输入流,释放与Socket关联的资源
        socket.close();         // 关闭Socket连接,释放网络资源  
        serverSocket.close();   // 关闭ServerSocket,释放监听端口资源  
    }
}

Tomcat

Tomcat是一个开源的Web应用服务器,主要用于Java Web开发。具体来说,Tomcat可以用于以下几个方面:

  1. 运行Java Web应用程序:Tomcat可以部署和运行标准的Java Web应用程序,如JSP(Java Server Pages)、Servlet、JSF(JavaServer Faces)和JSTL(JSP Standard Tag Library)等。
  2. 管理Web应用程序:Tomcat提供管理Web应用程序的功能,包括部署、启动、停止、重新加载和卸载Web应用程序。
  3. 处理HTTP请求:Tomcat支持HTTP协议,可以处理来自客户端的HTTP请求和响应,并支持HTTPS透明加密连接。
  4. 实现动态网站:Tomcat可以与JSP、Servlet和JavaBeans等技术结合使用,实现动态网站的开发。
  5. 跨平台支持:Tomcat可以在多种操作系统上运行,如Windows、Linux、Unix和Mac OS等。

总之,Tomcat是一个非常重要的工具,它为Java Web开发提供了方便的运行环境和强大的功能支持。无论是开发者还是系统管理员,都可以通过Tomcat来快速部署和管理Web应用程序,实现高质量的Web服务。

Tomcat服务器下载、安装、配置环境变量教程

UDP

发送消息的示例

客户端示例

import java.net.DatagramPacket;   // 导入Java的数据报包类,用于UDP通信中数据的封装
import java.net.DatagramSocket;   // 导入Java的数据报套接字类,用于创建UDP套接字
import java.net.InetAddress;      // 导入Java的Internet地址类,用于表示IP地址
import java.net.SocketException;  // 导入Java的网络异常类,用于处理网络操作中出现的异常

//不而要连接服务器
public class UdpClientDemo01 {
    public static void main(String[] args) throws Exception {
        //1.创建UDP套接字:使用默认端口创建一个新的DatagramSocket实例。这表示客户端将使用系统分配的任意可用端口
        DatagramSocket socket = new DatagramSocket();
        //2.准备发送的数据:msg:要发送的消息的内容;port:指定服务器的监听端口
        String msg = "你好啊,服务器!";
        int port = 9090;
        //确定数据报的目的地:使用InetAddress.getByName方法获取表示本地主机的InetAddress实例。这意味着数据报将发送到运行此客户端的同一台机器上的服务器
        InetAddress localhost = InetAddress.getByName("localhost");
        //封装数据报:使用DatagramPacket构造函数创建一个新的数据报。该数据报包含要发送的数据(这里是msg的字节形式)、数据的偏移量(从字节数组的开始位置)、数据的长度(msg的字节长度)、以及数据报的目的地(localhost和port)
        DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.getBytes().length, localhost, port);

        //3.发送数据报:使用DatagramSocket的send方法发送之前封装好的数据报
        socket.send(packet);

        //4.关闭套接字:使用close方法关闭DatagramSocket,释放与其关联的资源
        socket.close();
    }
}

分析

P20
socket.send(packet);

  • socket 是一个 DatagramSocket 对象,它代表了UDP通信的端点。在发送数据之前,你需要创建这个对象
  • send 是 DatagramSocket 类的一个方法,用于发送 DatagramPacket 对象
  • packet 是一个 DatagramPacket 对象,它包含了要发送的数据以及目标IP地址和端口号

使用 DatagramSocket 的 send 方法来发送UDP数据报( DatagramPacket )是一种常见的网络通信方式

服务端示例

import java.net.DatagramPacket;   // 导入Java的数据报包类,用于UDP通信中数据的封装
import java.net.DatagramSocket;   // 导入Java的数据报套接字类,用于创建UDP套接字
import java.net.SocketException;  // 导入Java的网络异常类,用于处理网络操作中出现的异常

public class UdpServerDemo01 {
    public static void main(String[] args) throws Exception {
    
        //开放端口:创建一个DatagramSocket对象,并开始监听9090端口上的UDP数据包
        DatagramSocket socket = new DatagramSocket(9090);
        
        //接收数据:创建一个大小为1024字节的字节数组buffer,用于存储接收到的UDP数据包的内容
        byte[] buffer = new byte[1024];
        
        //创建一个DatagramPacket对象,该对象封装了之前创建的buffer数组,并指定了另外两个参数
        DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
        
        //阻塞接收:使用socket对象的receive方法接收UDP数据包,并将其存储在packet对象中
        //这个方法会阻塞程序的执行,直到收到一个数据包为止
        socket.receive(packet);
        
        System.out.println(packet.getAddress().getHostAddress());  //打印出接收到的数据包的IP地址
        
        //将接收到的数据包的内容(字节数组)转换为字符串,并打印出来
        //这里使用了packet.getData()获取字节数组,packet.getLength()获取实际的数据长度
        System.out.println(new String(packet.getData(),0,packet.getLength()) );
        
        System.out.println(packet);//打印packet对象的信息,包括发送方的IP地址、发送方的端口、接收到的数据内容等
        
        //关闭连接:关闭socket对象,释放相关资源
        socket.close();
    }
}

分析

P18
getData():这是 DatagramPacket 类的一个方法,用于获取该数据包中包含的数据。返回一个字节数组(byte[]),该数组包含了数据包的原始字节数据。调用此方法不需要任何参数

getLength():这是 DatagramPacket 类的一个方法,用于获取该数据包中数据的长度(以字节为单位)。返回一个整数,表示数据包中数据的长度。调用此方法同样不需要任何参数

这两个方法都是 DatagramPacket 类的实例方法,用于处理 UDP 数据包中的数据。当你创建一个DatagramPacket 对象来接收 UDP 数据时,你可以使用 getData() 方法来访问数据,并使用 getLength() 方法来确定数据的长度。

循环发送消息的示例

发送方示例

import java.io.BufferedReader;     // 导入Java的字符输入流,用于从文件或网络连接中读取文本数据
import java.io.InputStreamReader;  // 导入Java的桥接字节流和字符流的类,用于从字节流(如网络连接)中读取文本
import java.net.DatagramPacket;    // 导入Java的数据报包类,用于UDP通信中数据的封装
import java.net.DatagramSocket;    // 导入Java的数据报套接字类,用于创建UDP套接字
import java.net.InetSocketAddress; // 导入Java的表示IP地址和端口号的类,用于指定网络连接的目标地址
import java.net.SocketException;   // 导入Java的网络异常类,用于处理网络操作中出现的异常

public class UdpSenderDemo01 {
    public static void main(String[] args) throws Exception {
        //创建了一个DatagramSocket实例socket,并在端口8888上绑定它。这意味着此UDP发送器将尝试从该端口发送数据
        DatagramSocket socket = new DatagramSocket(8888);

        //准备数据:控制台读收 System.in
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        //开始一个无限循环,这将持续读取数据并发送,直到遇到“bye”为止
        while (true){
            String data = reader.readLine();  //从控制台读取一行数据
            byte[] datas = data.getBytes();   //将读取的字符串数据转换为字节数组,因为UDP发送需要字节数据
            
            //创建一个DatagramPacket实例,封装要发送的数据和目标地址。这里,数据发送到本地机器(localhost)的6666端口
            DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("localhost", 6666));
            socket.send(packet);       //使用之前创建的DatagramSocket实例发送数据报
            if (data.equals("bye")){   //如果从控制台读取的数据是“bye”,则跳出循环
                break;
            }
        }
        socket.close();   //关闭UDP套接字,释放资源
    }
}

分析

P14
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
在Java中,BufferedReader 是一个字符输入流,用于从字符输入流中读取文本,缓冲字符,以提供字符、数组和行的高效读取。InputStreamReader 是一个桥接字节流和字符流的类,它可以将字节流转换为字符流。而 System.in 是Java中表示标准输入流的静态成员,通常指的是键盘输入。

当执行以下代码时:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

则是在创建一个 BufferedReader 实例,用于从控制台(标准输入 System.in)读取数据。这里涉及几个步骤和概念:

  1. System.in: 这是Java的标准输入流,通常用于从键盘读取用户输入。它是一个 InputStream 类型的对象。
  2. InputStreamReader: 这个类是一个桥接器,它将 InputStream(字节流)转换为 Reader(字符流)。这是必要的,因为 BufferedReader 需要一个字符流作为输入。InputStreamReader 使用系统默认的字符集来解码字节为字符。
  3. BufferedReader: 这个类使用内部缓冲区来存储从 Reader 中读取的字符。这使得从流中读取字符、数组和行变得高效,因为读取操作通常是从内部缓冲区中进行的,而不是直接从底层的 InputStreamReader 中进行。

结合起来,new BufferedReader(new InputStreamReader(System.in)) 创建了一个可以从控制台高效读取字符、数组和行的 BufferedReader 实例。这样,你就可以使用 reader.readLine() 方法来读取用户从控制台输入的一行文本,或者使用其他 BufferedReader 的方法来进行更复杂的读取操作。

例如,可以这样使用 BufferedReader 来读取用户输入的一行文本:

String data = reader.readLine();
System.out.println("You entered: " + data);

这段代码会等待用户在控制台输入一行文本,然后按下回车键。输入的内容会被存储在 data 变量中,并随后被打印出来。

接收方示例

import java.net.DatagramPacket;    // 导入Java的数据报包类,用于UDP通信中数据的封装
import java.net.DatagramSocket;    // 导入Java的数据报套接字类,用于创建UDP套接字
import java.net.SocketException;   // 导入Java的网络异常类,用于处理网络操作中出现的异常

public class UdpReceiveDemo01 {
    public static void main(String[] args) throws Exception {
    //创建一个DatagramSocket实例,并绑定到端口6666。这意味着此UDP接收器将尝试在端口6666上接收数据
        DatagramSocket socket = new DatagramSocket(6666);
        while (true){
            //准备接受包裹
            byte[] container = new byte[1024];  //创建一个大小为1024字节的字节数组,用于存储接收到的数据
            //创建一个DatagramPacket实例,并准备接收数据。这里,container数组作为数据缓冲区,`0`是偏移量,container.length是缓冲区的大小
            DatagramPacket packet = new DatagramPacket(container, 0, container.length);
            socket.receive(packet);//阻塞式接受包裹

            //断开连接
            byte[] data = packet.getData();  //从DatagramPacket实例中获取接收到的数据,存储到data字节数组中
            String receiveData = new String(data, 0, packet.getLength());  //将接收到的字节数据转换为字符串。packet.getLength()方法返回实际接收到的数据长度

            System.out.println(receiveData);  //在控制台上打印接收到的数据

            if (receiveData.equals("bye")){  //如果接收到的数据是“bye”,则跳出循环
                break;
            }

        }
        socket.close();  //关闭UDP套接字,释放与之关联的资源
    }
}

分析

P14
receive():这是 DatagramSocket 实例的一个方法,用于阻塞式地接收数据报。这意味着程序会在 receive 调用处暂停,不做任何事情,直到网络上有一个数据报到达并准备好被读取。

equals():这是 Object 类的一个方法,用于比较当前字符串对象A 与作为参数传递进来的另一个字符串对象B 的内容是否相同。如果内容相同,则返回 true;如果内容不同,则返回 false。([字符串A].equals("[字符串B]"))

多线程循环发送消息的示例

发送端示例

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

 
public class TalkSend implements Runnable{  // 声明一个名为TalkSend的公共类,它实现了Runnable接口,因此可以在线程中运行
    DatagramSocket socket = null;  // 声明一个DatagramSocket类型的变量socket,用于发送UDP数据包
    BufferedReader reader = null;  // 声明一个BufferedReader类型的变量reader,用于从标准输入(键盘)读取文本
    private int fromPort;          // 声明一个整型变量fromPort,表示发送方的端口号
    private String toIP;           // 声明一个字符串变量toIP,表示接收方的IP地址
    private int toPort;            // 声明一个整型变量toPort,表示接收方的端口号
    public TalkSend(int fromPort, String toIP, int toPort) {  // 构造方法,用于初始化TalkSend类的实例
        this.fromPort = fromPort;  // 设置发送方的端口号
        this.toIP = toIP;          // 设置接收方的IP地址
        this.toPort = toPort;      // 设置接收方的端口号
        try {
            socket = new DatagramSocket(fromPort);  // 创建一个DatagramSocket实例,并绑定到指定的fromPort端口
            reader = new BufferedReader(new InputStreamReader(System.in));  // 创建一个BufferedReader实例,用于从标准输入(System.in)读取文本
        } catch (Exception e){  
            e.printStackTrace();  // 如果在创建socket或reader时发生异常,打印异常堆栈信息
        }  
    }  
      
    @Override  
    public void run() {  // 实现Runnable接口的run方法,这是线程执行时调用的方法
        while (true){    // 无限循环,用于持续从控制台读取数据并发送,直到输入"bye"为止
            try {  
                String data = reader.readLine();  // 从标准输入(键盘)读取一行文本
                byte[] datas = data.getBytes();   // 将读取的字符串转换为字节数组,因为UDP发送需要字节数据
                 
                 // 创建一个新的DatagramPacket实例,封装要发送的数据和接收方的地址信息
                DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress(this.toIP, this.toPort));  
                socket.send(packet);      // 使用DatagramSocket实例发送数据报
                if (data.equals("bye")){  // 如果读取的数据是"bye",则跳出循环,停止发送数据
                    break;  
                }  
            } catch (Exception e){  
                e.printStackTrace();  // 如果在发送数据过程中发生异常,打印异常堆栈信息
            }  
        }  
        socket.close();  // 关闭DatagramSocket,释放资源
    }  
}

接收端示例

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

public class TalkReceive implements Runnable{     // 定义一个名为TalkReceive的公共类,它实现了Runnable接口  
    DatagramSocket socket = null;  // 定义一个DatagramSocket对象,用于接收UDP数据包  
    private int port;              // 定义一个整型变量port,用于存储要监听的端口号
    private String msgFrom;        // 定义一个字符串变量msgFrom,用于标识消息来源
    public TalkReceive(int port,String msgFrom) {  // 构造方法,初始化端口和消息来源,并尝试创建一个DatagramSocket实例 
        this.port = port;        // 设置端口号  
        this.msgFrom = msgFrom;  // 设置消息来源  
        try {  
            socket = new DatagramSocket(port);  // 尝试在指定端口上创建DatagramSocket  
        } catch (SocketException e) {  
            e.printStackTrace(); 
        }  
    }  
      
    @Override          // 实现Runnable接口的run方法,这是线程的主体部分
    public void run() {  
        while (true){  // 无限循环,持续接收数据包
            try {  
                byte[] container = new byte[1024];  // 准备一个字节数组作为接收数据包的容器
                DatagramPacket packet = new DatagramPacket(container, 0, container.length);  // 创建一个DatagramPacket实例,用于接收数据
                socket.receive(packet);             // 阻塞式接收数据包 
                byte[] data = packet.getData();     // 获取接收到的数据
                String receiveData = new String(data, 0, packet.getLength());  // 将接收到的字节数据转换为字符串
                System.out.println(msgFrom + ": " + receiveData);  // 打印接收到的消息,包括消息来源和内容
                if (receiveData.equals("bye")){     // 如果接收到的消息是"bye",则退出循环
                    break;  
                }  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
        socket.close();  // 关闭DatagramSocket,释放资源
    }  
}

学生线程示例

public class TalkStudent {  
    public static void main(String[] args) {  
        // 创建一个新的线程,该线程执行TalkSend类的实例,该实例在端口7777上监听并发送数据到localhost的9999端口  
        new Thread(new TalkSend(7777,"localhost",9999)).start();  
  
        // 创建另一个新的线程,该线程执行TalkReceive类的实例,该实例在端口8888上接收数据,并标记接收者的名称为"老师"  
        new Thread(new TalkReceive(8888,"老师")).start();  
    }  
}

老师线程示例

public class TalkTeacher {  
    // 主方法,是Java程序的入口点  
    public static void main(String[] args) {  
        // 创建一个新的线程,该线程执行TalkSend类的实例,该实例在端口5555上监听,并将数据发送到localhost的8888端口  
        new Thread(new TalkSend(5555,"localhost",8888)).start();  
  
        // 创建另一个新的线程,该线程执行TalkReceive类的实例,该实例在端口9999上接收数据,并标记接收者的名称为"学生"  
        new Thread(new TalkReceive(9999,"学生")).start();  
    }  
}

URL

URL(Uniform Resource Locator,统一资源定位符)是互联网上标准资源的地址,用于标识和定位互联网上的资源。它通过一个标准的格式来表示互联网资源的位置,并且可以指向各种类型的资源,如网页、图片、视频、文件等。

URL通常由多个部分组成,其标准格式如下:
<协议类型>://<主机名>:<端口号>/<路径>/<文件名>?<查询参数>#<锚点>

虽然这个格式包含了很多部分,但并非每个URL都会完整包含这些部分,一些部分是可以省略的。以下是各部分的详细解释:

  1. 协议类型:指定使用的传输协议,最常见的是HTTP和HTTPS。HTTP是超文本传输协议,用于传输普通的网页内容。而HTTPS是HTTP的安全版,用于加密传输敏感信息,如信用卡信息、登录凭据等。
  2. 主机名:指定了资源所在的主机,通常是一个域名,如www.example.com。也可以是IP地址,但IP地址不如域名易于记忆。
  3. 端口号:指定了资源所在主机的端口号。不同的服务通常使用不同的端口号。例如,HTTP服务的默认端口号是80,HTTPS服务的默认端口号是443。如果使用的是默认端口号,那么在URL中可以省略端口号。
  4. 路径:指定了资源在主机上的位置。路径可以是相对路径,也可以是绝对路径。例如,/index.html表示主机根目录下的index.html文件。
  5. 文件名:指定了具体的资源文件。这通常是路径的一部分,但在某些情况下,也可以单独指定。
  6. 查询参数:用于传递额外的信息给服务器。查询参数通常用于搜索、过滤或定制网页内容。例如,?q=keyword表示搜索关键字为"keyword"。
  7. 锚点:用于指定网页内部的定位点。当用户点击一个包含锚点的链接时,浏览器会滚动到网页的指定位置。锚点以#开头,后面跟着锚点的名称。

示例

  1. 展示如何使用Java的URL类来解析一个URL字符串,并从中提取出各个组成部分,如协议、主机名、端口号、路径和查询参数。
import java.net.MalformedURLException;  // 导入java.net包中的MalformedURLException类,这个异常类用于处理URL格式不正确的情况
import java.net.URL;   // 导入java.net包中的URL类,这个类用于表示一个统一资源定位符,即URL

public class URLDemo01 {
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=kuangshen&password=123");
        System.out.println(url.getProtocol());  //协议
        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());     //URL查询名字 参数
    }
}

2 . 从指定的URL地址下载一个.m4a音频文件,并将其保存到本地的y.m4a文件中。

import java.io.FileOutputStream;    // 导入java.io.FileOutputStream类,用于将数据写入文件输出流 
import java.io.InputStream;         // 导入java.io.InputStream类,用于读取字节流数据 
import java.net.HttpURLConnection;  // 导入java.net.HttpURLConnection类,用于处理HTTP连接 
import java.net.MalformedURLException;  // 导入java.net.MalformedURLException类,用于处理URL格式错误的情况
import java.net.URL;                // 导入java.net.URL类,表示一个统一资源定位符,即网址
import java.net.URLConnection;      // 导入java.net.URLConnection类,是URL类的子类,用于打开到URL的连接

public class URLDown {
    public static void main(String[] args) throws Exception {  // 创建一个URL对象,表示要下载的资源的地址
        URL url = new URL("https://m701.music.126.net/20220123220857/b014483528776645552494faff4733a0/jdyyaac/obj/w5rDlsOJwrLDjj7CmsOj/11506531065/c33e/3db1/f24c/d25af884b0dc20c1179631516e93341a.m4a");  
        
        // 通过URL对象的openConnection()方法,创建一个HttpURLConnection对象,用于建立到指定URL的HTTP连接  
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();  
        // 通过HttpURLConnection对象的getInputStream()方法,获取一个InputStream对象,用于读取该连接的数据流  
        InputStream inputStream = urlConnection.getInputStream();  
        // 创建一个FileOutputStream对象,用于将下载的数据写入到本地文件"y.m4a"中  
        FileOutputStream fos = new FileOutputStream("y.m4a");  
        
        byte[] buffer = new byte[1024];  // 创建一个大小为1024字节的缓冲区
        int len;  
        while ((len=inputStream.read(buffer))!=-1){  // 使用循环从输入流中读取数据,直到读取完毕(即返回-1)
            fos.write(buffer,0,len);     // 将从输入流 inputStream 读取到的数据写入到文件输出流 fos 所指向的文件中
        }  
        fos.close();                  // 关闭文件输出流,完成文件写入操作
        inputStream.close();          // 关闭输入流,释放与之关联的资源
        urlConnection.disconnect();   // 断开与URL的连接,释放与之关联的资源  
    }  
}