当前位置: 首页 > article >正文

【ESP32】ESP-IDF开发 | WiFi开发 | HTTP服务器

1. 简介

1.1 HTTP

        HTTP(Hyper Text Transfer Protocol),全称超文本传输协议,用于从网络服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还能确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本、图形等)。HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器(C/S)模型。HTTP是一个无状态的协议,基于TCP协议传输数据,默认使用端口80

1.1.1 请求

        HTTP把请求分成多种类型,其中最常用的是GET请求和POST请求

1. GET请求

        GET请求一般用于信息的获取,如访问网站使用的就是GET请求。GET请求仅仅只是获取资源信息,就像数据库查询一样,不会修改、增加数据,不会影响资源的状态。如果我们想在请求资源的同时附带数据,那么这些数据会被显式地放在请求URL上面

2. POST请求

        POST请求则表示可能会修改服务器上的资源,GET请求能做的,POST请求也能做。但最大的区别是请求参数的存放位置,POST请求会把参数隐式地放在请求报文中,所以对于敏感参数如账号密码等,会是更推荐的。

1.2 HTML

        HTML(HyperText Markup Language),全称超文本标记语言,是一种用于创建网页的标准标记语言。使用 HTML ,可以建立自己的 WEB 站点,HTML 运行在浏览器上,由浏览器来解析。

        最基础的HTML由以下组成:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My HTML</title>
</head>
<body>
<h1>This is a title</h1>
<p>This is a paragraph</p>
</body>
</html>
  • <!DOCTYPE html>:声明这时一个HTML文档;
  • <html></html>:HTML内容;
  • <head></head>:头部内容;
  • <body></body>:页面内容。

2. 例程

        这个例程会在ESP32上面搭建一个简单的HTTP服务器,供局域网中的设备访问,包含基本的GET和POST请求演示。

2.1 函数API 

2.1.1 启动HTTP服务器

esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config);
  • handle:HTTP服务器句柄;
  • config:配置参数。 
typedef struct httpd_config {unsigned    task_priority;size_t      stack_size;BaseType_t  core_id;uint32_t    task_caps;uint16_t    server_port;uint16_t    ctrl_port;uint16_t    max_open_sockets;uint16_t    max_uri_handlers;uint16_t    max_resp_headers;uint16_t    backlog_conn;bool        lru_purge_enable;uint16_t    recv_wait_timeout;uint16_t    send_wait_timeout;void * global_user_ctx;httpd_free_ctx_fn_t global_user_ctx_free_fn;void * global_transport_ctx;httpd_free_ctx_fn_t global_transport_ctx_free_fn;bool enable_so_linger;int linger_timeout;bool keep_alive_enable;int keep_alive_idle;int keep_alive_interval;int keep_alive_count;httpd_open_func_t open_fn;httpd_close_func_t close_fn;httpd_uri_match_func_t uri_match_fn;
} httpd_config_t;

        配置参数比较多,ESP-IDF也提供了HTTPD_DEFAULT_CONFIG宏来初始化默认配置。如果要自定义,可以关注几个比较常用的:

  • stack_size:HTTP服务器任务的栈空间;
  • server_port:服务器端口,默认是80;
  • max_open_sockets:最大可开启socket,即可以连接的客户端数量;
  • max_uri_handlers:最大URI句柄数量;
  • recv_wait_timeout:接收超时时间;
  • send_wait_timeout:发送超时时间;
  • keep_alive_enable:网页保活使能;
  • keep_alive_idle:保活空闲时间;
  • keep_alive_interval:保活间隔时间;
  • keep_alive_count:保活包失败重传次数。

2.1.2 注册URI处理

esp_err_t httpd_register_uri_handler(httpd_handle_t handle, const httpd_uri_t *uri_handler);
  • handle:HTTP句柄;
  • uri_handler:URI处理结构体。
typedef struct httpd_uri {const char       *uri;httpd_method_t    method;esp_err_t (*handler)(httpd_req_t *r);void *user_ctx;
} httpd_uri_t;
  • uri:URI;
  • method:请求类型,格式如HTTP_XXX;
  • handler:处理函数;
  • user_ctx:用户上下文。

2.1.3 获取请求头字段内容长度

size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field);
  • r:HTTP请求句柄;
  • field:字段名。

2.1.4 获取请求头字段内容

esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size);
  • r:HTTP请求句柄;
  • field:字段名;
  • val:输出数组;
  • val_size:数组长度。

2.1.5 获取URL参数长度

size_t httpd_req_get_url_query_len(httpd_req_t *r)
  • r:HTTP句柄。

2.1.6 获取URL参数

esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len)
  • r:HTTP句柄;
  • buf:输出数组;
  • buf_len:数组长度。

2.1.7 发送响应

esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len);
esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len);
static inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *str);
static inline esp_err_t httpd_resp_sendstr_chunk(httpd_req_t *r, const char *str);

         发送响应有几个函数,send结尾的就是一次性把数据发送完;send后面接str的就是发送字符串,这样就不需要传长度;chunk结尾的就是可以多次发送数据,最后一定要发一个长度为0的包,表示发送完成。

2.1.8 接收请求内容

int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len)
  • r:HTTP句柄;
  • buf:输出数组;
  • buf_len:数组长度。

2.2 代码

#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "sys/socket.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "netdb.h"
#include "arpa/inet.h"
#include "esp_http_server.h"#include <string.h>#define TAG "app"static httpd_handle_t http_server;static const char index_html[] = " \
<!DOCTYPE html> \
<html> \<head> \<meta charset=\"utf-8\"> \<title>index</title> \</head> \
\<body> \<h1>Hello from ESP32</h1> \<form action=\"/hello\" method=\"post\"> \<label for=\"name\">What's your name:</label> \<input type=\"text\" id=\"name\" name=\"name\" required> \<input type=\"submit\" value=\"OK\"></button> \</form> \</body> \
</html> \
";static const char hello_html_template[] = " \
<!DOCTYPE html> \
<html> \<head> \<meta charset=\"utf-8\"> \<title>hello</title> \</head> \
\<body> \<h1>Oh, Hello %s</h1> \</body> \
\
</html> \
";static esp_err_t index_get_handler(httpd_req_t *req)
{/* 获取Host信息 */size_t buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1;if (buf_len > 1) {char *buf = malloc(buf_len);if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) {ESP_LOGI(TAG, "Get request to host: %s", buf);}free(buf);}/* 回复数据包 */httpd_resp_sendstr(req, index_html);return ESP_OK;
}static const httpd_uri_t index_uri = {.uri       = "/index",.method    = HTTP_GET,.handler   = index_get_handler,.user_ctx  = NULL
};static esp_err_t hello_post_handler(httpd_req_t *req)
{/* 获取Host信息 */size_t buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1;if (buf_len > 1) {char *buf = malloc(buf_len);if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) {ESP_LOGI(TAG, "Post request to host: %s", buf);}free(buf);}/* 获取内容长度 */int len = 0;{char *buf = malloc(128);memset(buf, 0, 128);if (httpd_req_get_hdr_value_str(req, "Content-Length", buf, 128) != ESP_OK) {ESP_LOGE(TAG, "Get content length failed");return ESP_FAIL;}len = atoi(buf) + 1;free(buf);}/* 获取表单数据 */char *buf = malloc(len);memset(buf, 0, len);if (httpd_req_recv(req, buf, len) <= 0) {ESP_LOGE(TAG, "Receive request content failed");return ESP_FAIL;}if (strstr(buf, "name=") == NULL) {ESP_LOGE(TAG, "Can't found fleid \"name\"");free(buf);return ESP_FAIL;}/* 发送数据 */char *hello_html = malloc(1024);snprintf(hello_html, 1024, hello_html_template, buf + strlen("name="));httpd_resp_sendstr(req, hello_html);free(buf);free(hello_html);return ESP_OK;
}static const httpd_uri_t hello_uri = {.uri       = "/hello",.method    = HTTP_POST,.handler   = hello_post_handler,.user_ctx  = NULL
};static void wifi_event_handler(void* arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{if (event_base == IP_EVENT) {if (event_id == IP_EVENT_STA_GOT_IP) {httpd_config_t config = HTTPD_DEFAULT_CONFIG();if (httpd_start(&http_server, &config) == ESP_OK) {httpd_register_uri_handler(http_server, &index_uri);httpd_register_uri_handler(http_server, &hello_uri);}ESP_LOGI(TAG, "HTTP server on port %d", config.server_port);}} else if (event_base == WIFI_EVENT) {if (event_id == WIFI_EVENT_STA_DISCONNECTED) {httpd_stop(http_server);ESP_LOGI(TAG, "HTTP server stopped");} else if (event_id == WIFI_EVENT_STA_START) {esp_wifi_connect();}}
}int app_main()
{/* 初始化NVS */esp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}/* 初始化WiFi协议栈 */ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&wifi_event_handler,NULL,NULL));wifi_config_t wifi_config = {.sta = {.ssid = "Your SSID",.password = "Your password",.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));ESP_ERROR_CHECK(esp_wifi_start());return 0;
}

         WiFi基站模式和AP连接在之前的文章有讲过,这里不再赘述。

        AP连接成功后会启动HTTP服务器,我这里全部使用默认的配置,注册了两个URI处理,一个是“/index”,用来演示GET请求,获取主页;一个是“/hello”,用来演示POST请求。

        第一个URI处理函数,演示一下请求头字段的获取,一般先获取字段的长度,接着请求对应大小的堆内存,再copy数据到数组中。不建议在函数内直接定义数组,因为处理函数是在HTTP任务中调用的,这样做很容易导致栈溢出。最后就是返回HTML页面文本给客户端。

如果ESP32在接受请求时报413错误,在SDK的配置文件(sdkconfig)中,修改CONFIG_HTTPD_MAX_REQ_HDR_LEN配置,增大请求头的长度。

         这个URI处理会返回一个HTML页面,如果你用的是浏览器请求的话,就会有自动解析并显示画面。

        这个HTML包含一个标题和一些表单控件,我们可以在文本框这里填写自己的名字,点击“OK”按钮,会向ESP32提交表单数据,其实就是向“/hello”这个URI发起POST请求。

        对于POST请求,在HTTP的请求头中会有一个“Content-Length”字段来描述数据包的大小。我们首先获取这个字段内容,然后去请求相应的内存空间,最后copy数据包数据到数组中;如果数据包非常大的话也可以多次获取。

        对于表单数据,一般都是以键值对的形式组成的,中间用等于号连接。接收到POST请求后需要返回对应的数据,这里就是HTML文件,我们把表单获取到的数据附到HTML文档中,显示的效果如下。

 

相关文章:

【ESP32】ESP-IDF开发 | WiFi开发 | HTTP服务器

1. 简介 1.1 HTTP HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff0c;全称超文本传输协议&#xff0c;用于从网络服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效&#xff0c;使网络传输减少。它不仅保证计算机正确快速地传输超文本文档…...

hbase合并队列超长问题分析

问题现象 hbase集群合并队列超长,有节点上合并任务已经运行超过1天未结束,合并队列总长不断增加。 问题分析 参数配置: 配置参数默认值含义hbase.hregion.memstore.flush.size128MMemStore达到该值会Flush成StoreFilehbase.hregion.memstore.block.multiplier4当region中…...

【YOLOv11改进- 主干网络】YOLOv11+CSWinTransformer: 交叉窗口注意力Transformer助力YOLOv11有效涨点;

YOLOV11目标检测改进实例与创新改进专栏 专栏地址:YOLOv11目标检测改进专栏,包括backbone、neck、loss、分配策略、组合改进、原创改进等 本文介绍 发paper,毕业皆可使用。 本文给大家带来的改进内容是在YOLOv11中更换主干网络为CSWinTransformer,助力YOLOv11有效涨点,…...

滚动弹幕案例

滚动弹幕案例 一、需求 1.页面上漂浮字体大小不一、颜色不一&#xff0c;从左向右滚动的弹幕&#xff1b; 2.底部中间有一个发送功能&#xff0c;可以发送新的弹幕&#xff1b; 3.底部的发送部分可以向下收起和弹出。 二、html <div class"container"><…...

图像处理篇---基本OpenMV图像处理

文章目录 前言1. 灰度化&#xff08;Grayscale&#xff09;2. 二值化&#xff08;Thresholding&#xff09;3. 掩膜&#xff08;Mask&#xff09;4. 腐蚀&#xff08;Erosion&#xff09;5. 膨胀&#xff08;Dilation&#xff09;6. 缩放&#xff08;Scaling&#xff09;7. 旋转…...

Linux软件编程(2)

一、标准IO 1.fread/fwrite size_t fwrite (const void *ptr,size_t size,size_t nmemb,FILE *stream); 功能&#xff1a;函数从指定的内存位置开始&#xff0c;将一块数据写入到指定的文件流中。 参数&#xff1a; ptr:指向要写入文件的数据块的指针 size:要写入的每个数据…...

vue框架生命周期详细解析

Vue.js 的生命周期钩子函数是理解 Vue 组件行为的关键。每个 Vue 实例在创建、更新和销毁过程中都会经历一系列的生命周期阶段&#xff0c;每个阶段都有对应的钩子函数&#xff0c;开发者可以在这些钩子函数中执行特定的操作。 Vue 生命周期概述 Vue 的生命周期可以分为以下几…...

2010年下半年软件设计师考试上午真题的知识点整理(附真题及答案解析)

以下是2010年下半年软件设计师考试上午真题的知识点分类整理&#xff0c;涉及定义的详细解释&#xff0c;供背诵记忆。 1. 计算机组成原理 CPU与存储器的访问。 Cache的作用: 提高CPU访问主存数据的速度&#xff0c;减少访问延迟。存储器的层次结构: 包括寄存器、Cache、主存和…...

459重复的子字符串(substr)

1、题目描述 给定一个非空的字符串 s &#xff0c;检查是否可以通过由它的一个子串重复多次构成。 2、示例 示例 1: 输入: s "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。示例 2: 输入: s "aba" 输出: false示例 3: 输入: s …...

腿足机器人之五- 粒子滤波

腿足机器人之五粒子滤波 直方图滤波粒子滤波 上一篇博客使用的是高斯分布结合贝叶斯准则来估计机器人状态&#xff0c;本篇是基于直方图和粒子滤波器这两种无参滤波器估计机器人状态。 直方图方法将状态空间分解成有限多个区域&#xff0c;并用直方图表示后验概率。直方图为每个…...

OpenAI 快速入门

文章来源&#xff1a;OpenAI开发者平台 | OpenAI开发文档|OpenAI中文官方文档|ChatGPT中文版|ChatGPT教程 开发人员快速入门 了解如何发出您的第一个 API 请求。 OpenAI API 为最先进的 AI 模型提供了一个简单的接口&#xff0c;用于自然语言处理、图像生成、语义搜索和语音识…...

React通用登录/注销功能实现方案(基于shadcn/ui)

React通用登录/注销功能实现方案&#xff08;基于shadcn/ui&#xff09; 一、功能需求分析二、通用功能封装1. 通用登录表单组件2. 认证Hook封装 三、功能使用示例1. 登录页面实现2. 用户菜单实现 四、路由保护实现五、方案优势 一、功能需求分析 需要实现以下核心功能&#x…...

Django中数据库迁移命令

在 Django 中&#xff0c;数据库迁移是确保数据库结构与 Django 模型定义保持一致的重要过程。以下是 Django 中常用的数据库迁移命令&#xff1a; 1. python manage.py makemigrations 功能&#xff1a;此命令用于根据 Django 项目的模型文件&#xff08;models.py&#xff…...

spring214

spring父子容器&#xff1a; 为什么会有spring父子容器&#xff0c;&#xff0c;因为一般大一点的项目都是分模块的&#xff0c;&#xff0c;不同的人开发不同的模块&#xff0c;&#xff0c;可以在两个不同的模块中&#xff0c;&#xff0c;使用相同的beanName&#xff0c;&a…...

AI 编程工具—Cursor 进阶篇 数据分析

AI 编程工具—Cursor 进阶篇 数据分析 上一节课我们使用Cursor 生成了北京房产的销售数据,这一节我们使用Cursor对这些数据进行分析,也是我们尝试使用Cursor 去帮我们做数据分析,从而进一步发挥Cursor的能力,来帮助我们完成更多的事情 案例一 房产销售数据分析 @北京202…...

搭建Deepseek推理服务

概述&#xff1a; 本文介绍用Open webui ollama搭建一套Deepseek推理服务&#xff0c;可以在web页面上直接进行对话。作为体验搭建的是Deepseek 7b参数版本 首先选择一个云厂商创建一台ubuntu系统的虚拟机&#xff0c;带公网IP&#xff0c;通过shell登录虚拟机完成以下操作&…...

GDB 调试入门教程

GDB 调试入门教程 1. sample.cpp1.1. Compile and Run 2. GDB 调试3. GDB commandsReferences GDB is a command line debugger. It is a good choice on Linux or WSL. On macOS, use LLDB instead. 1. sample.cpp (base) yongqiangyongqiang:~/workspace/yongqiang$ ls -l …...

STM32的HAL库开发---ADC

一、ADC简介 1、ADC&#xff0c;全称&#xff1a;Analog-to-Digital Converter&#xff0c;指模拟/数字转换器 把一些传感器的物理量转换成电压&#xff0c;使用ADC采集电压&#xff0c;然后转换成数字量&#xff0c;经过单片机处理&#xff0c;进行控制和显示。 2、常见的AD…...

6.编写正排索引切分字符串|倒排索引原理|引入jieba到项目(C++)

编写正排索引 继续编写incde.hpp #pragma once#include <iostream> #include <string> #include <vector> #include <fstream> #include <unordered_map> #include "util.hpp"namespace ns_index{struct DocInfo{std::string title;…...

在Windows系统上测试safari浏览器的兼容性

文章目录 前言手机端的safari浏览器能替代PC端吗在Windows上测试safari浏览器的兼容性的方法利用云服务使用虚拟机在Windows上下载虚拟机遇到的问题以及解决思路总结 前言 在测试网站的兼容性时需要用到safari浏览器&#xff0c;在没有Mac的情况下&#xff0c;又不想麻烦同事&…...

【设计模式】【结构型模式】桥接模式(Bridge)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…...

惠普HP Color LaserJet CP1215/1210彩色打印机打印校准方法

执行校准 &#xff08;用随机光盘安装驱动&#xff09;完整安装打印机驱动程序。安装驱动程序的操作方法请参考以下文章&#xff1a; 惠普HP Color laserjet cp1215激光打印机在windows 7下使用随机光盘安装驱动程序&#xff0c;安装完成后&#xff1b; 依次点击“开始”→“所…...

【雅思博客02】Virus!

Elementary ‐ Virus! (C0007) A: Oh great! This stupid computer froze again! That’s the third time today! Hey Samuel, can you come take a look at my PC? It’s acting up again. It must have a virus or something. B: Just give me a second; I’ll be right …...

模型GPU->NPU(Ascend)迁移训练简述

目录 一、迁移训练流程图解 二、详细流程步骤 1. 模型训练与日志记录 2. 跨平台精度对齐对比 3. 问题定位与修复 4. 迭代验证 三、关键技术点 四、常见问题与解决方案 一、迁移训练流程图解 通过华为云的modelart进行运行环境选型 北京四使用GPU进行模型训练&#xff…...

skywalking实现原理

SkyWalking 是一个开源的分布式应用性能监控&#xff08;APM&#xff09;系统&#xff0c;主要用于微服务、云原生应用的性能监控、追踪和故障诊断。其实现原理涉及多个核心模块和技术&#xff0c;以下是 SkyWalking 的实现原理概述&#xff1a; 1. 采集数据&#xff08;数据收…...

sql语言语法的学习

sql通用语法 sql分类 DDL(操作数据库和表) 操作数据库 操作表_查询 操作表_创建 举例&#xff1a; 操作表_删除 操作表_修改 DML(增删改表中数据) DML添加数据 DML删除数据...

3.buuctf [BSidesCF 2019]Kookie

进入题目页面如下 尝试弱口令密码登录&#xff0c;无果 显示无效密码 用题中给出的用户名和密码登录虽然成功但没得到flag 用burp suite抓包试试 看到username处显示cookie 题目说用admin登录 将username的值改为admin 拿到flag 最后拿到flag...

springboot245-springboot项目评审系统(源码+论文+PPT+部署讲解等)

&#x1f495;&#x1f495;作者&#xff1a; 爱笑学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…...

Dify+Ollama+DeepSeek部署本地大模型+知识库搭建

前言 上一篇文章《OllamaDeepSeek部署本地大模型》我们已经知道如何在本地搭建自己的大模型了&#xff0c;不过想要让大模型能够根据我们个人或者企业的数据情况做出精准的回答&#xff0c;我们还需要将自己的数据投喂给大模型才可以。本篇文章我们将会使用一个开源项目dify集…...

每日一题——不同路径的数目与矩阵最小路径和

机器人路径问题与矩阵最小路径和 1. 机器人路径问题题目描述示例示例 1示例 2 解题思路动态规划 代码实现复杂度分析 2. 矩阵的最小路径和题目描述示例示例 1示例 2 解题思路动态规划 代码实现复杂度分析 总结 1. 机器人路径问题 题目描述 一个机器人在 (m \times n) 大小的地…...