简单实现网络编程

news2024/12/25 10:33:54

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();
    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线性表--链表--带头双向循环链表

目录 1.什么是带头双向循环链表&#xff1f; 2.实现增删查改功能&#xff1a; 2.1使用链表前必须对头节点初始化 2.2尾插 2.3尾删 2.4头插 2.5头删 2.8查找 2.7指定位置插入 2.8指定位置删除 2.9改变数据 ​编辑 2.10打印 2.11销毁 3.代码 1.什么是带头双向循环链表&…

docker 基础手册

文章目录 docker 基础手册docker 容器技术镜像与容器容器与虚拟机docker 引擎docker 架构docker 底层技术docker 二进制安装docker 镜像加速docker 相关链接docker 生态 docker 基础手册 docker 容器技术 开源的容器项目&#xff0c;使用 Go 语言开发原意“码头工人”&#x…

【C++练级之路】【Lv.7】【STL】vector类的模拟实现

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C语言》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、成员变量二、默认成员函数2.1 constructor2.2 destructor2.3 copy constructor2.4 operator 三、迭代器…

Vue3+TS+dhtmlx-gantt实现甘特图

实现样式 因为只做展示&#xff0c;所以实现很简单 实现功能 自定义列头增加斑马线&#xff0c;实际结束时间&#xff08;自定义实现&#xff09;自定义进度展示&#xff0c;根据层级让进度背景颜色变浅marker标记今天自定义提示框内容 实现 import { gantt } from "d…

某顺cookie逆向

目标网站:aHR0cHM6Ly9xLjEwanFrYS5jb20uY24v 这个网站是对cookie进行反爬虫的&#xff0c;可以看到cookie中有一个加密参数v 二、分析参数 可以使用hook方法&#xff0c;来hook住cookie中v生成的位置&#xff0c;可以直接在控制台中输入hook函数 (function () {use strict;v…

【详细解释深度学习图像分类检测评价指标】准确率Accuracy、精确率Precision、召回率Recall、mAP等(一文搞懂,建议收藏)

前言&#xff1a; &#x1f60a;&#x1f60a;&#x1f60a;欢迎来到本博客&#x1f60a;&#x1f60a;&#x1f60a; &#x1f31f;&#x1f31f;&#x1f31f; 本专栏主要是记录工作中、学习中关于AI(Deep Learning)相关知识并分享。 &#x1f60a;&#x1f60a;&#x1f…

Parallels Desktop 19 mac 虚拟机软件 兼容M1 M2

Parallels Desktop 19 for Mac 是一款适用于 macOS 的虚拟机软件。无需重启即可在 Mac 上运行 Windows、Linux 等系统&#xff0c;具有速度快、操作简单且功能强大的优点。包括 30 余种实用工具&#xff0c;可简化 Mac 和 Windows 上的日常任务。 软件下载&#xff1a;Parallel…

大模型时代的计算机系统革新:更大规模、更分布式、更智能化

编者按&#xff1a;2023年是微软亚洲研究院建院25周年。借此机会&#xff0c;我们特别策划了“智启未来”系列文章&#xff0c;邀请到微软亚洲研究院不同研究领域的领军人物&#xff0c;以署名文章的形式分享他们对人工智能、计算机及其交叉学科领域的观点洞察及前沿展望。希望…

搭建网站使用花生壳的内网穿透实现公网访问

目录 一 搭建网站 二 使用花生壳进行内网穿透 1、创建内网映射 2、linux系统安装花生壳客户端 3、重新打开浏览器&#xff0c;输入http://b.oray.com&#xff0c;完成账户登录&#xff0c;激活&#xff08;SN登录&#xff09; 一 搭建网站 准备工作&#xff1a; [rootse…

主播产品对比话术

—、价格对比 主播产品A︰这款产品定价相对较高&#xff0c;但是其品质和功能都是一流的&#xff0c;对于追求高端体验的消费者来说&#xff0c;物有所值。 主播产品B∶这款产品的价格相对较低&#xff0c;性价比很高&#xff0c;对于预算有限的消费者来说&#xff0c;是个不…

基于Java SSM框架现图书馆借阅管理系统项目【项目源码+论文说明】

基于java的SSM框架实现图书馆借阅管理系统演示 摘要 以往的图书馆管理事务处理主要使用的是传统的人工管理方式&#xff0c;这种管理方式存在着管理效率低、操作流程繁琐、保密性差等缺点&#xff0c;长期的人工管理模式会产生大量的文本借书与文本数据&#xff0c;这对事务的…

控制项目风险

一、风险预算 暴雪公司经理艾莉森&#xff0c;暴雪公司是一家小型工业企业&#xff0c;该公司的高管为了降低生产成本&#xff0c;决定搬迁工厂。项目经理明白实际情况与初始计划之间常常会有很大的出入。项目经理需要事先为一些事情做好准备&#xff0c;并在项目运作或预算方面…

Vue基础-Computed-Watch

一、computed计算属性使用 1.复杂data的处理方式 我们知道&#xff0c;在模板中可以直接通过插值语法显示一些data中的数据。 但是在某些情况&#xff0c;我们可能需要对数据进行一些转化后再显示&#xff0c;或者需要将多个数据结合起来进行显示&#xff1b; 比如我们需要…

ORA-12528: TNS: 监听程序: 所有适用例程都无法建立新连

用了网上的办法&#xff1a; 1、修改listener.ora的参数,把动态的参数设置为静态的参数,红色标注部分 位置D:\oracle\product\10.2.0\db_1\NETWORK\ADMIN SID_LIST_LISTENER (SID_LIST (SID_DESC (SID_NAME PLSExtProc) (ORACLE_HOME D:\oracle\produ…

微信小程序(十)表单组件(入门)

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.type 属性指定表单类型 2.placeholder 属性指定输入框为空时的占位文字 源码&#xff1a; form.wxml <!-- 提前准备好的布局结构代码 --> <view class"register"><view class"…

【LangChain学习之旅】—(10) 用RouterChain确定客户意图

【【LangChain学习之旅】—&#xff08;10&#xff09; 用RouterChain确定客户意图 任务设定整体框架具体步骤如下&#xff1a; 具体实现构建提示信息的模板构建目标链 Reference&#xff1a;LangChain 实战课 任务设定 首先&#xff0c;还是先看一下今天要完成一个什么样的任…

Pyro —— DOP Nodes

目录 Smoke Object —— 创建smoke对象及相关场 Smoke Solver —— Smoke解算器 Color Relationships Advanced Pyro Solver —— Pyro解算器 Smoke Object (Sparse) —— 创建smoke对象及相关场 Smoke Solver (Sparse) —— Sparse Smoke解算器 Simulation Advanced …

[足式机器人]Part2 Dr. CAN学习笔记- 最优控制Optimal Control Ch07

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记 - 最优控制Optimal Control Ch07-1最优控制问题与性能指标 1. 最优控制问题与性能指标2. 动态规划 Dynamic Programming2.1 基本概念2.2 代码详解2.3 简单一维案例 3. 线性二次型调节器&#xff…

SourceTree修改仓库密码

1、找到 SourceTree缓存文件目录&#xff1a; passwd 目录保存账号对应的密码&#xff08;已加密&#xff09; 2、删除密码 删除passwd文件即可。重启 SourceTree 软件&#xff0c;进行操作&#xff0c;就会有输入密码的弹窗&#xff0c;输入即可。

高标准农田气象站

在当今社会&#xff0c;科技的发展正在深刻地改变着我们的生活。特别是在农业领域&#xff0c;科技的运用已经成为了保障粮食安全、提高农业生产效率的重要手段。其中&#xff0c;高标准农田气象站作为现代农业的重要组成部分&#xff0c;正在发挥着越来越重要的作用。 TH-NQ14…