[javaWeb]Socket网络编程

网络编程:写一个应用程序,让这个程序可以使用网络通信。这里就需要调用传输层提供的 api。

Socket套接字

传输层提供协议,主要是两个: UDP和TCP
提供了两套不同的 api,这api也叫做socket api。

UDP和 TCP 特点对比:

  • UDP: 无连接,不可靠传输,面向数据报,全双工
  • TCP : 有连接,可靠传输,面向字节流,全双工

有连接,无连接:客户端和服务器之间,彼此之间使用内存空间保存对端的信息,双方都保存这个信息此时“连接”就出现了。

可靠传输,不可靠传输:不是说,A 给 B 发的消息 100% 能到(这个要求太难了)。A 尽可能的把消息传给 B.并且在传输失败的时候,A 能感知到,**或者在传输成功的时候,也能知道自己传输成了。**可靠传输效率低,不可靠传输效率高。

面向字节流,面向数据报:

  1. TCP 和 文件操作类似,都是“流”式的(由于这里传输的单位是字节,称为字节流)
  2. UDP 是面向数据报,读写的基本单位是一个 UDP 数据报(包含了一系列的数据/属性)

全双工,半双工:

  1. 全双工:一个通道,可以双向通信
  2. 半双工:一个通道,只能单向通信

UDP数据报套接字编程

DatagramSocket API

DatagramSocket API 是一个 Socket 对象。操作系统,使用文件这样的概念,来管理一些软硬件资源,操作系统也是使用文件的方式来管理网卡的。表示网卡的这类文件,称为 Socket 文件。Java 中的 socket 对象,就对应这系统里的 socket 文件。

构造方法:

方法签名方法说明
DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定的端口**(一般用 于服务端)**

客户端使用哪个端口,系统自动分配。但是服务器使用哪个端口,是手动指定的。对于服务器来说,需要有一个固定的端口号,方便其他客户端找到。一个客户端的主机,上面运行的程序很多,天知道你手动指定的端口是不是被别的程序占用了。让系统自动分配一个端口是更明智的选择。服务器是完全在程序猿手里控制的。程序猿可以把服务器上的多个程序安排好,让他们使用不同的端口。

普通方法:

方法签名方法说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻 塞等待)
void send(DatagramPacket p)从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()关闭此数据报套接字

DatagramPacket API

表示了一个 UDP 数据报,代表了系统中设定的 UDP 数据报的二进制结构。

构造方法:

方法签名方法说明
DatagramPacket(byte[] buf, int length)构造一个DatagramPacket以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个DatagramPacket以用来发送数据报,发送的数据为字节 数组(第一个参数buf)中,从0到指定长度(第二个参数 length)。address指定目的主机的IP和端口号

普通方法:

方法签名方法说明
SocketAddress getSocketAddress()SocketAddress对象包含了IP地址和端口号
InetAddress getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据。udp 数据报载荷部分(完整的应用层数据报)

示例一:回显服务器(echo server).

回显服务器(echo server):客户端发啥,服务器返回啥.

// UDP 的 回显服务器.
// 客户端发的请求是啥, 服务器返回的响应就是啥.
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) {
            // 反复的, 长期的执行针对客户端请求处理的逻辑.
            // 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节.
            // 1. 读取请求, 并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //    这样的转字符串的前提是, 后续客户端发的数据就是一个文本的字符串.
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            // 2. 根据请求, 计算出响应
            String response = process(request);
            // 3. 把响应写回给客户端
            //    此时需要告知网卡, 要发的内容是啥, 要发给谁.
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            // 记录日志, 方便观察程序执行效果.
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
                    request, response);
        }
    }

    // 根据请求计算响应. 由于是回显程序, 响应内容和请求完全一样.
    public String process(String request) {
        return request;
    }

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

客户端代码:

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    // 服务器的 ip 和 服务器的端口.
    public UdpEchoClient(String ip, int port) throws SocketException {
        serverIp = ip;
        serverPort = port;
        // 这个 new 操作, 就不再指定端口了. 让系统自动分配一个空闲端口.
        socket = new DatagramSocket();
    }

    // 让这个客户端反复的从控制台读取用户输入的内容. 把这个内容构造成 UDP 请求, 发给服务器. 再读取服务器返回的 UDP 响应
    // 最终再显示在客户端的屏幕上.
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("客户端启动!");
        while (true) {
            // 1. 从控制台读取用户输入的内容
            System.out.print("-> "); // 命令提示符, 提示用户要输入字符串.
            String request = scanner.next();
            // 2. 构造请求对象, 并发给服务器.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            // 3. 读取服务器的响应, 并解析出响应内容.
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4. 显示到屏幕上.
            System.out.println(response);
        }
    }

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

可以启动多个客户端,服务器也是可以应对的。IDEA 上启动多个程序,需要稍微设置一下:

image-20230828095652845

其他人的电脑不能访问我这个程序。原因是:网络环境的现状,NAT 机制是主流。NAT 机制下,就把 IP 地址分成了外网 IP 和内网 IP.(内网IP 不能直接访问)。

示例二:翻译服务器

翻译服务器:请求是一些英文单词,响应则是对应的中文翻译。代码很多和回显服务器相同,所以继承UdpEchoServer 来实现代码的复用。

public class UdpDictServer extends UdpEchoServer {
    private Map<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("hello", "你好");
        // 可以在这里继续添加千千万万个单词. 使每个单词都有一个对应的翻译.
    }

    // 是要复用之前的代码, 但是又要做出调整.
    @Override
    public String process(String request) {
        // 把请求对应单词的翻译, 给返回回去.
        return dict.getOrDefault(request, "该词没有查询到!");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        // start 不需要重新再写一遍了. 直接就复用了之前的 start !
        server.start();
    }
}

TCP流套接字编程

TCP 分量要比 UDP 更重用的,用的更多的协议。

TCP 提供的 api 也是主要有两个类

  • ServerSoeket:给服务器使用的 socket
  • Socket: 既会给服务器使用,也会给客户端使用

字节流,一个字节一个字节进行传输的。一个 tcp 数据报,就是一个 字节数组 byte[]。

ServerSocket API

ServerSocket 构造方法:

方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket 方法:

方法签 名方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

Socket API

Socket 构造方法:

方法签名方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接

Socket 方法:

方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

ServerSocket的accept()方法负责产生连接,产生连接的类型是Socket。Socket负责后续的连接后的通信。 ServerSocket的accept()是在服务器中使用,当服务器执行到 accept 的时候, 此时客户端可能还没来呢。accept 就会阻塞到有客户端连接成功为止。

accept()把内核中的连接获取到应用程序中了,这个过程类似于,"生产者消费者模型”。

image-20230829100509766

TCP版本的回显服务器

客户端代码,要做的事情:

  1. 读取请求并解析.
  2. 根据请求计算响应
  3. 把响应写回给客户端.
public class TcpEchoServer {

    private ServerSocket serverSocket = null;

    // 这个操作就会绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    // 启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }
    // 通过这个方法来处理一个连接的逻辑.
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d]客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 接下来就可以读取请求, 根据请求计算响应, 返回响应三步走了.
        // Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            // 一次连接中, 可能会涉及到多次请求/响应
            while (true){
                //1. 读取请求并解析. 为了读取方便, 直接使用 Scanner.
                Scanner sc = new Scanner(inputStream);
                if(!sc.hasNext()){
                    //读取完毕,客户端下线
                    System.out.printf("[%s:%d]客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                // 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
                String request = sc.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回给客户端. 把 OutputStream 使用 PrinterWriter 包裹一下, 方便进行发数据.
                PrintWriter writer = new PrintWriter(outputStream);
                //    使用 PrintWriter 的 println 方法, 把响应返回给客户端.
                //    此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
                writer.println(response);
                //    这里还需要加一个 "刷新缓冲区" 操作.
                writer.flush();
                // 日志, 打印当前的请求详情.
                System.out.printf("[%s:%d] req: %s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request,response);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }

    }

    private String process(String request) {
        return request;
    }

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

客户端代码,要做的事情:
1.从控制台读取用户的输入
2.把输入的内容构造成请求并发送给服务器
3.从服务器读取响应
4.把响应显示到控制台上

public class TcpEchoClient {
    private Socket socket = null;

    // 要和服务器通信, 就需要先知道, 服务器所在的位置.
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 这个 new 操作完成之后, 就完成了 tcp 连接的建立.
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动");

        Scanner scannerConsole = new Scanner(System.in);

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 从控制台输入字符串.
                System.out.print("-> ");
                String request = scannerConsole.next();
                // 2. 把请求发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                //    使用 println 带上换行. 后续服务器读取请求, 就可以使用 scanner.next 来获取了
                printWriter.println(request);
                //    不要忘记 flush, 确保数据是真的发送出去了!!
                printWriter.flush();
                // 3. 从服务器读取响应.
                Scanner scannerNetwork = new Scanner(inputStream);
                String response = scannerNetwork.next();
                // 4. 把响应打印出来
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

多线程版本的会先服务器

当前代码还存在一个很大的问题。多个客户端访问会出现bug。当第一个客户端连接好了之后,第二个客户端,不能正确被处理服务器。看不到客户端上线,同时客户端发来的请求也无法被处理。当第一个客户端退出之后,之前第二个客户端发的请求,就能正确响应了。

上面的单线程代码中,serverSocket.accept(),建立一个连接,但是在进入processConnection()后,连接不断开,下一个连接就没办法连接上。主线程一个线程,又要建立连接,又要进行通信,只有通信结束连接断开之后,才会有下一个连接。要想解决这个问题我们就需要用多线程来解决,主线程负责建立连接,新建立的线程负责连接后的通信,各司其职就能解决这个问题。

public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    // 启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = serverSocket.accept();
            Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
        }
    }

经过上述改进,只要服务器系统资源足够,有几个客户端都是可以的了。

TCP 程序的时候,涉及到两种写法

  1. 一个连接中只传输一次请求和响应.短连接

  2. 一个连接中,可以传输多次请求和响应.长连接

上面代码属于长连接。其实如果我们刚才的代码,不写成这个样子。比如要求每个客户端只能发一次请求,发完就断开。上述情况能得到一定的缓解,但是还是有类似的问题的。处理多个消息,自然就会延长 processConnection 的执行时间,后续的连接也得等到上个连接断开后才能继续连接。

现在是有一个连接,就有一个新的线程。如果很多客户端,频繁的来连接/断开,服务器就涉及到频繁创建/释放线程了。我们使用线程池会比较好的解决这个问题。

    // 此处不应该创建固定线程数目的线程池.
    private ExecutorService service = Executors.newCachedThreadPool();
        // 启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = serverSocket.accept();
            // 使用线程池, 来解决上述问题
            service.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

虽然使用线程池,避免了频繁创建销毁线程。但是毕竟是每个客户端对应一个线程。如果服务器对应的客户端很多,服务器就需要创建出大量的线程.对于服务器的开销是很大的。当客户端进一步增加,线程数目进一步增加系统的负担就越来越重,响应速度也会大打折扣。

< C10K问题:同一时刻,有 10k 个客户端(1w 个客户端),通过前面的一些技术手段,硬件设备,是可以处理好.(线程池之类的)
随着互联网的发展,客户端越来越多,请求也越来越多,进而引发了C10M问题。
C10M: 同一时刻,有 1kw 的客户端并发请求。引入了很多技术手段.其中一个非常 有效/必要 的手段,IO 多路复用/IO 多路转接.
IO 多路复用:一个线程完成多个IO操作。 不是说使用了这一个方法就能解决C10M问题,但是解决 高并发(C10M) 重要手之一。

解决高并发,就是四个字:

  1. 开源: 引入更多的硬件资源 (多核CPU)
  2. 节流: 提高单位硬件资源能够处理的请求数。同样的请求数,消耗的硬件资源更少。

IO 多路复用,就是节流的方式。同样的请求,消耗的硬件资源更少了。本质上就是减少线程的数量。操作系统提供的另一组网络编程的 API 可以和 tcp udp api 配合使用的。可以去搜索 Java NIO 一些关键词来学习IO多路复用。

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

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

相关文章

【Redis从头学-13】Redis哨兵模式解析以及搭建指南

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;啥技术都喜欢捣鼓捣鼓&#xff0c;喜欢分享技术、经验、生活。 &#x1f60e;人生感悟&#xff1a;尝尽人生百味&#xff0c;方知世间冷暖。 &#x1f4d6;所属专栏&#xff1a;Re…

安防监控/视频汇聚平台EasyCVR调用rtsp地址返回的IP不正确是什么原因?

安防监控/云存储/磁盘阵列存储/视频汇聚平台EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RT…

QML Book 学习基础4(状态和转换)

目录 states&#xff08;状态&#xff09; Transition&#xff08;过渡&#xff09; states&#xff08;状态&#xff09; 用户界面的某些部分可以用状态来描述。状态定义一组属性更改&#xff0c;并且可以由特定条件触发。 QML 中定义状态&#xff0c;该元素需要绑定到任何项…

JavaScript中的事件委托(event delegation)

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ JavaScript事件委托⭐ 事件冒泡&#xff08;Event Bubbling&#xff09;⭐ 事件委托的优点⭐ 如何使用事件委托⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启…

RuntimeError: scatter_cpu_(): Expected self.dtype to be equal to src.dtype

1. 问题描述 如下图&#xff0c;输入scatter_时报错&#xff01; 2. 报错原因 查阅资料发现是因为要填充的value与要被填充的tensor类型不同&#xff01;如下图 3. 解决办法 将其转换成一样的类型即可&#xff0c;如下图&#xff0c;测试没有报错&#xff1a;

深度学习怎么学?

推荐这本小白看的《深度学习&#xff1a;从基础到实践&#xff08;上下册&#xff09;》。 深度学习&#xff1a;从基础到实践&#xff08;上下册&#xff09; 深入浅出的讲述了深度学习的基本概念与理论知识&#xff0c;不涉及复杂的数学内容&#xff0c;零基础小白也能轻松掌…

为什么删除Windows 11上的Bloatware可以帮助加快你的电脑速度

如果你感觉你的电脑迟钝&#xff0c;彻底清除软件会有所帮助&#xff0c;而且这个过程对Windows用户来说越来越容易。 微软正在使删除以前难以删除的其他预装Windows应用程序成为可能。专家表示&#xff0c;这项新功能可能会改变用户的游戏规则。 科技公司Infatica的主管Vlad…

三个视角解读ChatGPT在教学创新中的应用

第一&#xff0c;我们正处于一个学生使用ChatGPT等AI工具完成作业的时代&#xff0c;传统的教育方法需要适应变化。 教育工作者不应该因为学生利用了先进技术而惩罚他们&#xff0c;相反&#xff0c;应该专注于让学生去挑战超越AI能力范围的任务。这需要我们重新思考教育策略和…

开源vue动态表单组件

一、项目简介 vueelement的动态表单组件&#xff0c;拖拽组件到面板即可实现一个表单 二、实现功能 支持拖拽 支持输入框 支持文本框 支持数字输入框 支持下拉选择器 支持多选框 支持日期控件 支持开关 支持动态表格 支持上传图片 支持上传文件 支持标签 支持ht…

数据可视化工具中的显眼包:奥威BI自带方案上阵

根据经验来看&#xff0c;BI数据可视化分析项目是由BI数据可视化工具和数据分析方案两大部分共同组成&#xff0c;且大多数时候方案都需从零开始&#xff0c;反复调整&#xff0c;会耗费大量时间精力成本。而奥威BI数据可视化工具别具匠心&#xff0c;将17年经验凝聚成标准化、…

零撸大肉,赛博尔Seppol游戏,无限制闯关打碎片,装备,直接变现项目。

2023年7月10日&#xff0c;在上海外滩酒店—— 由来自硅谷、华尔街的技术先锋&#xff0c;与中国科技翘楚阿里、腾讯的骨干团队联手呈现&#xff0c;区块链元宇宙游戏塞波尔 Seppol于上海精彩亮相路演。 1&#xff0c;栖息之地&#xff0c;宠物可放入栖息之地进行挖矿&#xf…

高效利用隧道代理实现无阻塞数据采集

在当今信息时代&#xff0c;大量的有价值数据分散于各个网站和平台。然而&#xff0c;许多网站对爬虫程序进行限制或封禁&#xff0c;使得传统方式下的数据采集变得困难重重。本文将向您介绍如何通过使用隧道代理来解决这一问题&#xff0c;并帮助您成为一名高效、顺畅的数据采…

Spring 如何解决循环依赖问题 - 三级缓存

1. 什么是循环依赖问题 ? 循环依赖问题是指对象与对象之间存在相互依赖关系&#xff0c;而且形成了一个闭环&#xff0c;导致两个或多个对象都无法准确的完成对象的创建和初始化。 两个对象间的循环依赖&#xff1a; 多个对象间的循环依赖 &#xff1a; 解决 Spring 中的循环…

信看课堂笔记—LDO和DC-DC电路打PK

LDO&#xff08;low dropout voltage regulator&#xff0c;低压差线性稳压器&#xff09;和DC-DC(Direct current-Direct current converter&#xff0c;直流电压转直流电压转换器)电源是非常常见的电源电路&#xff0c;LDO 出来的比较早&#xff0c;像老戏骨一样&#xff0c;…

接口优化通用方案

目录 批量异步、回调缓存预取池化并行锁粒度索引大事务海量数据 批量 批量思想&#xff1a;批量操作数据库 优化前&#xff1a; //for循环单笔入库 for(TransDetail detail:transDetailList){ insert(detail); } 优化后&#xff1a; batchInsert(transDetailList); 异步、回…

模块化与组件化:开发中的双剑合璧

引言&#xff1a;模块化与组件化的重要性 在现代软件开发中&#xff0c;随着项目规模的增长和技术的复杂性增加&#xff0c;如何有效地组织和管理代码变得越来越重要。模块化与组件化作为两种主要的代码组织方法&#xff0c;为开发者提供了有效的工具&#xff0c;帮助他们创建…

数学建模:层次分析法

&#x1f506; 文章首发于我的个人博客&#xff1a;欢迎大佬们来逛逛 层次分析法 步骤描述 将问题条理化&#xff0c;层次化&#xff0c;构建出一个有层次的结构模型。层次分为三类&#xff1a;目标层&#xff0c;准则&#xff08;指标&#xff09;层&#xff0c;方案层。比…

若依 vue中el-radio无法默认选中

网上看了很多方法都不管用, 即便是element官方示例方法也不行 解决方法: html <el-form-item label"是否公开" prop"isOpen"><el-radio-group v-model"form.isOpen"><el-radio :label"0">不公开</el-radio>…

攻防世界-What-is-this

原题 解题思路 解压后文件 没有后缀&#xff0c;不知道是什么文件。用notepad打开找不到flag。 尝试当成压缩包解压。 用stegsolve以打开图片1&#xff0c; 合成两张图片。