STM32移植lwip之建立web服务器
来源:互联网 发布:深圳it软件开发 编辑:程序博客网 时间:2024/05/20 15:12
本篇目标:在之前能ping通pc机的工程基础上搭建web服务器,借鉴官方web服务器的程序与网页,能够用pc机浏览器访问web服务器,并返回设置的网页
材料准备:
- 基础工程:修改后能ping通pc机的工程(STM32官方移植lwip修改代码)
- 搭建工程:最终搭建好的web服务器工程(STM32搭建web服务器工程)
- 调试工具:用来调试tcp连接下的数据接收(网络调试助手)
- 测试浏览器:这里使用的是Chrome谷歌浏览器
ps:通过修改官方搭建web服务器的代码,来了解搭建的过程,其中暂时去掉了ssi和cgi的程序,仅仅实现网页数据的返回和网页的跳转,并将官方的代码简化到相对最简,以便以后的学习之用
浏览器请求指令探索
要搭建服务器,首先肯定要先了解远程客户端是怎么访问服务器的,这里pc机的浏览器则作为客户端:
- 打开浏览器(谷歌浏览器测试),输入服务器ip;
- 浏览器发送请求命令给服务器;
- 服务器接收到指令后,通过程序来解析指令,找到对应应该返回的网页;
- 服务器发送网页代码给浏览器;
- 浏览器显示网页;
接下来再用搭建虚拟服务器的方法,来模拟一下上面的过程:
打开网络调试助手,切换到网络服务器,在服务器操作端口输入80,点击创建,如图;这样我们就创建了一个虚拟的服务器,这个服务器的ip就是pc机的本地ip
查看确认一下本地ip地址,可以在网络连接里面查看,也可以在cmd输命令查看,这里的ip地址为192.168.6.104,如图:
打开浏览器(谷歌浏览器测试),输入刚才确认的本地ip地址,这里输入192.168.6.104:
返回去看看刚才搭建的服务器有什么变化,会发现有接收到的数据,只要重点观察第一行的数据“GET / HTTP/1.1”,这个字符串将会被服务器解析,然后将网页代码返回回去:
找到一个官方程序有一个fs文件夹,里面有已经做好的网页,打开网页index.html,右击-查看源代码,然后全选复制下来,在网络调试助手的发送区粘贴,并点击发送,如图:
这时,会发现浏览器已经显示了一张网页,但是好像又有点不全,因为图片没有显示,为什么呢?返回网络调试助手,发现接收区又有好多请求,看字面意思,好像就是图片的请求,然而服务器没有返回图片数据,所以图片无法显示
这时候,将所有的浏览器请求列出来比较一下:
“GET / HTTP/1.1”
“GET /STM32F4x7_files/ST.gif HTTP/1.1”
“GET /inchtml-pages-stm32_connectivity_files/pixel.gif HTTP/1.1”
“GET /STM32F4x7_files/stm32.jpg HTTP/1.1”
发现请求中 / 后面一部分的内容不相同,所以服务器只需要解析这一部分的字符串内容,来返回对应的网页数据即可
搭建web服务器
现在创建一个新的c文件,取名为 http_server.c ,接下来写几个函数来建立web服务器,抽重要的函数进行总结一下:
- web服务器初始化函数 Http_Server_Init():
void Http_Server_Init(void){ struct tcp_pcb *http_server_pcb; /* 为web服务器分配一个tcp_pcb结构体 */ http_server_pcb = tcp_new(); /* 绑定本地端号和IP地址 */ tcp_bind(http_server_pcb, IP_ADDR_ANY, 80); /* 监听之前创建的结构体http_server_pcb */ http_server_pcb = tcp_listen(http_server_pcb); /* 初始化结构体接收回调函数 */ tcp_accept(http_server_pcb, http_server_accept);}
小结:上面函数主要就是为搭建web服务器做准备,包括申请网络结构体、设置80端口号、监听数据、设置接收数据回调函数;
- 接收数据回调函数 tcp_server_accept() :
static err_t http_server_accept(void *arg, struct tcp_pcb *pcb, err_t err){ struct http_state *hs; /* 分配内存空间 */ hs = (struct http_state *)mem_malloc(sizeof(struct http_state)); if (hs != NULL) { memset(hs, 0, sizeof(struct http_state)); } /* 确认监听和连接 */ tcp_arg(pcb, hs); /* 配置接收回调函数 */ tcp_recv(pcb, http_server_recv); /* 配置轮询回调函数 */ tcp_poll(pcb, http_server_poll, 4); /* 配置发送回调函数 */ tcp_sent(pcb, http_sent); return ERR_OK;}
小结:函数中主要配置一些回调函数,比如接收,轮询,发送;
- 接收数据处理函数 http_server_recv() :
static err_t http_server_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *http_recv_pbuf, err_t err){ err_t parsed = ERR_ABRT; struct http_state *hs = (struct http_state *)arg; /* 告诉tcp已经接收到数据 */ tcp_recved(pcb, http_recv_pbuf->tot_len); if (hs->handle == NULL) { /* 解析接收到的浏览器请求数据 */ parsed = http_parse_request(&http_recv_pbuf, hs, pcb); } /* 清空请求字符串 */ if (parsed != ERR_INPROGRESS) { if (hs->req != NULL) { pbuf_free(hs->req); hs->req = NULL; } } if (parsed == ERR_OK) { /* 发送网页数据 */ http_send_data(pcb, hs); } else if (parsed == ERR_ARG) { /* 关闭连接 */ close_conn(pcb, hs); } return ERR_OK;}
小结:函数主要工作将接收到的数据放入 http_parse_request() 函数进行解析,然后把网页数据发送出去;
- 接收数据解析函数 http_parse_request():
static err_t http_parse_request(struct pbuf **inp, struct http_state *hs, struct tcp_pcb *pcb){ char *data; char *crlf; u16_t data_len; struct pbuf *p = *inp; char *sp1, *sp2; u16_t uri_len; char *uri; /* 排列字符串 */ if (hs->req == NULL) { hs->req = p; } else { /* 将多次的请求字符串进行连接排序 */ pbuf_cat(hs->req, p); } /* 拷贝输入数据 */ if (hs->req->next != NULL) { data_len = hs->req->tot_len; pbuf_copy_partial(hs->req, data, data_len, 0); } else { data = (char *)p->payload; data_len = p->len; } /* 提取接收到的浏览器字符串,浏览器请求示例:"GET / HTTP/1.1" */ if (data_len > 7) { crlf = strstr(data, "\r\n"); if (crlf != NULL) { /* 比较前4个字符是否为 "GET " */ if (strncmp(data, "GET ", 4) == 0) { /* sp1指向字符串 "/ HTTP/1.1" */ sp1 = (data + 4); } /* 在sp1字符串中寻找字符" ",sp2指向字符串 " HTTP/1.1" */ sp2 = strstr(sp1, " "); /* uri_len获取sp1字符串首地址到sp2字符串首地址的长度 */ uri_len = sp2 - (sp1); if ((sp2 != 0) && (sp2 >= (sp1))) { /* 将解析的字符串赋给uri,并在最后加上结束符\0, uri指向字符串 "/\0" */ uri = sp1; *(sp1 - 1) = 0; uri[uri_len] = 0; /* 根据字符串寻找对应网页数据 */ return http_find_file(hs, uri, 0); } } } return ERR_OK;}
小结:这个函数是重要的请求数据解析函数,函数将接收到的字符串(例:“GET /STM32F4x7_files/ST.gif HTTP/1.1”)
分离出重要的判断字符串(例:“ /STM32F4x7_files/ST.gif”),然后根据这个字符串的内容来读取对应的网页数据;
- 读取对应网页数据函数 http_find_file():
static err_t http_find_file(struct http_state *hs, const char *uri, int is_09){ struct fs_file *file = NULL; /* 如果字符串为 "/\0",则打开index网页 */ if((uri[0] == '/') && (uri[1] == 0)) { file = fs_open("/index.html"); uri = "/index.html"; } else { /* 如果为其他请求,则打开相应网页 */ file = fs_open(uri); } /* 将网页文件数据赋值给http_state结构体,之后发送出去 */ return http_init_file(hs, file, is_09, uri);}
小结:此函数就是网页数据读取函数,里面最重要的函数就是 fs_open() 函数了,这个函数在官方建立web服务器工程里的 fs.c 文件里面,这个函数的解析放到后面;
ps:http_server.c 还有头文件的包含,函数的定义;另外再编写一个http_server.h文件,包含宏定义,结构体定义,函数定义;在下面贴出这两个文件的源码;
上面基本包括了几个重要的函数,当然还有其他的函数,包括发送函数等等,这些函数可以看源代码的注释来理解
文件源码
- http_server.c
#include "lwip/debug.h"#include "lwip/stats.h"#include "lwip/tcp.h"#include "http_server.h"#include "fs.h"#include <string.h>#include <stdio.h>#include <stdlib.h>/*********************************************************************************************************** LOCAL TABLES**********************************************************************************************************/static err_t http_server_accept(void *arg, struct tcp_pcb *pcb, err_t err);static err_t http_server_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *tcp_recv_pbuf, err_t err);static err_t http_server_poll(void *arg, struct tcp_pcb *pcb);static err_t http_init_file(struct http_state *hs, struct fs_file *file, int is_09, const char *uri);static err_t http_find_file(struct http_state *hs, const char *uri, int is_09);static err_t http_parse_request(struct pbuf **inp, struct http_state *hs, struct tcp_pcb *pcb);static u8_t http_send_data(struct tcp_pcb *pcb, struct http_state *hs);static err_t http_sent(void *arg, struct tcp_pcb *pcb, u16_t len);static void close_conn(struct tcp_pcb *pcb, struct http_state *hs);/*********************************************************************************************************** LOCAL FUNCTION PROTOTYPES**********************************************************************************************************//*** * 函数名称 : Http_Server_Init(); * * 函数描述 : web服务器初始化; * * 传递值 : 无; * * 返回值 : 无; * **/void Http_Server_Init(void){ struct tcp_pcb *http_server_pcb; /* 为web服务器分配一个tcp_pcb结构体 */ http_server_pcb = tcp_new(); /* 绑定本地端号和IP地址 */ tcp_bind(http_server_pcb, IP_ADDR_ANY, 80); /* 监听之前创建的结构体http_server_pcb */ http_server_pcb = tcp_listen(http_server_pcb); /* 初始化结构体接收回调函数 */ tcp_accept(http_server_pcb, http_server_accept);}/*** * 函数名称 : http_server_accept(); * * 函数描述 : lwip数据接收回调函数,包含对tcp连接的确认,接收回调函数的配置; * * 传递值 : *arg, *pcb, err ; * * 返回值 : ERR_OK 无错误; * **/static err_t http_server_accept(void *arg, struct tcp_pcb *pcb, err_t err){ struct http_state *hs; /* 分配内存空间 */ hs = (struct http_state *)mem_malloc(sizeof(struct http_state)); if (hs != NULL) { memset(hs, 0, sizeof(struct http_state)); } /* 确认监听和连接 */ tcp_arg(pcb, hs); /* 配置接收回调函数 */ tcp_recv(pcb, http_server_recv); /* 配置轮询回调函数 */ tcp_poll(pcb, http_server_poll, 4); /* 配置发送回调函数 */ tcp_sent(pcb, http_sent); return ERR_OK;}/*** * 函数名称 : http_server_recv(); * * 函数描述 : 接受到数据后,根据接收到数据的内容,返回网页; * * 传递值 : *arg, *pcb, *http_recv_pbuf, err; * * 返回值 : ERR_OK无错误; * **/static err_t http_server_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *http_recv_pbuf, err_t err){ err_t parsed = ERR_ABRT; struct http_state *hs = (struct http_state *)arg; /* 告诉tcp已经接收到数据 */ tcp_recved(pcb, http_recv_pbuf->tot_len); if (hs->handle == NULL) { /* 解析接收到的浏览器请求数据 */ parsed = http_parse_request(&http_recv_pbuf, hs, pcb); } /* 清空请求字符串 */ if (parsed != ERR_INPROGRESS) { if (hs->req != NULL) { pbuf_free(hs->req); hs->req = NULL; } } if (parsed == ERR_OK) { /* 发送网页数据 */ http_send_data(pcb, hs); } else if (parsed == ERR_ARG) { /* 关闭连接 */ close_conn(pcb, hs); } return ERR_OK;}/*** * 函数名称 : http_server_poll(); * * 函数描述 : 轮询函数; * * 传递值 : *arg, *pcb; * * 返回值 : ERR_OK无错误; * **/static err_t http_server_poll(void *arg, struct tcp_pcb *pcb){ struct http_state *hs = arg; if (hs == NULL) { close_conn(pcb, hs); return ERR_OK; } else { hs->retries++; if (hs->retries == 4) { close_conn(pcb, hs); return ERR_OK; } /* 如果连接存在打开的文件,则将会发送剩下的数据; * 如果一直没有收到GET请求,那么连接将会立刻关闭 */ if (hs && (hs->handle)) { if (http_send_data(pcb, hs)) { tcp_output(pcb); } } } return ERR_OK;}/*** * 函数名称 : http_parse_request(); * * 函数描述 : 对接收到的数据进行解析,根据不同的浏览器请求,返回对应的网页数据; * * 传递值 : **inp, *hs, *pcb; * * 返回值 : ERR_OK无错误; * **/static err_t http_parse_request(struct pbuf **inp, struct http_state *hs, struct tcp_pcb *pcb){ char *data; char *crlf; u16_t data_len; struct pbuf *p = *inp; char *sp1, *sp2; u16_t uri_len; char *uri; /* 排列字符串 */ if (hs->req == NULL) { hs->req = p; } else { /* 将多次的请求字符串进行连接排序 */ pbuf_cat(hs->req, p); } /* 拷贝输入数据 */ if (hs->req->next != NULL) { data_len = hs->req->tot_len; pbuf_copy_partial(hs->req, data, data_len, 0); } else { data = (char *)p->payload; data_len = p->len; } /* 提取接收到的浏览器字符串,浏览器请求示例:"GET / HTTP/1.1" */ if (data_len > 7) { crlf = strstr(data, "\r\n"); if (crlf != NULL) { /* 比较前4个字符是否为 "GET " */ if (strncmp(data, "GET ", 4) == 0) { /* sp1指向字符串 "/ HTTP/1.1" */ sp1 = (data + 4); } /* 在sp1字符串中寻找字符" ",sp2指向字符串 " HTTP/1.1" */ sp2 = strstr(sp1, " "); /* uri_len获取sp1字符串首地址到sp2字符串首地址的长度 */ uri_len = sp2 - (sp1); if ((sp2 != 0) && (sp2 >= (sp1))) { /* 将解析的字符串赋给uri,并在最后加上结束符\0, uri指向字符串 "/\0" */ uri = sp1; *(sp1 - 1) = 0; uri[uri_len] = 0; /* 根据字符串寻找对应网页数据 */ return http_find_file(hs, uri, 0); } } } return ERR_OK;}/*** * 函数名称 : http_find_file(); * * 函数描述 : 对提取的数据进行判断,读取对应的网页数据; * * 传递值 : *hs, *uri, is_09; * * 返回值 : ERR_OK无错误; * **/static err_t http_find_file(struct http_state *hs, const char *uri, int is_09){ struct fs_file *file = NULL; /* 如果字符串为 "/\0",则打开index网页 */ if((uri[0] == '/') && (uri[1] == 0)) { file = fs_open("/index.html"); uri = "/index.html"; } else { /* 如果为其他请求,则打开相应网页 */ file = fs_open(uri); } /* 将网页文件数据赋值给http_state结构体,之后发送出去 */ return http_init_file(hs, file, is_09, uri);}/*** * 函数名称 : http_init_file(); * * 函数描述 : 将要发送的数据保存到http_state结构体当中; * * 传递值 : *hs, *file, is_09, *uri; * * 返回值 : ERR_OK无错误; * **/static err_t http_init_file(struct http_state *hs, struct fs_file *file, int is_09, const char *uri){ if (file != NULL) { hs->handle = file; /* 将网页数据赋值给http_state */ hs->file = (char*)file->data; /* 将网页长度赋值给http_state */ hs->left = file->len; hs->retries = 0; } else { hs->handle = NULL; hs->file = NULL; hs->left = 0; hs->retries = 0; } return ERR_OK;}/*** * 函数名称 : http_send_data(); * * 函数描述 : 数据发送函数; * * 传递值 : *pcb, *hs; * * 返回值 : ERR_OK无错误; * **/static u8_t http_send_data(struct tcp_pcb *pcb, struct http_state *hs){ err_t err = ERR_OK; u16_t len; u8_t data_to_send = 0; /* 配置发送数据长度,如果发送数据过长则分批发送 */ if (tcp_sndbuf(pcb) < hs->left) { len = tcp_sndbuf(pcb); } else { len = (u16_t)hs->left; } /* 发送网页数据 */ err = tcp_write(pcb, hs->file, len, 1); if (err == ERR_OK) { data_to_send = 1; hs->file += len; hs->left -= len; } if ((hs->left == 0) && (fs_bytes_left(hs->handle) <= 0)) { /* 关闭连接 */ close_conn(pcb, hs); return 0; } return data_to_send;}/*** * 函数名称 : http_sent(); * * 函数描述 : 数据已经被发送,并且被远程主机确定; * * 传递值 : *arg, *pcb, len; * * 返回值 : ERR_OK无错误; * **/static err_t http_sent(void *arg, struct tcp_pcb *pcb, u16_t len){ struct http_state *hs = (struct http_state *)arg; if (hs == NULL) { return ERR_OK; } hs->retries = 0; http_send_data(pcb, hs); return ERR_OK;}/*** * 函数名称 : close_conn(); * * 函数描述 : 关闭tcp连接; * * 传递值 : *pcb, *hs; * * 返回值 : 无; * **/static void close_conn(struct tcp_pcb *pcb, struct http_state *hs){ tcp_arg(pcb, NULL); tcp_recv(pcb, NULL); tcp_err(pcb, NULL); tcp_poll(pcb, NULL, 0); tcp_sent(pcb, NULL); if (hs != NULL) { if(hs->handle) { fs_close(hs->handle); hs->handle = NULL; } mem_free(hs); } tcp_close(pcb);}
- http_server.h
#ifndef HTTP_SERVER_H#define HTTP_SERVER_H/*********************************************************************************************************** INCLUDE FILES**********************************************************************************************************//*********************************************************************************************************** CONSTANTS**********************************************************************************************************//*********************************************************************************************************** PERIPH DEFINES**********************************************************************************************************//*********************************************************************************************************** DATA TYPES**********************************************************************************************************//*********************************************************************************************************** GLOBAL VARIABLES**********************************************************************************************************/struct http_state { struct fs_file *handle; char *file; /* Pointer to first unsent byte in buf. */#if 1 struct pbuf *req;#endif /* LWIP_HTTPD_SUPPORT_REQUESTLIST */#if 1 char *buf; /* File read buffer. */ int buf_len; /* Size of file read buffer, buf. */#endif /* LWIP_HTTPD_SSI || LWIP_HTTPD_DYNAMIC_HEADERS */ u32_t left; /* Number of unsent bytes in buf. */ u8_t retries;#if 0 const char *parsed; /* Pointer to the first unparsed byte in buf. */#if 1 const char *tag_started;/* Poitner to the first opening '<' of the tag. */#endif /* !LWIP_HTTPD_SSI_INCLUDE_TAG */ const char *tag_end; /* Pointer to char after the closing '>' of the tag. */ u32_t parse_left; /* Number of unparsed bytes in buf. */ u16_t tag_index; /* Counter used by tag parsing state machine */ u16_t tag_insert_len; /* Length of insert in string tag_insert */#if 0 u16_t tag_part; /* Counter passed to and changed by tag insertion function to insert multiple times */#endif /* LWIP_HTTPD_SSI_MULTIPART */ u8_t tag_check; /* true if we are processing a .shtml file else false */ u8_t tag_name_len; /* Length of the tag name in string tag_name */ char tag_name[LWIP_HTTPD_MAX_TAG_NAME_LEN + 1]; /* Last tag name extracted */ char tag_insert[LWIP_HTTPD_MAX_TAG_INSERT_LEN + 1]; /* Insert string for tag_name */ enum tag_check_state tag_state; /* State of the tag processor */#endif /* LWIP_HTTPD_SSI */#if 0 char *params[LWIP_HTTPD_MAX_CGI_PARAMETERS]; /* Params extracted from the request URI */ char *param_vals[LWIP_HTTPD_MAX_CGI_PARAMETERS]; /* Values for each extracted param */#endif /* LWIP_HTTPD_CGI */#if 0 const char *hdrs[NUM_FILE_HDR_STRINGS]; /* HTTP headers to be sent. */ u16_t hdr_pos; /* The position of the first unsent header byte in the current string */ u16_t hdr_index; /* The index of the hdr string currently being sent. */#endif /* LWIP_HTTPD_DYNAMIC_HEADERS */#if 0 u32_t time_started;#endif /* LWIP_HTTPD_TIMING */#if 0 u32_t post_content_len_left;#if 0 u32_t unrecved_bytes; struct tcp_pcb *pcb; u8_t no_auto_wnd;#endif /* LWIP_HTTPD_POST_MANUAL_WND */#endif /* LWIP_HTTPD_SUPPORT_POST*/};/*********************************************************************************************************** MACRO'S**********************************************************************************************************//*********************************************************************************************************** FUNCTION PROTOTYPES**********************************************************************************************************/void Http_Server_Init(void);/********************************************************************************************************** MODULE END**********************************************************************************************************/#endif /* HTTP_SERVER_H */
官方部分函数解析
读取网页数据文件 fs.c (路径:Project->Standalone->web_server->http):
struct fs_file *fs_open(const char *name){ struct fs_file *file; const struct fsdata_file *f; /* 分配空间 */ file = fs_malloc(); if(file == NULL) { return NULL; } for(f = FS_ROOT; f != NULL; f = f->next) { /* 循环比较,如果输入的请求与网页的头数据一致,则返回该网页数据 */ if (!strcmp(name, (char *)f->name)) { file->data = (const char *)f->data; file->len = f->len; file->index = f->len; file->pextension = NULL; file->http_header_included = f->http_header_included; return file; } } fs_free(file); return NULL;}
这里关注 f 变量的结构体 fsdata_file,定义在 fsdata.h:
struct fsdata_file { const struct fsdata_file *next; const unsigned char *name; const unsigned char *data; int len; u8_t http_header_included;};
结构体中有三个重要的变量*next、*name、*data
而在文件fsdata.c中,拉到最后,发现有几个 fsdata_file 的结构体变量,取其中一个来解析一下:
const struct fsdata_file file__index_html[] = { { /* 变量*next,指向下一个要循环比较的数据 */ file__404_html, /* 变量*name,指向数据数组 data__index_html[] */ data__index_html, /* 变量*data,指向数据数组 data__index_html[]12个之后的数据 */ data__index_html + 12, /* 网页数据长度 */ sizeof(data__index_html) - 12, 1, }};
- 变量*name指向的数组前12个数据是字符串 “/index.html” 的ascii码,用于与输入的浏览器请求 “GET /index.html HTTP/1.1” 进行对比;
- 而*data指向数组的12个后的数据,便是网页源代码的16进制数据,这些数据将会由发送函数发送给浏览器,使浏览器显示网页;
web服务器测试
将工程编译后,烧进stm32,将网线与pc机连接:
- 打开浏览器(谷歌浏览器测试)
- 输入服务器ip(这里搭建的服务器ip:192.168.0.10),Enter;
- 浏览器会显示网页,点击网页上的按钮即可以切换不同的网页
如图:
总结:从上面的一系列过程可以get到搭建web服务器的核心思想,然而,现在并没有加入ssi和cgi,所以还无法用网页控制stm32,后面会加上ssi、cgi、post与get请求来完善整个web服务器;
ps:有部分细节的地方解析的不是很清楚,而且自己也没有想明白,需要再加把劲看一些其他的资料来填补空白,共勉~
- STM32移植lwip之建立web服务器
- STM32移植lwip之建立tcp服务器
- STM32移植lwip之建立tcp客户端
- STM32移植lwip之硬件连接
- STM32移植lwip之官方工程修改
- STM32移植lwip之官方源码解析
- LWIP 移植 stm32 注意事项
- stm32上基于LwIP移植LibArtnet
- stm32移植lwip时遇到的问题。
- STM32移植LWIP验证DNS功能
- lwip之http服务器
- Uip在STM32平台移植之建立UDP链接
- LwIP学习笔记——STM32 ENC28J60移植与入门
- LwIP在stm32上的无操作系统移植
- LwIP学习笔记——STM32 ENC28J60移植与入门
- STM32移植LWIP网线热插入网络不通的解决办法
- lwip 移植
- lwip 移植
- PAT B1034
- 浅谈stm32 pwm
- 增删改查
- AE中加载个人数据库GDB方法探讨
- PAT B1036
- STM32移植lwip之建立web服务器
- Servlet URL映射中/和/*的区别
- 有return的情况下try catch finally的执行顺序
- Shell--对字符串的操作
- jquery ajax 实现批量删除
- PAT B1037
- php实现无限极分类
- PAT B1038
- ScrollView嵌套LinearLayout设置android:layout_height="match_parent"没效果解决