浅谈网络通信(1)

文章目录

一、认识一些网络基础概念

1.1、ip地址

使用 ip 地址,来描述网络上一个设备所在的位置。

1.2、端口号

区分一个主机上不同的应用程序。

一个网络程序,在启动时,都需要绑定一个或多个端口号,因为后续的通信都需要依赖端口来展开。

1.3、协议

协议描述了网络通信时传输的数据的含义。

1.4、协议分层

协议就只是表示一种约定,这样的约定可以是任意的,由于网络通信协议是复杂的,因此在经过了多年的发展后,行业标准、专家已经规定出了现成的协议,我们只需要学习好规定出的现成协议即可。

那为什么要约定好这么一份通用的协议呢?

这是由于在网络通信中,电脑种类很多,制作设备的厂商也很多,必须有一份统一的协议标准,让大家都按照同样的标准研发设备,以确保不同的厂商研发出的东西都能够在一起相互通信。

但由于网络通信确实是十分复杂的,故会涉及到一系列非常繁琐、细节的工作,因此仅靠一个协议就解决所有问题,就导致这个协议就会十分庞大、复杂,因此此时就需要对协议进行分层。

上层协议 调用 下层协议,下层协议 给 上层协议提供服务。

例子:

协议也是如此。

1.5、协议分层的2种方式

1.5.1、OSI七层模型

OSI七层模型一般只存在于教科书中。

1.5.2、TCP/IP五层模型[!]

TCP/IP五层模型是如今网络通信最常用的模型,很重要!

1.5.2.1、TCP/IP五层协议各层的含义及功能

第一层:物理层
含义: 负责在物理媒介上发送和接收原始比特流。
功能: 为网络通信提供物理连接,定义了如网卡、网口、网线等物理设备的标准。

第二层:数据链路层
含义: 为相邻节点提供端到端的直接链接服务,以帧的形式传输数据。
功能: 错误检测和修正,管理帧数据。

第三层:网络层
含义: 负责数据在网络中的路由选择和转发,确保数据从源 ip 地址传送到 目的 ip 地址。
功能: 路由选择,数据包的封装与分用。

第四层:传输层
含义 :直接提供端到端的服务,只关注起点和终点,不关注中间的实现过程。
功能: 对端口号进行管理

第五层:应用层
含义: 为应用程序提供网络通信服务。
功能: 允许应用程序与网络交互,实现数据传输、文件共享、电子邮件...等网络通信功能。

举个例子加深对TCP/IP五层模型中各层的理解:

1、物理层 :约定网络通信中的一些基础设施需要遵守的规范,如约定这些信息:网线、网口、网口... 就像快递车行驶在公路上运送快递时,公路、信号灯、公路上的绿化形式...都有一定的制定标准。

2、数据链路层 :相邻节点之间,数据是如何传输的。快递公司运送快递时,确定好快递的运输路线之后,假设此时快递车运送快递的路线是:广东------>柳州------>南宁,那么此时就需要考虑该条路线中的相邻节点之间是怎么走的,如从广东------>柳州,快递是怎么运送的,到底是水运、还是航运...,而从柳州------>南宁,快递是怎么运送的,到底是陆运、还是航运...

3、网络层 :与路径规划有关。j就像当前快递车运送快递的路线是怎么规划的:俗话说,条条大路通罗马,因此运送快递到达目的地其实可以有很多条不同的路线能够到达目的地。譬如说有,广东------>柳州------>南宁、广东------>桂林------>南宁...究竟选择哪条路线运送快递,由网络层负责。

4、传输层:只关注起点和终点,不关注中间过程。譬如网购时,需要告知卖家自己的收件地址,那此时卖家去快递公司寄快递给用户时,就会告知快递公司当前所寄商品是从哪里寄出,要寄往哪里,此时就行了,至于快递公司将此快递从哪里运输到哪里,怎么打包、运输、运输的过程产生的油费、运输的路径规划...都不需要我们关心,是由快递公司负责,我们只需要在规定时间内能收到自己的快递即可。

5、应用层:拿到这个数据之后我们要干什么。即获取到数据之后是要进行文件传输、邮件发送、还是...其他的,是由应用层说了算的。

对于我们程序员来说,我们和传输层、应用层打的交道比较多,因此必须重点掌握传输层、应用层的含义、功能、api、使用。

二、网络中数据传输的基本流程------封装、分用

2.1、封装

以QQ发送消息为例子:主机A发送消息给主机B,介绍网络中传输数据的基本流程。

主机A的情况:

1、应用层:

QQ应用程序,从输入框中获取到我们要传送的数据,根据应用层协议,构造成应用层数据报,应用层含有很多现成的协议,但很多应用程序中会自定义应用层协议,那么QQ自定义的应用层协议是啥样的?咱们不知道,只有开发者才知道,因此此处QQ自定义的应用层协议我们通过假设进行举例子。

假设QQ的应用层协议是这样自定义的:

发件人的QQ号、接收人的QQ号、时间、消息内容

那么此时就需要将自定义好的应用层协议,构造成应用层数据报构造应用层数据报的过程,就是按照一定格式进行字符串拼接

发送方和接收方需要达成一致,发送方使用什么样的应用层协议进行发送数据,接受方就要使用什么样的应用层协议接收数据。

应用程序自定义的好应用层协议后,并且构造好应用层数据报后,就会调用传输层提供的接口,把应用层数据报(携带了应用程序所想发送的消息)交给传输层进行处理

2、传输层:

传输层的现成协议很多,最常用的就是 TCP 协议、UDP 协议,此处假设使用 UDP 协议处理应用层传过来的的数据报。

UDP 协议就会按照自己的协议格式,生成一个 UDP 数据报:该 UDP 数据报,会将应用层数据报作为自己的载荷部分,并且在载荷部分的前面添加一个 UDP 报头。

UDP 协议不关心应用层数据报里含有什么数据,是什么内容,只是把应用层数据报当作一个字符串,再构造出一个传输层数据报。

就像发快递一样,假设你想寄的快递是衣服,你拿到快递站之后,交给快递员,快递员会把你的衣服包装成一个包裹,然后在上面贴上快递单,此时你要寄的衣服(相当于应用层数据报)就已经变了一个样子,变成了一个快递包裹件,上面有快递单(此时这个贴了快递单的包裹,就相当于 UDP 数据报)。只要你邮寄的东西,不管是衣服、还是其他的,只要不违禁,快递是不会关心你要寄什么东西的,他只负责给你包装,然后贴单子(这个单子上面就含有快递的发件人、收件人...这里的发件人、收件人就相当于一个 源端口、目的端口),发出去。

3、网络层:

网络层中也含有众多现成的网络层协议,最主要的协议的是 IP 协议。 IP协议会根据从传输层中收到的 UDP 数据报,构造出 IP 数据报,IP数报中就包含 源IP地址 和 目的IP地址,来确保数据能够从 源IP地址 正确传输到 目的IP 地址。

就像我们发快递一样,在快递上贴的快递单上的发件人地址和收件人地址,就相当于 源IP地址 和 目的IP地址,都是为了确保正确传输。

4、数据链路层:

数据链路层也提供了许多现成的协议,最主要的是 以太网 协议,以太网,又会针对网络层传输来的数据报进行封装,为IP数据报添上帧头、帧尾。

以太网也不关心载荷里是啥,只是把载荷当作字符串,进一步的拼接上帧头、帧尾,构造成以太网数据帧,然后进一步再将以数据链路层数据报发送给物理层。

5、物理层:

物理层是硬件设备(如网卡),硬件设备需要将上述数据进行转换,将拼接好的字符串数据转成二进制数据,通过光信号/电信号/电磁波传输。

此时,将数据进行了多层封装,最终将信号转成了光信号,在网络中进行传输,主机A就完成了发送过程。

2.2、分用

主机B的情况。

1、物理层:

硬件设备(网卡),收到网络方传输来的光信号/电信号/电磁波,那此时就需要通过调制解调器(猫),针对光信号进行调制解调。

调制:把你要传输的信息放到光电信号中。

解调:从光电信号中把信息取出来。

那此时,物理层就通过调制解调器器,将光电信号中的数据取出来,即:以太网的数据帧,这个数据就要被交给上一层,数据链路层。

2、数据链路层

数据链路层的以太网协议,就会针对这个数据进行解析,即:将以太网数据帧的帧头、帧尾去掉,取出其载荷部分,交给上层------网络层。

3、网络层:

IP协议针对这个数据进行解析:即去掉IP报头,取出载荷,进一步交给传输层。

4、传输层:

根据IP报头中的字段,就知道当前这个载荷是一个 UDP 数据报,故将此数据报交给 UDP 协议处理,此时UDP也要针对数据报进行解析,去掉报头。

5、应用层:

UDP报头中,有一个字段------>目的端口,根据目的端口找到关联的应用程序,然后将此应用层数据报交给这个程序即可,该程序会根据自定义的应用层协议解析该数据报,然后将数据显示到界面上。

此时,完成上述一系列分用步骤后,QQ中对应的头像就开始闪烁,点进去,就能显示出这个消息,以及消息的接收时间等信息...

主机A,从上到下,依次添加报头的过程,称为 "封装",主机B,从下到上,依次解析报头的过程,称为 "分用",每次网络传输,都需要经历这个过程。封装就像是在打包快递,分用,就像是在拆快递。

消息转发到某个设备,每个设备的处理流程都是和上面的封装分用是一致的。

如果消息转发到的设备是一个交换机,交换机封装分用到数据链路层即可:交换机解析出以太网数据帧,进一步获取到帧头中的 "mac"地址,根据 "mac"地址查询交换机内部的转发表,确定接下来数据从哪个网口发出去,在发送之前又会把以太网数据帧封装好。

如果消息转发到的设备是一个路由器,路由器封装分用到网络层即可:路由器解析出IP数据报,进一步获取到IP报头中的I目的P地址,根据目的IP地址进一步规划接下来要走的路线,然后在发送之前又会把IP数据报封装好。

2.2.1、5元组

使用 5元组 来描述一次网络通信:1、源 IP 地址 2、目的 IP 地址 3、源端口号 4、目的端口号。5、协议类型。

三、进行网络编程

写一个应用程序,这个应用程序可以使用网络通信,就需要依靠操作系统给传输层协议对外提供的api。传输层主要的协议有:TCP、UDP,这两个协议关于网络通信方面,提供了两套完全不同的 api。

传输层用于网络通信的 api 叫做 socket api。

3.1、UDP

3.1.1、UDP 的特点

1、无连接

2、不可靠传输

3、面向数据报

4、半双工

3.2、TCP

3.2.1、TCP 的特点

1、有连接

2、可靠传输

3、面向字节流

4、全双工

3.3 TCP、UDP特点解析

1、什么叫做 无连接?有连接?

譬如说我们在javase时学过的JDBC编程,JDBC是:先创建一个数据源 DataSource,再通过 DataSource 创建 Connection 连接,那么这就是一种 有连接,该连接是抽象的,连接是用来通信时保存对方信息的。

再譬如说,打电话。当我们打电话时,首先按下拨号键,直到对方接通为止,才算是完成连接,这也是一种有连接的表现。

那么客户端与服务器之间进行通信时,使用 内存(本本)保存对端的信息,双方都保存这个信息,此时 "连接" 就出现了,那么与JDBC的连接、打电话的连接不同的是,一个客户端可以连接多个服务器,一个服务器也可以对应多个客户端。

那么像QQ、微信进行发送消息时,是不需要建立连接的,就能直接发送消息进行通信,这是一种 无连接 的变现。

其实除了连接,还有一种叫做 链接 的情况,链接与连接不同,链接相当于一种快捷方式,即:通过一个文件,让该文件的内容保存另一个文件的路径,实现的链接,一般是软链接(软件的链接)。

2、什么叫 可靠传输?什么叫 不可靠传输?

可靠传输,不是说A给B发消息,100%能到,这个要求太难了,可靠传输,就是说,A给B发消息时,会尽可能地将消息传给B,并且传输失败的时候,A能感知到、或者在传输的时候,A能知道自己是否传输成功!

譬如说,对于应用程序:钉钉、飞书、企业微信...这些应用程序用到的就是可靠传输,即当用户A给用户B发消息时,用户A能够清楚地感知到用户B是否接收到消息,如果消息旁边出现 "已读",那就说明用户B已经收到该消息并且查看了;如果消息旁边什么都没有出现,说明当前消息已经通过网络传输到给了用户B,但是用户B并没有点击查看该消息。

不可靠传输即:就像我们平常所用的QQ、微信...这些软件,当用户A给用户B发送了一条消息,无论用户是否点击查看了该条消息,该消息的是输入框旁边,都不会出现 "已读" 的提示,因此,用户A此时就无法判断当前消息的发送情况。

TCP的特点就是可靠传输,但是可靠传输的前提是TCP牺牲了传输效率,UDP的特点是不可靠传输,因此UDP的传输效率比TCP高,但是UDP并不能保证消息传输过程的可靠性,极易出现丢包、顺序颠倒、数据包重复的情况。

3、什么叫做 面向数据报?面向字节流?

面向数据报、面向字节流 这样的特点是跟代码的编写息息相关的,务必要重视的去掌握该知识点。对于 UDP 来说,其在网络中传输数据的基本单位是 数据报,那么一个UDP数据报的格式,是十分重要的,后续会介绍。而TCP在网络中传输数据的基本单位是 字节,就跟文件操作类似,都是 "流" 式的。譬如说,通过 TCP 读写 100字节数据,可以一次读写 100 字节,也可以分2次读写,分别读写50字节;也可以分10次读写,每次读写10字节。

[易错题] :TCP是可靠传输、UDP是不可靠传输,因此TCP比UDP更安全?这是正确的还是错误的?[错误的],谈到网络安全,指的是:你传输的数据是否容易被黑客截获,以及如果被截获之后是否会泄露一些重要信息,网络安全是和 安全、入侵、加密、反编译...有关,而不是和可靠传输与否有关。

4、什么叫做 全双工?半双工?

全双工:进行网络通信的通道可以双向传递数据。

半双工:进行网络通信的通道只能单向传递数据。

3.4、使用传输层协议 TCP/UDP 提供的 socket api 进行网络通信

3.4.1、UDP------ DatagramSocket、DatagramPacket

UDP提供两个不同的核心类进行网络通信:

一、DatagramSocket:

操作系统是使用 文件 这样的概念,来管理一些软硬件资源,我们使用一些硬件的网络设备进行网络通信时(网卡...),操作系统也是使用 文件 这样的方式管理网卡这样的硬件设备,而我们将表示网卡的这类文件,称为 socket 文件。

Java中的 socket 对象,就对应系统里的 socket 文件,最终落到网卡这样用于进行网络通信的硬件设备。

还有一些其他的硬件设备,譬如说:键盘、鼠标、显示器等硬件设备插入电脑上使用时,操作系统也是将这些硬件设备当作 一个一个的 文件 管理起来。

因此,如果我们程序员写的应用程序,想要进行网络通信,就必须在代码中先有 socket 对象。

那么,上述的 DatagramSocket 就是一个 socket 对象。

DatagramSocket 的构造方法:

1、DatagramSocket()

此构造方法是在客户端处使用,客户端想要进行网络通信,也是需要先有一个 socket 对象。客户端使用哪个端口,不需要客户端自己指定,而是由系统自动分配,这是因为客户端大部分是计算机小白,他们自己指定端口号,极易发生端口冲突,造成网络通信失败。

2、DatagramSocket(int port)

此构造方法是在服务器端使用的,服务器端需要自己指定端口号,以便客户端能够找到当前服务器来进行网络通信。那为什么服务器端又能够进行手动指定端口号??此时不怕端口冲突了??这是因为对于服务器端来说,服务器端是由程序员掌控的,因此此时程序员对于服务器上的应用程序的端口号分配情况了如指掌,因此此时指定的端口号是经过考察才分配的,此时就不会造成端口冲突,以至于网络通信失败这样的情况发生了。

当客户端给服务器发送消息时,对于客户端来说,客户端的端口号是源端口号,服务器的端口号是目的端口号;当服务器给客户端发消息时,对于服务器来说,服务器的端口号是源端口号,客户端的端口号是目的端口号。

举个例子理解端口号在网络通信中的重要作用:

有一天,我去城中街道58号餐厅吃饭。那么这个地址:城中街道58号 就唯一标识了一条街道上的某一家餐厅,因为一个城市,会有许多条街道,一条街道上,会有许许多多的餐馆(那么对于一个服务器端来说,一个服务器上,也会有许多应用程序)。此时的 城中街道 就相当于服务器的 IP地址,58号 就相当于 服务器的端口号,而慕名而来吃饭的客人,就相当于客户端,假如我第一次到这个餐厅里,坐的位置是10号桌,那么下一次来这个餐厅,坐的还是10号桌吗??不一定了,这个位置是随机分配的。因此此时 10号 桌 就相当于 客户端的端口号,不需要客户端自己指定,而是由系统自动分配即可。

3、DatagramSocket 中提供的用于网络通信的方法:

1)、void receive(DatagramPacket p)

使用该方法读取请求并解析,该方法的参数是一个 数据报,**因为 UDP 协议在网络传输数据的基本单位的 数据报。

2)、void send( DatagramPacket p)

使用该方法发送一个数据报,该方法的参数也是一个 数据报。

3)、void close()

通过该方法,关闭 socket 文件,防止出现 文件资源泄露 问题。

二、DatagramPacket

DatagramPacket 表示一个 数据报。

DatagramPacket 的构造方法:

1、DatagramPacket(byte[] buf,int length)

该构造方法代表了系统中设定的 UDP 数据报的二进制结构。该构造方法使用 byte数组来接受数据,因为 DatagramPacket 作为 UDP 数据报,必然要能够承载一些数据,我们需要通过手动指定 byte[] 作为数据存储的空间。

2、DatagramPacket(byte[] buf,intt length,SocketAddress address)

该构造方法一般用于发送数据,参数有 byte 数组,用来为发送的数据作承载空间,length表示数据的实际长度,address表示IP地址以及端口号。

3.4.1.1 使用UDP协议编写回显服务器

回显服务器:客户端发什么、服务器就返回什么。

服务器端代码:

java 复制代码
package UDP.network;


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;

//回显服务器
public class UDPEchoServer {
    private DatagramSocket socket = null;

    ExecutorService executorService = null;
//    参数是:服务器的端口号,客户端通过这个服务器的端口号与服务器进行网络通信
    public UDPEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }



//    启动服务器
    public void start() throws IOException {
//        通过 while 循环反复处理 不知道何时与服务器进行通信的 众多客户端
        System.out.println("[UDPEchoServer] 服务器启动!");
        while (true){
                    //            服务器做的3步:
//            1、读取请求并解析
                    DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
                    try {
                        socket.receive(requestPacket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
//            通过 getData() 获取数据报中的数据
                    String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//            2、根据请求计算出响应   通过 process() 方法,完成根据请求计算出响应,这一步是最复杂最关键的,不同的服务器不同的功能,就是这一步不同
                    String response = process(request);
//            3、返回响应   requestPacket.getSocketAddress():通过这个获取到客户端的ip地址以及端口号,这样就可以将响应返回给特定的客户端
                    DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
                    try {
                        socket.send(responsePacket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
//            打印日志信息
                    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(8909);
        server.start();
    }
}

客户端代码:

java 复制代码
package UDP.network;

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

//客户端
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;
        socket = new DatagramSocket();
    }


    public void start() throws IOException {
        System.out.println("[UDPEchoClient] 客户端启动!");
        while (true){
//            从控制台输入用户信息
            System.out.println("请输入信息:-> ");
            Scanner sc = new Scanner(System.in);
            String request = sc.next();

//            再将用户输入的信息构造成一个UDP数据报,发送给服务器处理
            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 {
        UDPEchoClient client = new UDPEchoClient("127.0.0.1",8909);
//        UDPEchoClient client = new UDPEchoClient("42.192.83.143",9090);
        client.start();
    }
}

端口号,是用来区分主机上不同的应用程序,**一个应用程序可以占据主机上的多个端口号,一个端口只能被一个进程占有(**其实是有特例的,但是一般情况下是在这样的)。如果应用程序在网络通信中出现通信异常:SocketException ,一般都是因为当前端口号已经被别的进程占用,此时再尝试创建这个 socket 对象,占用该端口,就会报错------编写网络通信代码时,此类异常会很常见!

解析使用UDP编写回显服务器代码:

在服务器端的 start() 方法中,使用 while循环来等待并循环处理客户端的请求,因为一个服务器需要给很多的客户端提供服务,服务器不知道客户端什么时候来,服务器只能 "时刻准备着"处理客户端的请求。

而 socket.receive() 方法中,参数是DatagramPacket,是一个 "输出型参数",传入 receive 的是一个空的 DatagramPacket 对象,receive() 内部会将这个空的 DatagramPacket 对象的内容填充上,当 receive() 执行结束之后,就会得到一个 装满内容的 DatagramPacket。

对于 response.getBytes().length 和 response.length()来说,这两者有什么区别??这很重要,一定不要弄错。如果当 response 里都是英文字母时,这两者获取到的长度是一样的,但是如果说 response 里是2个汉字时,此时这两者获取到的长度就会有差别,一个汉字是3个字节,response.length()此时获取到的长度是2,而 response.getBytes().length 获取到的长度是 6。 Socket api 本身,是按照字节来处理数据的。

对于 requestPacket.getSocketAddress() 来说,是用来获取IP地址和端口号的,DatagramPacket 这个对象里就包含了通信双方的 IP地址和端口号。

socket对象 就相当于一个 socket 文件,因此 socket 对象用完后,就需要及时关闭,以免造成文件资源泄露问题,导致程序异常。那么此时 socket = new DatagramSocket(port); 来说,是否需要关闭呢??

对于我们当前编写的这个服务器程序来说,DatagramSocket 不关闭,也是可以的。因为整个程序中,只有这么一个 socket 对象,且不是在频繁创建的情况下。这个 socket 对象生命周期十分长,伴随整个程序,此时,socket 就需要保持打开的状态。

socket 对象 ------>系统中的 socket 文件------>文件描述符(文件描述符即表示当前进程所占有的文件,进程每打开一个文件,就会生成一个文件描述符)(关闭 socket 对象的主要原因,就是因为要释放文件描述符),但是因为我们此时程序中只有一个 socket 对象,且生命周期伴随整个程序,此时无需手动关闭,只需要等待进程结束后,系统将PCB回收后,PCB里的文件描述符表字段也就被销毁了。

仅限于,只有一个 socket 对象,并且生命周期是跟随整个进程。如果是含有多个 socket 对象,并且 socket 对象的生命周期短,需要频繁创建、释放,就必须去关闭 socket!

...

相关推荐
逊嘘15 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
Half-up18 分钟前
C语言心型代码解析
c语言·开发语言
Source.Liu39 分钟前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng40 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马42 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
Jacob程序员1 小时前
java导出word文件(手绘)
java·开发语言·word
小白学大数据1 小时前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin
VBA63371 小时前
VBA之Word应用第三章第三节:打开文档,并将文档分配给变量
开发语言
半盏茶香1 小时前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏