HTTP 服务器实验
使用树莓派 Pico 和 w5500 模块实现 HTTP 服务器时,要经过以下步骤:
- 建立连接:
Web浏览器(客户端)与Web服务器通过TCP连接建立起一个虚拟的"套接字"。这个套接字就成为了客户端与服务器间的通信通道
- 发送请求:
- 客户端通过这个套接字发送
HTTP请求,通常是GET或POST方法 GET请求用于获取资源,POST请求用于提交数据
- 客户端通过这个套接字发送
- 服务器处理:
- 服务器接收到请求后,会根据请求的
URL查找并读取相应的资源(如HTML文件) - 对于
POST请求,服务器还会解析请求体中包含的表单数据
- 服务器接收到请求后,会根据请求的
- 返回响应:
- 服务器将资源数据以
HTTP响应的形式通过套接字返回给客户端 - 响应包含
HTTP头信息和实际的HTML文件内容
- 服务器将资源数据以
- 关闭连接:
- 在数据交换完成后,客户端和服务器会关闭这个 TCP 连接
以下代码可以在我们提供的资料包中 elegance-devkit v1\Demo\47 ETH_HTTP_Server 文件夹找到。
在以下示例代码中,我们搭建了一个简单的 HTTP 服务器,并通过网页控制开发板上的 LED 灯,代码如下:
Python
# Python env : MicroPython v1.23.0 on Wiznet W5500
# -*- coding: utf-8 -*-
# @Time : 2024/8/8 下午10:33
# @Author : 李清水
# @File : main.py
# @Description : 以太网类实验,进行Http服务器测试
# ======================================== 导入相关模块 ========================================
# 导入硬件相关模块
from machine import Pin,SPI
# 导入时间相关模块
import time
# 导入网络相关模块
import network
# 导入用于创建和管理网络连接的套接字模块
from usocket import socket
# ======================================== 全局变量 ============================================
# 设备IP地址
ip = '192.168.1.20'
sn = '255.255.255.0'
gw = '192.168.1.1'
dns= '8.8.8.8'
# 用于网络连接的元组对象
netinfo=(ip, sn, gw, dns)
# LED灯标志
on_flag = 0
off_flag = 0
blink_flag = 0
# ======================================== 功能函数 ============================================
# 初始化网络连接
def w5x00_init() -> 'network.WIZNET5K':
"""
初始化 network.WIZNET5K 实例,设置静态 IP 地址并进行连接。
Returns:
network.WIZNET5K: 初始化并连接成功的 network.WIZNET5K 实例。
Raises:
None: 该函数不抛出异常。
"""
# 声明全局变量
global netinfo
# 初始化SPI对象
spi = SPI(0, 2_000_000, mosi=Pin(19), miso=Pin(16), sck=Pin(18))
# 实例化network.WIZNET5K类,传入使用的片选引脚CS和复位引脚RST
nic = network.WIZNET5K(spi, Pin(17), Pin(20))
# 激活网络接口
nic.active(True)
# 如果DHCP获取失败,使用静态IP地址配置
nic.ifconfig(netinfo)
# 若是没有连接网络,则循环执行
while not nic.isconnected():
time.sleep(1)
# 输出寄存器信息
print(nic.regs())
# 打印设置的IP地址
print('ip :', nic.ifconfig()[0])
print('sn :', nic.ifconfig()[1])
print('gw :', nic.ifconfig()[2])
print('dns:', nic.ifconfig()[3])
# 返回network.WIZNET5K实例
return nic
def web_page() -> str:
"""
生成网页内容,根据 LED 状态动态显示内容。
Returns:
str: 生成的 HTML 网页内容。
Raises:
None: 该函数不抛出异常。
"""
# 声明全局变量led
global led
# 根据LED的状态标志位生成对应的文本
if on_flag == 1:
led_state = "ON"
elif off_flag == 1:
led_state = "OFF"
else:
led_state = "BLINK"
# 生成HTML网页内容,根据LED状态动态显示内容
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Freak Studio Raspberry Pi Pico Web Server Test </title>
<style>
body {
background-color: #2c3e50;
color: #ecf0f1;
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
h1 {
color: #e74c3c;
font-size: 3em;
}
h2 {
color: #3498db;
font-size: 2em;
}
.button {
background-color: #27ae60;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 12px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #2ecc71;
}
.button2 {
background-color: #e74c3c;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 12px;
transition: background-color 0.3s ease;
}
.button2:hover {
background-color: #c0392b;
}
.button3 {
background-color: #90448e;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 12px;
transition: background-color 0.3s ease;
}
.button3:hover {
background-color: #c0392b;
}
</style>
</head>
<body>
<div>
<H1>Freak Studio Raspberry Pi Pico Web Server Test</H1>
<h2>Control LED</h2>
<p>PICO LED state: <strong>""" + led_state + """</strong></p>
<p><a href="/?led=on"><button class="button">ON</button></a></p>
<p><a href="/?led=off"><button class="button button2">OFF</button></a></p>
<p><a href="/?led=blink"><button class="button button3">BLINK</button></a></p>
</div>
</body>
</html>
"""
# 返回生成的HTML内容
return html
def web_server() -> None:
"""
运行 Web 服务器,处理客户端请求并控制 LED 状态。
Returns:
None: 该函数没有返回值。
Raises:
None: 该函数不抛出异常。
"""
# 声明全局变量ip
global ip
# LED灯状态全局变量
global on_flag, off_flag, blink_flag
# 创建套接字对象
s = socket()
# 绑定IP地址和端口号80
s.bind((ip, 80))
# 监听最多5个客户端连接
s.listen(5)
while True:
# 接受客户端的连接
conn, addr = s.accept()
# 打印客户端的IP地址和端口号
print('Connect from %s' % str(addr))
# 接收客户端的请求数据,最多1024个字节
request = conn.recv(1024)
# 将请求数据转换为字符串
request = str(request)
# 打印接收到的请求内容
print('Content = %s' % request)
# 查找LED打开命令在请求字符串中的位置
led_on = request.find('/?led=on')
# 查找LED关闭命令在请求字符串中的位置
led_off = request.find('/?led=off')
# 查找LED闪烁命令在请求字符串中的位置
led_blink = request.find('/?led=blink')
# 如果找到了打开命令,打开LED
if led_on == 6:
print("LED ON")
led.value(1)
on_flag = 1
off_flag = 0
blink_flag = 0
# 如果找到了关闭命令,关闭LED
if led_off == 6:
print("LED OFF")
led.value(0)
on_flag = 0
off_flag = 1
blink_flag = 0
# 如果找到了闪烁命令,闪烁LED
if led_blink == 6:
print("LED BLINK")
for i in range(3):
led.value(1)
time.sleep(0.2)
led.value(0)
time.sleep(0.2)
on_flag = 0
off_flag = 0
blink_flag = 1
# 生成HTML响应内容
response = web_page()
# 发送HTTP响应头
conn.send('HTTP/1.1 200 OK\n')
conn.send('Connection: close\n')
conn.send('Content-Type: text/html\n')
conn.send('Content-Length: %s\n\n' % len(response))
# 发送HTML内容
conn.send(response)
# 关闭连接
conn.close()
# ======================================== 自定义类 ============================================
# ======================================== 初始化配置 ==========================================
# 延时3s,等待设备上电
time.sleep(3)
# 打印调试信息
print("FreakStudio : Using W5x00 Ethernet to do HTTP Server test")
# 将GPIO25配置为输出引脚,用于控制LED
led = Pin(25, Pin.OUT)
# 初始化w5500模块
nic = w5x00_init()
# ======================================== 主程序 ===========================================
# 运行Web服务器
web_server()
基本工作流程可描述为:
- 初始化网络接口 :使用
w5x00_init()函数初始化网络接口,设置 IP 地址并进行连接; - 创建 Web 服务器 :定义
web_server()函数,创建一个套接字对象并绑定到 IP 地址和端口 80,然后监听最多 5 个客户端连接; - 处理客户端请求 :在
web_server()函数中,接受客户端的连接,接收请求数据,并根据请求内容控制 LED 的状态; - 生成网页内容 :使用
web_page()函数生成网页内容,根据LED状态动态显示内容; - 发送
HTTP响应 :将生成的HTML内容作为HTTP响应发送给客户端; - 关闭连接:完成客户端请求后,关闭与客户端的连接。
在 web_page() 函数中我们生成一个 HTML 网页,允许用户通过点击按钮来控制 LED 的状态。网页提供了三个按钮:ON、OFF 和 BLINK,分别用于控制 LED 的亮灭和闪烁状态,在 HTML 网页内容部分,我们主要做了以下工作:
- 使用多行字符串(
"""...""")来生成网页的 HTML 代码 HTML内容包括基础的网页结构,如<!DOCTYPE html>声明、<html>,<head>和<body>标签- 页面包含了一些
CSS样式,用于美化网页,包括背景色、字体样式、按钮样式等 - 其中有三个主要按钮,每个按钮都是一个
<a>标签,其中包含一个href属性,用于定义点击按钮时发送的请求路径:ON 按钮: 点击后发送/led=on请求路径OFF 按钮: 点击后发送/led=off请求路径BLINK 按钮: 点击后发送/led=blink请求路径
当用户点击这些按钮时,Web 服务器将会收到相应的 HTTP 请求,并根据请求路径对 LED 进行操作,如打开、关闭或让 LED 闪烁。
在 web_server() 函数中,我们主要进行如下操作:
- 创建服务器套接字
- **
s = socket()**:创建一个套接字对象,用于服务器端的网络通信 - **
s.bind((ip, 80))**:绑定 IP 地址和端口号 80,表示服务器将通过这个 IP 地址和端口接受 HTTP 请求 - **
s.listen(5)**:设置服务器最多可以接受 5 个客户端的连接请求
- **
- 进入主循环,等待客户端连接
- **
while True**:服务器进入一个无限循环,持续运行,等待客户端连接 - **
conn, addr = s.accept()**:服务器等待并接受客户端连接,conn代表连接对象,addr是客户端的 IP 地址和端口号 - **
print('Connect from %s' % str(addr))**:打印连接的客户端的 IP 地址和端口号
- **
- 处理客户端请求
- **
request = conn.recv(1024)**:接收客户端发送的 HTTP 请求数据,最多 1024 个字节 - **
request = str(request)**:将请求数据转换为字符串,以便后续处理 - **
print('Content = %s' % request)**:打印接收到的请求内容
- **
- 解析和处理 LED 控制命令
- **
led_on = request.find('/?led=on')**:查找请求字符串中 LED 打开命令的位置 - **
led_off = request.find('/?led=off')**:查找 LED 关闭命令的位置 - **
led_blink = request.find('/?led=blink')**:查找 LED 闪烁命令的位置
- **
根据客户端发送的请求:
- **
LED ON**:- 如果找到了
LED打开命令(led_on == 6),则打开 LED(led.value(1)); - 将
on_flag设为 1,表示 LED 处于打开状态;同时将off_flag和blink_flag设为 0;
- 如果找到了
- **
LED OFF**:- 如果找到了
LED关闭命令(led_off == 6),则关闭 LED(led.value(0)); - 将
off_flag设为 1,表示 LED 处于关闭状态;同时将on_flag和blink_flag设为 0;
- 如果找到了
- **
LED BLINK**:- 如果找到了
LED闪烁命令(led_blink == 6),则使 LED 闪烁三次,每次亮灭间隔 0.2 秒; - 将
blink_flag设为 1,表示 LED 处于闪烁状态;同时将on_flag和off_flag设为 0;
- 如果找到了
- 生成和发送
HTTP响应:- **
response = web_page()**:调用web_page()函数,生成包含LED状态的HTML网页内容; - 发送 HTTP 响应头 :
conn.send('HTTP/1.1 200 OK\n'):发送状态行,表示请求处理成功;conn.send('Connection: close\n'):告知客户端关闭连接;conn.send('Content-Type: text/html\n'):指定响应内容的类型为 HTML;conn.send('Content-Length: %s\n\n' % len(response)):指定响应内容的长度;
- 发送 HTML 内容 :
conn.send(response):发送生成的 HTML 页面;
- **
- 关闭连接:
- **
conn.close()**:关闭与客户端的连接,等待下一个客户端请求;
- **
其中,HTTP 请求包括以下内容:
Python
Content = b'GET /favicon.ico HTTP/1.1\r\n
Host: 192.168.1.20\r\nConnection: keep-alive\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0\r\n
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\n
Referer: http://192.168.1.20/?led=off\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'
各个部分含义如下:
GET /favicon.ico HTTP/1.1:这是一个 HTTP GET 请求方法,用于请求资源,/favicon.ico是请求的资源路径,通常浏览器会请求网站的图标文件以显示在地址栏或标签页标题旁,HTTP/1.1指定了请求使用的 HTTP 协议版本;Host: 192.168.1.20:这一行指定了服务器的域名或 IP 地址,这里是192.168.1.20;Connection: keep-alive:这行指示客户端希望使用持久连接(keep-alive),这样可以在同一个TCP连接上发送多个HTTP请求,减少了建立和关闭连接的开销;User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0:这是用户代理字符串,它告诉服务器发起请求的客户端是什么,这里是一个模拟的 Chrome 浏览器的用户代理字符串;Accept:image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8:这一行指定了客户端能够接收的内容类型,以及它们的优先级。在这个例子中,客户端首选AVIF、WebP、APNG格式的图像,其次是所有类型的图像,但是质量因子为 0.8;Referer: http://192.168.1.20/?led=off:这行指定了请求的引用页面,即用户从哪个页面链接到当前请求的页面;Accept-Encoding:gzip, deflate:这一行告诉服务器客户端支持哪些内容编码,以便服务器可以提供压缩的数据,加快传输速度;Accept-Language:zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6:这一行指定了客户端接受的语言类型,以及它们的优先级。在这个例子中,客户端首选简体中文(zh-CN),其次是其他各种英语变体;
可以看到,我们上面代码中使用如下命令判断是否改变 LED 状态:
Python
# 如果找到了打开命令,打开LED
if led_on == 6:
...
# 如果找到了关闭命令,关闭LED
if led_off == 6:
...
# 如果找到了闪烁命令,闪烁LED
if led_blink == 6:
...
是因为请求字符串中 /?led=on 等表示 LED 状态的子字符串的起始位置索引就是 6。
整体工作流程图如下所示:
烧录程序,打开终端,可以看到输出如下:
在浏览器中输入该地址,显示如下页面:

点击"ON"按钮,可以看到 LED 灯点亮,终端输出如下信息:

点击其他按钮,可以看到都可以正常执行:

需要注意的是,在 **LED 灯闪烁中,我们使用阻塞执行,在 LED灯闪烁完成之前,我们点击所有按钮都不会执行操作。**