网络编程套接字

一,socket套接字

1.1 概念

socket 英文原意"插槽",最早是电脑上的一种接口

在操作系统中,给应用程序提供的一组网络编程的API称为 socket api

socket api ,操作系统给应用程序提供的函数,而应用程序---> 应用层 --> 和应用层交互的是传输层 ---> socket api都是传输层提供的

操作系统中,也把socket 理解成一种"特殊的文件",代指网卡(所以在socket中写数据,相当于往网卡中写,从socket中读数据相当于往网卡中读)

1.2 分类

1.2.1 流套接字

流套接字使用传输层TCP协议

TCP,即TransmissionControlProtocol(传输控制协议),传输层协议。

TCP的特点:有连接,可靠传输,面向字节流,全双工,有接收缓冲区,也有发送缓冲区,大小不限

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。

TCP中,在正式通信之前,把对方的信息获取到,保存下来

代码中,先调用"建立连接"的函数,然后再调用"发送数据"的函数

1.2.2 数据报套接字

数据报套接字使用传输层UDP协议

UDP,即UserDatagramProtocol(用户数据报协议),传输层协议。

UDP的特点:无连接 ,不可靠传输 ,面向数据报,有接收缓冲区,无发送缓冲区,大小受限,⼀次最多传输64k

对于数据报来说,可以简单的理解为,传输数据是⼀块⼀块的,发送⼀块数据假如100个字节,必须一次发送,接收也必须⼀次接收100个字节,而不能分100次,每次接收1个字节。

UDP则是不会保存对方的信息,每次调用"发送"接口的时候,现场传递这个信息

在代码中,直接调用"发送数据"的函数

1.2.3 原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据

1.3 TCP和UDP的区别

1.3.1 关于连接

而所谓的"连接",本质上就是通信双方,记录了对方的信息

AB两个主机,A记录了B的++关键信息++(IP和端口号),回头A看到,自己要和B通信

B也记录了A的关键信息,回头B看,也知道自己要和A通信

1.3.2 可靠传输和不可靠传输

不可靠传输:把数据发出去之后,就不管了

可靠传输:把数据发出去之后,关心一下对方是否收到了数据(并不能保证百分之百到达对方,不过可以知道对方是否收到了,如果没收到,TCP自动进行补救措施)

1.3.3 面向字节流和面向数据报

面向字节流:TCP的特点和文件比较类似,比如想发送100个字节的内容。可以一次性全发送,也可以分十次,一次发送10字节,也可以分20次发送,一次发5个字节等等多种情况

面向数据报:UDP传输的时候,以"数据报"为单位,进行传输,一次必须是发送/接受一个UDP数据报(数据报即由若干个字节构成的结构体)

1.3.4 全双工

全双工:同时进行双向通信

半双工:同一时刻,只能单向通信

二,UDP数据报套接字编程

2.1 API介绍

2.1.1 DatagramSocket

datagram:数据报

DatagramSocket是UDP Socket,用于发送和传输UDP数据报

创建一个这样的对象,就相当于在系统内核里创建了一个socket文件,创建socket文件,也会占用文件描述符表的一个位置,用完之后要关闭,否则可能会出现"文件资源泄露"

DatagramSocket构造方法

|--------------------------|----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建⼀个UDP数据报套接字的Socket,绑定到本机任 意⼀个随机端⼝(⼀般⽤于客⼾端) |
| DatagramSocket(int port) | 创建⼀个UDP数据报套接字的Socket,绑定到本机指 定的端⼝(⼀般⽤于服务端) |

DatagramSocket方法

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

其中,DatagramPacket表示一个UDP数据报,是UDP进行网络通信传输的基本单位

2.1.2 DatagramPacket

DatagramPacket构造方法

|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte\[\] buf, int length) | 构造⼀个DatagramPacket以⽤来接收数据报,接收的数据保存在字节数组(第⼀个参数buf)中,接收指定⻓度(第⼆个参数length) |
| DatagramPacket(byte\[\] buf, int offset, int length, SocketAddress address) | 构造⼀个DatagramPacket以⽤来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定⻓度(第⼆个参数length)。address指定⽬的主机的IP和端⼝号 |

DatagramPacket方法

|--------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发 送的数据报中,获取接收端主机IP地址 |
| int getPort() | 从接收的数据报中,获取发送端主机的端⼝号;或从 发送的数据报中,获取接收端主机端⼝号 |
| byte\[\]getData() | 获取数据报中的数据 |

2.2 代码实例

编写一个最简单的 udp 客户端服务器程序

udp客户端给udp服务器发送一个字符串(用户控制台输入)

udp服务器收到之后,把这个字符串,原封不动的返回给客户端

形如这样的客户端 -> 服务器,然后服务器 -> 客户端,即回显服务器(echo server)

服务器

java 复制代码
public class UdpEchoServer {
    //首先先创建一个socket对象
    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[1024],1024);
            socket.receive(requestPacket);
            //为了处理方便,把里面的载荷数据获取出来,构造成一个String
            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(),responsePacket.getPort(),request,response);
        }
    }

    //此处是回显服务器,响应就和请求完全一样
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        // 端口号是一个整数, 我们可以随意指定.
        // 端口范围是 0-65535, < 1024 的端口一般也不使用
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }

}

其中,SocketException也包含在IOException中,最典型的形况就是端口号冲突,从而导致的异常

在这个代码中,通过端口号来区分应用程序

端口号是网络通信中的重要的概念,客户端想要访问服务器,就需要知道服务器的IP(IP比较容易确定,即在哪个机器上启动的服务器)和服务器的端口号(服务器启动的时候,由程序员指定的)

此处收到的UDP数据报,就会把载荷的内容解析出来后放到参数中的字节数组中

以上这段代码是服务器代码,是被动的一方,当服务器运行到receive的时候,客户端可能已经发了请求,也可能没法请求

如果客户端没有发数据,此时receive就会阻塞,一直等到客户端有数据发过来

这个代码是字节数组转换成字符串

这个代码是字符串转换成字节数组

UDP是无连接的,服务器的UDP协议中不保存客户端的信息(客户端IP和端口)

所以这样的代码并不知道数据要发给谁,需要在send的时候,通过参数来制定,这次要发给谁(数据从哪来的就发回到哪里去)

客户端

java 复制代码
package network.udp;

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

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

    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

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

        // 客户端我们也想要让它持续运行.
        // 每循环一次, 就从控制台读取到一个输入的数据, 把这个数据发送到服务器, 读取服务器的响应.
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 从控制台读取用户输入
            System.out.print("-> ");
            String request = scanner.next();

            // 2. 构造请求, 发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), this.serverPort);
            socket.send(requestPacket);

            // 3. 读取服务器的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

            // 4. 打印出 response
            System.out.println(response);
        }
    }

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

在客户端构造socket的时候,没有指定端口号,但是这不意味着没有端口号,端口号是一定要有的,是由操作系统分配的一个空闲的端口

服务器必须要指定固定的端口 以确保客户端随时都能够找到服务器

但是客户端必须不指定固定端口,让系统随机分配空闲的端口,避免端口号出现冲突

英译中服务器

再写一个英译中服务器,请求发来英文单词,响应对应的中文翻译

需要先让服务器知道,英文对应的中文是什么

java 复制代码
public class UdpDictServer extends UdpEchoServer{
    private Map<String,String> dict = new HashMap<>();

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

        dict.put("hello","你好");
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("pig", "小猪");
    }

    //重写process
    public String process(String request){
        //英文--中文
        return dict.getOrDefault(request,"[没有找到该单词]");
    }

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

其中,main方法中的start调用的是父类中的start

而父类的start中的

这个process即this.process()

this类型看起来是父类的类型,实际上指的是子类的实例

三,TCP流套接字编程

TCP是面向字节流的,所以TCP中数据传输的基本单位是字节,用byte,byte\[\]即可

3.1 API介绍

3.1.1 ServerSocket

ServerSocket是创建TCP服务端Socket的API

ServerSocket构造方法:

服务器启动的时候,指定具体的端口号

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

ServerSocket方法:

accept是ServerSocket最关键的方法,用于辅助完成"连接"的建立

TCP是有连接的,TCP的连接建立流程是操作系统内核完成的

通过accept把内核里建立好的连接,拿到应用程序中然后才能通信

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

调用close是为了释放资源(文件描述符表)防止文件资源泄露,确定不再使用了才能释放

而对于UDP的服务器/客户端来说,得是进程结束才会不使用,但是进程结束时系统就自动释放了,所以不需要手动调用close

3.1.2 Socket

Socket是客户端socket,或服务器中接收到客户端建立连接(accept方法)的请求之后,返回的服务器socket

不管是客户端还是服务端socket,都是双方建立连接之后,保存的对端信息,及用来与对方发数据的

Socket构造方法:

|-------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建⼀个客⼾端流套接字Socket,并与对应IP的主机 上,对应端⼝的进程建⽴连接 |

Socket方法:

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

3.2 代码实例

服务器

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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("服务器启动...");

        ExecutorService service = Executors.newCachedThreadPool();

        while (true) {
            // TCP 每次循环是要处理一个连接.
            // 一个连接里, 可能会有多组请求和响应.
            Socket socket = serverSocket.accept();

            // 之前是直接调用, 会存在问题
            // processConnection(socket);

            // 现在改成创建新的线程, 由线程调用 processConnection
//            Thread t = new Thread(() -> {
//                processConnection(socket);
//            });
//            t.start();

            // 还可以使用线程池的方式, 进一步减少频繁创建销毁线程的开销.
            service.submit(() -> {
                processConnection(socket);
            });
        }
    }

    private void processConnection(Socket socket) {
        // 一个连接中, 可能会涉及到多组请求/响应的交互
        System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress().toString(), socket.getPort());

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {

            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            // 一次循环, 就是处理一组请求和响应
            while (true) {
                // 1. 读取请求并解析
                if (!scanner.hasNext()) {
                    // 判定当前 tcp 的连接是否断开了!
                    break;
                }
                String request = scanner.next();

                // 2. 根据请求计算响应
                String response = process(request);

                // 3. 把响应写回给客户端
                printWriter.println(response);
                printWriter.flush();

                // 4. 打印日志
                System.out.printf("[%s:%d] req: %s; resp: %s\n",
                        socket.getInetAddress().toString(), socket.getPort(), request, response);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());

                // 可能会存在大量的业务逻辑, 消耗很多的时间.

                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

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

使用这个socket对象,通过getInputStream和getOutputStream获取到流对象,进一步和客户端通信(ServerSocket相当于在外面揽客的,不提供具体的服务,而Socket是给客户提供具体的服务的)

区分连接和链接:

Connection连接:虚拟的关系,通信双方保存对端的关键信息

Link链接:相当于"快捷方式",快捷方式中包含了指向应用程序的路径

在这个步骤的第一步中,对于UDP来说,读取到一个DatagramPacket 就是一个请求

但是对于TCP来说,一次读一个字节,一个请求,应该是包含多个字节?

为了明确哪里是一个请求结束,就需要做出约定,约定一个请求到底是多长,比如可以约定一个请求以 \n 为结尾

所以第一个方法为:

java 复制代码
 //1,读取请求并解析
                StringBuilder request = new StringBuilder();
                while(true){
                    int b = inputStream.read();
                    char c = (char) b;
                    if(c == '\n'){
                        break;
                    }
                    request.append(b);
                }

第二种更为方便的方法为:

利用Scanner

由上可以看出Scanner的构造方法的参数是InputStream

Scanner并不是和"控制台"绑定的,而是和"InputStream"绑定

InputStream 可以是控制台,也可以是硬盘文件,还可以是socket文件

java 复制代码
 Scanner scannner = new Scanner(inputStream);

            //一次循环就是处理一组请求和响应
            while(true){
                //1,读取请求并解析
                String request = scannner.next();

next方法就是以 \n 为结束标志的,准确来说是以"空白符"作为结束标记,包括不限于空格,回车,换行,tab,垂直制表符,翻页符等等

步骤三的方法一:

此处返回响应的时候,需要给响应这里也加上 \n 作为结尾

客户端后续读的时候,也是要用 scanner.next(),所以也是要有明确的结束标记的

java 复制代码
//3,把响应写回给客户端
                response += "\n";
                outputStream.write(response.getBytes());

第二个更好的方法为:

java 复制代码
 PrintWriter printWriter = new PrintWriter(outputStream);

            //一次循环就是处理一组请求和响应
            while(true){
                //1,读取请求并解析
                String request = scannner.next();
                if(!scannner.hasNext()){
                    //判定当前 TCP 的连接是否断开
                    break;
                }

                //2,根据请求计算响应
                String response = process(request);

                //3,把响应写回给客户端
                printWriter.println(response);

printWrite和System.out用法差不多

此处的println 是先把数据写入到缓冲区中,而不是直接写到网卡里,等到缓冲区攒了一波数据了,然后再写入IO设备

这样操作是因为计算机访问IO设备(控制台,硬盘,网络)效率比访问内存要低很多(差2到3个数量级),假设我们需要连续访问IO设备,总体的时间开销比一次性访问要多很多

但是在这个地方我们需要打印操作一执行,就立刻访问网卡,所以需要加上冲刷(强制把缓冲区的数据写入到IO设备):

java 复制代码
//3,把响应写回给客户端
                printWriter.println(response);
                printWriter.flush();

客户端

java 复制代码
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;

    // 需要指定, 要连接的服务器的 IP 和 端口
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp, serverPort);
    }

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

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {

            Scanner scannerConsole = new Scanner(System.in);
            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                // 1. 从控制台读取用户输入
                System.out.print("-> ");
                String request = scannerConsole.next();

                // 2. 构造请求, 发送给服务器
                printWriter.println(request);
                printWriter.flush();

                // 3. 从服务器读取响应
                if (!scannerNetwork.hasNext()) {
                    break;
                }
                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();
    }
}
关于close的问题

socket是文件,需要被关闭

tcp 客户端的socket,不需要单独关闭,生命周期跟随进程

tcp 服务器的server socket 不需要单独关闭,生命周期跟随进程

tcp服务器的socket,需要单独关闭

1,socket 有很多个,每次有客户端连上来,都会创建新的socket

2,这个socket的生命周期,不是跟随进程,而是跟随tcp连接

随着时间的推移,连接上的客户端越来越多,长此以往,就会使文件描述符表越占越多

一个服务器需要能够给多个客户端提供服务

在以上代码运行之后会发现,当第一个客户端运行的时候,后面的其他的客户端就会卡住

这是因为,客户端1连接成功之后,服务器就会进入到processConnection的循环中,此时客户端1如果不发送请求,服务器就会阻塞在hasNext,如果客户端1发送请求,hasNext解除阻塞,继续执行,执行完毕之后,循环回来,继续hasNext阻塞,等待下一个请求,直到客户端退出,此时循环才结束

当服务器的代码,在processConnection中,不断的循环进行的时候,此时意味着无法第二次执行到accept了,即无法处理第二个客户端的请求

建立连接是内核的事情,连接建立了之后,代码如何处理,就是应用程序的事情,得accept了才能处理

Q:而如何做到又能等待accept处理新客户端的连接,又能等待hasNext处理当前客户端的请求呢?

A:多线程

主线程负责执行accept,每次有客户端连接上来了,创建新的线程,由新线程负责处理客户端的请求和响应,由N个客户端,就创建N个线程

服务器引入多线程
java 复制代码
//启动服务器
 
public void start() throws IOException {
    System.out.println("服务器启动!");
    while (true) {
        Socket clientSocket = serverSocket.accept();
        Thread t = new Thread(() -> {
            processConnection(clientSocket);
        });
        t.start();
    }
}

注意:在操作系统中,线程和线程之间,地位是平等的,没有"父子""主从"这样的关系,进程中,有父进程和子进程的说法

服务器引入线程池

为了避免频繁创建销毁线程,也可以引入线程池

java 复制代码
// 启动服务器
 
public void start() throws IOException {
   System.out.println("服务器启动!");
    ExecutorService service = Executors.newCachedThreadPool();
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 使⽤线程池, 来解决上述问题
 
        service.submit(new Runnable() {
            @Override
            public void run() {
                processConnection(clientSocket);
            }
        });
    }
}

四,长短连接

现在的时代是"高并发,高可用,大数据"

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。

长连接:不关闭连接,⼀直保持连接状态,双⽅不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

对比以上长短连接,两者区别如下:

  • 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建⽴连接,关闭连接;⽽⻓连接只需要 第⼀次建⽴连接,之后的请求、响应都可以直接传输。相对来说建⽴连接,关闭连接也是要耗时 的,⻓连接效率更⾼。
  • 主动发送请求不同:短连接⼀般是客⼾端主动向服务端发送请求;⽽⻓连接可以是客⼾端主动发送 请求,也可以是服务端主动发。
  • 两者的使⽤场景有不同:短连接适⽤于客⼾端请求频率不⾼的场景,如浏览⽹⻚等。⻓连接适⽤于 客⼾端与服务端通信频繁的场景,如聊天室,实时游戏等。

基于BIO(同步阻塞IO)的⻓连接会⼀直占⽤系统资源。对于并发要求很⾼的服务端系统来说,这样的 消耗是不能承受的。

由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在⼀个线程中运⾏。 ⼀次阻塞等待对应着⼀次请求、响应,不停处理也就是⻓连接的特性:⼀直不关闭连接,不停的处理 请求。

实际应⽤时,服务端⼀般是基于NIO(即同步⾮阻塞IO)来实现⻓连接,性能可以极⼤的提升

相关推荐
AI备忘录1 小时前
(十六)GRE/IPSec 隧道配置命令五厂商对照:华为 华三 锐捷 迈普 思科
运维·服务器·网络·网络协议·网络安全·华为
2601_967659891 小时前
芯片设计企业DMS落地实践:昂宝电子如何用八骏管好“代理-价格-订单-库存-费控-分析“全链路
网络
实心儿儿1 小时前
Linux —— 数据链路层
linux·网络·智能路由器
牛马工作号1 小时前
大二层组网技术详解:VXLAN、GRETAP 等
网络·网络协议·安全·arp·大二层网络
Messy create2 小时前
【储能系统三大核心】
网络·stm32·单片机·算法·能源
QYRdata2 小时前
24.7%年复合增长率!工业元宇宙平台2026-2032年发展预期明确
大数据·网络·人工智能
盛世宏博智慧档案2 小时前
工程商以太网(RJ45/POE)温湿度传感器完整选型指南
网络
捷烽2 小时前
三大运营商熔接损耗验收:国家规范与0.08dB的正确理解
运维·网络·信息与通信
CHENKONG_CK3 小时前
晨控CK-FR08系列与欧姆龙NX系列PLC配置MODBUSTCP例程使用手册
网络·单片机·嵌入式硬件