简单实现网络编程

1. 前置知识

在学习网络编程前,我们需要先了解一些前置知识

1.1 客户端和服务器

在网络编程中,客户端和服务器是两个关键的角色。

客户端是发起连接并向服务器发送请求的一方。客户端通常是一个应用程序或设备,通过与服务器建立连接,发送请求并接收响应来获取所需的服务或数据。

服务器是提供服务或数据的一方。服务器通常是一个强大的计算机,它等待客户端的连接请求,并根据请求提供相应的服务或数据。服务器可以同时处理多个客户端的请求,每个请求都会分配给一个独立的线程或进程进行处理。服务器也使用特定的协议来与客户端进行通信。

1.2 请求和响应

请求(Request):请求是客户端发起的一个操作或服务请求。客户端通过发送请求给服务器,表达其需要获取某项服务、数据或执行某个操作的意图。

响应(Response):响应是服务器对客户端请求的回应。服务器接收到请求后,根据请求内容执行相应的操作,并返回相应的结果给客户端。

请求和响应之间的关系通常是一对一的。每个请求都对应着一个相应的响应。客户端发送请求后,服务器接收并处理请求,并生成相应的响应返回给客户端。客户端接收到响应后,可以根据响应的状态码和内容进行相应的处理。

也有特定情况下,存在一对多,多对一,多对多。

2. TCP/UDP 协议之间的差别

进行网络编程,本质上是使用传输层的协议提供的API接口。传输层主要有两个协议,TCP和UDP由于这两个协议之间存在一些差异,所以,它们的API也存在一些差异。这里我们先简单介绍一下TCP和UDP的差异。

TCP的特点:

  1. 有连接(Connection-Oriented):TCP是一种面向连接的协议,即在进行数据传输之前,必须先建立双方之间的连接。连接建立后,双方可以进行数据传输,传输完成后再关闭连接。

  2. 可靠传输(Reliable Transmission):TCP提供可靠的数据传输机制,保证数据的完整性、顺序性和不丢失。为了实现可靠传输,TCP采用了多种机制,如序列号、确认应答、超时重传、流量控制和拥塞控制等。通过这些机制,TCP可以检测并纠正数据传输中的错误,并确保数据按正确的顺序到达目标。

  3. 面向字节流(Byte-Oriented):TCP是一种面向字节流的协议,意味着数据在发送端和接收端之间是按照字节流的方式进行传输的,而不考虑应用层的消息边界。发送端将应用层数据分割成小块的字节流,在接收端进行重新组装。这种特性使得TCP更加灵活,可以适应不同大小的数据传输。

  4. 全双工(Full Duplex):TCP连接是全双工的,意味着数据可以在双方同时进行双向传输。发送端和接收端可以同时发送和接收数据,而且两个方向的数据流是独立的,互不影响。这种特性使得双方可以同时进行实时的双向通信,提高了传输效率。

UDP的特点:

  1. 无连接(Connectionless):UDP是一种无连接的协议,发送端和接收端之间不需要建立连接。每个UDP数据包都是独立的,可以单独发送并独立处理,不需要等待前面的数据包确认。

  2. 不可靠传输(Unreliable Transmission):与TCP不同,UDP不提供可靠的数据传输机制。UDP数据包被发送后,不会去确认是否到达目标地址,也不会进行重传操作。这意味着在网络传输过程中,可能会出现丢包、乱序或重复的情况。

  3. 面向数据报(Datagram-Oriented):UDP是一种面向数据报的协议,每个UDP数据包被视为一个独立的数据报文。每个数据报都有自己的头部信息,包含了源地址、目标地址、长度等字段。由于数据报之间是独立的,因此UDP可以灵活地处理不同大小的数据。

  4. 全双工(Full Duplex)UDP也可以在双方同时进行双向传输。

3. 网络编程

操作系统给我们提供的网络编程的 API叫做 "Socket APOI" 即 "网络编程套接字"

3.1 UDP Socket API 的使用

在Java中,其实是把操作系统提供的原生API进行封装过的,所以我们调用的API其实是JVM提供的。

使用UDP进行网络编程,核心的类有两个:

3.1.1 DatagramSocket

DatagramSocket类表示一个UDP套接字,它用于在端点之间发送和接收UDP数据包。DatagramSocket对象可以绑定到本地IP地址和端口号,以便在该地址和端口上进行监听和传输数据包(操作系统中有一类文件叫做Socket,Socket文件抽象表示了网卡这样的设备,DatagramSocket就是通过读写Socket文件,来发送和接收数据的)。

DatagramSocket的常用方法包括:

  • DatagramSocket():构造函数,创建一个绑定到本机随机端口号的DatagramSocket对象(通常用于客户端。
  • DatagramSocket(int port):构造函数创建一个绑定到本机指定端口号的DatagramSocket对象(通常用于服务器)。
  • void send(DatagramPacket p):发送指定的数据包。
  • void receive(DatagramPacket p):接收一个数据包,并将其存储在指定的DatagramPacket对象中,如果没有接收到数据报,该方法会阻塞。
  • void close():关闭DatagramSocket对象。
3.1.2 DatagramPacket

DatagramPacket类表示一个UDP数据包,它包含了要发送或接收的数据、目标地址、目标端口等信息,相当于储存数据的一个载体。DatagramPacket对象可以用于在DatagramSocket之间传递UDP数据包。

DatagramPacket的常用方法包括:

  • DatagramPacket(byte[] buf, int length):构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)。
  • DatagramPacket(byte[] buf, int length, InetAddress address, int port):构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP,port表示端口号。
  • byte[] getData():获取数据包的数据。
  • int getLength():获取数据包的长度。
  • InetAddress getAddress():获取数据包的发送端地址。
  • int getPort():获取数据包的发送端端口号。
  • void setData(byte[] buf):设置数据包的数据。
  • void setLength(int length):设置数据包的长度。
  • void setAddress(InetAddress address):设置数据包的目标地址。
  • void setPort(int port):设置数据包的目标端口号。 
3.1.3 代码示例

编写代码实现一个回显服务器,返回客户端发送的请求,即客户端发什么就返回什么。

public class UdpEchoServer {
    private DatagramSocket socket = null;//接收/发出数据
    public UdpEchoServer(int port) throws SocketException {
        //构造方法,传入端口号,指定服务器绑定的端口号
        socket = new DatagramSocket(port);
    }
    //运转逻辑,包含接收数据,返回响应
    public void start() throws IOException {
        System.out.println("服务器 启动!!!");
        while(true) {
            //请求数据报,用于接收客户端的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //调用receive方法接收数据
            socket.receive(requestPacket);
            //将接收到的数据转为字符串存储起来
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //打印出发送端的IP,端口号,和接收到的数据
            System.out.print(requestPacket.getAddress().toString() + " " + requestPacket.getPort() + ":" + request);
            //调用方法,构造出对应的响应(这个方法需自己实现)
            String response = process(request);
            //构造响应数据报,注意此处要传入对应的IP地址和端口号,可以用接收到的数据报,调用对应方法获取
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getAddress(),requestPacket.getPort());
            //发送数据
            socket.send(responsePacket);
            //打印出返回的信息
            System.out.println(" <返回>:" + response);
        }
    }
    private String process(String request) {
        //根据构造响应,我们这里直接返回request
        return request;
    }
    public static void main(String[] args) throws IOException {
        //实例化回显服务器
        UdpEchoServer udpEchoServer = new UdpEchoServer(7510);
        //调用start方法启动服务器
        udpEchoServer.start();
    }

有了服务器我们还要实现一个客户端用来发送请求:

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp = null;//目标IP地址,即服务器的地址
    private int serverPort = 0;//目标端口号,即服务器的端口号
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        //构造方法
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }
    //实现start方法用于启动服务器
    public void start() throws IOException {
        System.out.println("客户端 启动!!!");
        Scanner in = new Scanner(System.in);//用于输入请求
        while(true) {
            //输入请求
            String request = in.next();
            //构造请求数据报,注意不能直接把字符串形式的IP地址传入,需要调用InetAddress类中的getByName方法,把字符串传入
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            //发送请求
            socket.send(requestPacket);
            //接收响应,
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //用字符串储存响应
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            //输出响应
            System.out.println(">" + response);
        }
    }
    public static void main(String[] args) throws IOException {
        //创建客户端对象,注意,IP和端口号要和服务器的对应,127.0.0.0 为本机IP
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 7510);
        udpEchoClient.start();
    }
}

现在我们可以运行代码查看效果了,注意要先运行服务器,再运行客户端。

3.2 TCP Socket API 的使用 

UDP传输数据是无连接的,相当于 "发短信" ,TCP传输数据是有连接的,相当于 "打电话"

3.2.1 ServerSocker

ServerSocket类是用于在服务器端监听特定端口、接受客户端连接请求的类。

通过创建ServerSocket对象,可以将其绑定到指定的IP地址和端口号,从而使服务器能够监听该端口并等待客户端连接。

ServerSocket类的常用方法:

  • ServerSocket(int port):创建一个绑定到指定端口的ServerSocket对象。
  • Socket accept():监听客户端连接请求,接受客户端的连接,并返回一个Socket对象用于与客户端进行通信,如果当前没有客户端连接,该方法会阻塞。
  • void close():关闭ServerSocket对象,释放相关资源。

ServerSocket只能给服务器使用。 

 3.2.2 Socket

Socket类是用于在客户端与服务器端建立连接并进行通信的类。
通过创建Socket对象,可以指定服务器的IP地址和端口号,从而与服务器建立连接。

  • Socket(String host, int port):创建一个与指定服务器IP地址和端口号建立连接的Socket对象。
  • InputStream getInputStream():获取与Socket对象关联的输入流,用于从服务器端接收数据。
  • OutputStream getOutputStream():获取与Socket对象关联的输出流,用于向服务器端发送数据。
  • InetAddress getInetAddress():获取连接的地址。
  • int getPort():获取连接的端口号。
  • void close():关闭Socket对象,释放相关资源。
 3.2.3 代码示例

接下来我们同样使用TCP的API实现一个回显服务器,和客户端。

服务器:

public class TcpEchoServer {
    private ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器 启动!!!");
        while(true) {
            //和客户端建立连接,如果没有客户端连接,该方法会阻塞
            Socket client = socket.accept();
            //连接成功,打印信息
            System.out.printf("[%s,%d]已连接\n", client.getInetAddress(), client.getPort());
            //通过接收到的Socket对象,获取到输入,输出流
            //循环读取输入流中的需求

            try(InputStream inputStream = client.getInputStream();
            OutputStream outputStream = client.getOutputStream()) {
                while(true) {
                    //通过scanner在输入流中读取数据
                    Scanner scanner = new Scanner(inputStream);
                    if(!scanner.hasNext()) {
                        //读取完毕,说明连接断开了
                        System.out.printf("[%s,%d]已断开\n", client.getInetAddress(), client.getPort());
                        break;
                    }
                    String request = scanner.next();
                    //调用方法根据请求构造响应
                    String response = process(request);

                    //使用print的子类PrintWriter,写入响应
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //调用flush方法立刻把response写入硬盘
                    printWriter.flush();

                    //打印日志
                    System.out.printf("[%s,%d] req:%s,res:%s\n",client.getInetAddress(),client.getPort(), request, response);
                }
                //释放资源
                client.close();
            }
        }
    }
    public String process(String request) {
        //根据请求构造响应
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(7510);
        server.start();
    }
}

客户端:

package TCP;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String ServerIp, int ServerPort) throws IOException {
        //传入服务器的 IP地址 和 端口号,在创建该对象时就会向服务器发送连接,
        // 创建完毕后等待服务器调用accept即可连接上
        socket = new Socket(ServerIp, ServerPort);
    }
    public void start() throws IOException {
        System.out.println("客户端 启动!!!");
        //连接成功,循环输入请求,获取响应
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            //从键盘接收请求
            Scanner in = new Scanner(System.in);
            //输出响应
            Scanner scanner = new Scanner(inputStream);
            //把请求输出给服务器
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true) {
                //接收请求
                System.out.print("> ");
                String request = in.next();

                //发送给服务器
                printWriter.println(request);
                //调用flush方法立刻把response写入硬盘
                printWriter.flush();

                //接受响应并输出
                String response = scanner.next();
                System.out.println(" >" + response);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 7510);
        client.start();
    }
}

现在我们运行代码,发现可以正常完成功能:

3.2.4 改进

我们现在的代码每次只能连接一个客户端,因为我们连接了一个客户端之后,除非这个客户端断开连接,否则是出不了循环的,也就无法再次执行accept 连接新的客户端。

所以我们可以使用多线程的方法,给每个连接的客户端分配一个线程:

public class TcpEchoServer {
    private ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器 启动!!!");
        while(true) {
            //和客户端建立连接,如果没有客户端连接,该方法会阻塞
            Socket client = socket.accept();
            //连接成功,打印信息
            System.out.printf("[%s,%d]已连接\n", client.getInetAddress(), client.getPort());
            //创建一个线程,处理本次连接
            Thread t = new Thread(()->{
                //为了代码简洁,我们把处理逻辑单独封装为一个方法
                try {
                    processConnect(client);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
    public void processConnect(Socket client) throws IOException {
        try(InputStream inputStream = client.getInputStream();
            OutputStream outputStream = client.getOutputStream()) {
            while(true) {
                //通过scanner在输入流中读取数据
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()) {
                    //读取完毕,说明连接断开了
                    System.out.printf("[%s,%d]已断开\n", client.getInetAddress(), client.getPort());
                    break;
                }
                String request = scanner.next();
                //调用方法根据请求构造响应
                String response = process(request);

                //使用print的子类PrintWriter,写入响应
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //调用flush方法立刻把response写入硬盘
                printWriter.flush();

                //打印日志
                System.out.printf("[%s,%d] req:%s,res:%s\n",client.getInetAddress(),client.getPort(), request, response);
            }
            //释放资源
            client.close();
        }
    }
    public String process(String request) {
        //根据请求构造响应
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(7510);
        server.start();
    }
}

现在我们的服务器就可以同时连接多个客户端了,我们也可以把上述代码优化为线程池的版本,节省线程大量创建和销毁带来的开销。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/344678.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

白盒测试和黑盒测试的区别

黑盒测试 等价类划分 白盒测试 灰盒测试

K8S图像化工具rancher

Rancher是一个开源的企业级多集群的k8s管理平台 Rancher和k8s的区别 都是为了容器的调度和编排系统&#xff0c;但是rancher不仅能够调度&#xff0c;还能挂历k8s集群&#xff0c;自带监控&#xff08;普罗米修斯&#xff09;&#xff0c;你哪怕不知带k8s是什么&#xff0c;一样…

跟着pink老师前端入门教程-day09

二十二、定位 22.1 为什么需要定位 1. 某个元素可以自由的在一个盒子内移动位置&#xff0c;并且压住其他盒子 2. 当我们滚动窗口时&#xff0c;盒子是固定屏幕某个位置的 解决方法&#xff1a; 1. 浮动可以让多个块级盒子一行没有缝隙排列显示&#xff0c;经常用于横向排…

C#用DateTime.Now静态属性返回日期的星期信息

目录 一、使用的方法 1.Now属性 2.ToString方法 二、示例 使用DateTime结构的Now静态属性&#xff0c;可以方便地获取系统日期信息。调用时间对象的ToString方法&#xff0c;在该方法的参数中添加适当的格式化字符串&#xff0c;将返回日期的星期信息。 一、使用的方法 1…

C语言入门到精通之练习实例10:打印楼梯,同时在楼梯上方打印两个笑脸(附源码)

题目&#xff1a;打印楼梯&#xff0c;同时在楼梯上方打印两个笑脸。 程序分析&#xff1a;用 ASCII 1 来输出笑脸&#xff1b;用i控制行&#xff0c;j来控制列&#xff0c;j根据i的变化来控制输出黑方格的个数。 如果出现乱码情况请参考【C 练习实例7】的解决方法。 // Cr…

uniapp 在static/index.html中添加全局样式

前言 略 在static/index.html中添加全局样式 <style>div {background-color: #ccc;} </style>static/index.html源码&#xff1a; <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"utf-8"><meta http-…

从零开始用Rust编写nginx,命令行参数的设计与解析及说明

wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器&#xff0c;四层TCP/UDP转发&#xff0c;七层负载均衡&#xff0c;内网穿透&#xff0c;后续将实现websocket代理等&#xff0c;会将实现过程分享出来&#xff0c;感兴趣的可以一起造个轮子 …

教育研究方法有哪些分类

教育研究方法的分类是多种多样的&#xff0c;可以从不同的角度进行划分。根据研究目的、研究范围、研究层次等&#xff0c;可以将教育研究方法分为不同的类型。 一、根据研究目的划分 基础性研究基础性研究也称为理论性研究&#xff0c;主要是为了探索和发现新的理论观点和原…

uniapp开发过程一些小坑

问题1、uniapp使用scroll-view的:scroll-into-view“lastChatData“跳到某个元素id时候&#xff0c;在app上不生效&#xff0c;小程序没问题 使用this.$nextTick或者 setTimeout(()>{that.lastChatData 元素id },500) 进行延后处理就可以了。 问题2&#xff1a;uniapp开…

运动刷步工具2.4。运动步数同步wx和支付宝

时隔一年&#xff1b;上次更新还是2022-11-26&#xff0c;陆续收到私信旧版已不能使用&#xff0c;2.4版本更新修复超时问题。后续有时间还会更新邮箱登录功能2023-11-15更新&#xff1a;修复错误超时问题测试不行的小伙伴&#xff0c;先看说明再使用&#xff01;&#xff01;&…

关于常见分布式组件高可用设计原理的理解和思考

文章目录 1. 数据存储场景和存储策略1.1 镜像模式-小规模数据1.2 分片模式-大规模数据 2. 数据一致性和高可用问题2.1 镜像模式如何保证数据一致性2.2 镜像模式如何保证数据高可用2.2.1 HA模式2.2.2 分布式选主模式 2.3 分片模式如何数据一致性和高可用 3. 大规模数据集群的架构…

垃圾收集算法

垃圾收集算法有如下几种&#xff1a; 分代收集理论&#xff1a;年龄代和老年代选择各自的垃圾收集算法。 复制算法&#xff1a;可达性分析算法找非垃圾对象&#xff0c;然后把非垃圾对象移动到另一端&#xff0c;这一端的垃圾对象清除&#xff0c;该方法浪费内…

如何查找SpringBoot应用中的请求路径(不使用idea)

背景 昨天有个同事向我咨询某个接口的物理表是哪个&#xff0c;由于公司业务较多、这块业务的确不是我负责的&#xff0c;也没有使用idea不能全局搜索(eclipse搜不到jar内的字符串)&#xff0c;也就回复了不清楚。 除了自己写代码输出servlet的路径和类外&#xff0c;发现了一…

【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍

Java技术体系方向-JVM虚拟机参数调优原理 内容简介栈上分配与逃逸分析逃逸分析(Escape Analysis)栈上分配基本思想使用场景线程私有对象 虚拟机内存逻辑图JVM内存分配源码&#xff1a;代码总体逻辑 在某些场景使用栈上分配设置JVM运行参数&#xff1a;开启逃逸模式&#xff0c;…

书生·浦语大模型--第四节课笔记--XTuner大模型单卡低成本微调

文章目录 Finetune简介指令跟随微调增量预训练微调LoRA QLoRA XTuner介绍快速上手 8GB显卡玩转LLM动手实战环节 Finetune简介 增量预训练和指令跟随 通过指令微调获得instructed LLM 指令跟随微调 一问一答的方式进行 对话模板 计算损失 增量预训练微调 不需要问题只…

数据结构:3_栈和队列

栈和队列 一.栈 1. 栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。**进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。**栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#x…

软件测试知识库+1,5款顶级自动化测试工具推荐和使用分析

“工欲善其事必先利其器”&#xff0c;在自动化测试领域&#xff0c;自动化测试工具的核心地位不容置疑的。目前市面上有很多可以支持接口测试的工具&#xff0c;在网上随便一搜就可以出来很多&#xff0c;利用自动化测试工具进行接口测试&#xff0c;可以很好的提高测试效率&a…

GPT4+Python近红外光谱数据分析及机器学习与深度学习建模

详情点击链接&#xff1a;GPT4Python近红外光谱数据分析及机器学习与深度学习建模 第一&#xff1a;GPT4 1、ChatGPT&#xff08;GPT-1、GPT-2、GPT-3、GPT-3.5、GPT-4模型的演变&#xff09; 2、ChatGPT对话初体验 3、GPT-4与GPT-3.5的区别&#xff0c;以及与国内大语言模…

最新AI系统ChatGPT网站H5系统源码,支持Midjourney绘画,GPT语音对话+ChatFile文档对话总结+DALL-E3文生图

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧。已支持GPT…

Django笔记(六):DRF框架

首 前后端分离是互联网应用开发的标准使用方式&#xff0c;让前后端通过接口实现解耦&#xff0c;能够更好的进行开发和维护。 RESTful接口常见规范 在接口设计中&#xff0c;大家遵循一定的规范可以减少很多不必要的麻烦&#xff0c;例如url应有一定辨识度&#xff0c;可以…