最近做了一个小实验,在esp8266上连接了一些外设,构建了一个websocket server,用的是micropython编写程序;在pc上写了原生js,构建了一个websocket client。
esp8266用的是sta模式,与pc连接到同一个WiFi,服务器和客户端在同一局域网内,用彼此的ip地址进行通信。采用的是websocket协议,esp8266使用的是
https://github.com/BetaRavener/upy-websocket-server
这个开源项目中的ws_server.py,ws_multiserver.py,ws_connection.py三个头文件,作为webscoket库,示例的demo如下:
from ws_connection import ClientClosedError
from ws_server import WebSocketServer, WebSocketClient
class TestClient(WebSocketClient):
def __init__(self, conn):
super().__init__(conn)
def process(self):
try:
msg = self.connection.read()
if not msg:
return
msg = msg.decode("utf-8")
items = msg.split(" ")
cmd = items[0]
if cmd == "Hello":
self.connection.write(cmd + " World")
print("Hello World")
except ClientClosedError:
self.connection.close()
class TestServer(WebSocketServer):
def __init__(self):
super().__init__("test.html", 2)
def _make_client(self, conn):
return TestClient(conn)
server = TestServer()
server.start()
try:
while True:
server.process_all()
except KeyboardInterrupt:
pass
server.stop()
pc程序使用的是ws库作为websocket库。首先要安装ws模块,使用npm安装的命令如下:
npm install -g ws
其中-g参数指的是全局安装,如果只需要当前项目安装也可以把-g参数删去。
安装好ws模块后,就可以引入ws模块并编写一个很简单的websocket_client.js。示例代码如下:
import WebSocket from 'ws';
try {
const ws = new WebSocket("ws://192.168.1.111:80");
ws.on('open', function open() {
console.log("open");
ws.send('Hello');
}); //在连接创建完成后发送一条信息
ws.on('message', function incoming(data) {
console.log(data);
}); //当收到消息时,在控制台打印出来
} catch (e) {
console.log(e.name + ": " + e.message);
console.log(e);
}
然后分别运行它们。esp8266我使用ide是Thonny,编写好程序后,启动运行,使得websocket server运行起来,并在Thonny的控制台打印出esp8266的局域网中的ip地址,例如打印出:
Server run on ws://192.168.1.111:80
那么这个 ws://192.168.1.111:80就是websocket server的ip地址,client去连接这个地址就能建立连接。
js的client使用node运行,命令如下:
node websocket_client.js
如果连接成功,esp8266会在Thonny的终端打印出:Hello World!
这个很简单的实验,让我搞了两天,为什么呢?因为出现了一个bug,那就是js编写的client的申请连接的socket可以被esp8266 listen到,但是在websocket handshake阶段却会发生错误,然后终止。
在client端打印出的错误为:
node test.js
events.js:291
throw er; // Unhandled 'error' event
^
Error: Unexpected server response: 200
at ClientRequest. (D:\Personal Data\ProjectXXX\node_modules\ws\lib\websocket.js:604:7)
at ClientRequest.emit (events.js:314:20)
at ClientRequest.EventEmitter.emit (domain.js:483:12)
at HTTPParser.parserOnIncomingClient [as onIncoming] (_http_client.js:602:27)
at HTTPParser.parserOnHeadersComplete (_http_common.js:122:17)
at Socket.socketOnData (_http_client.js:475:22)
at Socket.emit (events.js:314:20)
at Socket.EventEmitter.emit (domain.js:483:12)
at addChunk (_stream_readable.js:298:12)
at readableAddChunk (_stream_readable.js:273:9)
Emitted 'error' event on WebSocket instance at:
at abortHandshake (D:\Personal Data\ProjectXXX\node_modules\ws\lib\websocket.js:731:15)
at ClientRequest. (D:\Personal Data\ProjectXXX\node_modules\ws\lib\websocket.js:604:7)
[... lines matching original stack trace ...]
at addChunk (_stream_readable.js:298:12)
这个错误让我百思不解,在网上找了一下,遇到这个问题的人很少,有人说可能是端口问题,80端口有可能被限制访问,把esp8266 server开放的端口从默认的80端口改为8080,90等等其他端口进行尝试。
这个方法对我不适用,改了端口后还是报这个错。最后我把问题解决了,但是在揭晓真正的问题之前,我想要记录一下我找问题的过程。
由于改端口没用,唯一的方法就是从代码本身出发。既然错误在handshake部分,所以我决定看一下micropython使用的websocket库中关于handshake的部分。关于websocket的handshake,我们先复习一下websocket协议。
首先附上Micropython源码:
https://github.com/micropython/micropython
协议详解:
WebSocket协议:5分钟从入门到精通 - 程序猿小卡 - 博客园 (cnblogs.com)
websocket协议详解及报文分析_海渊_haiyuan的博客-CSDN博客
首先,我的esp8266板子里的代码结构是这样的:
boot.py
main.py
ws_server.py
ws_multiserver.py
ws_connection.py
其中main.py入口程序,就是上面说的demo程序。它使用了ws_server模块和ws_connection模块,在mian.py中继承了父类WebSocketServer,实例化了TestServer。
所以我们去看ws_server.py中的Class WebSocketServer,它长这样:
import os
import socket
import network
import websocket_helper
from time import sleep
from ws_connection import WebSocketConnection, ClientClosedError
# 省略WebSocketClient代码,只看WebSocketServer类的实现
class WebSocketServer:
def __init__(self, page, max_connections=1):
self._listen_s = None
self._clients = []
self._max_connections = max_connections
self._page = page
def _setup_conn(self, port, accept_handler):
self._listen_s = socket.socket()
self._listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ai = socket.getaddrinfo("0.0.0.0", port)
addr = ai[0][4]
self._listen_s.bind(addr)
self._listen_s.listen(1)
if accept_handler:
self._listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
for i in (network.AP_IF, network.STA_IF):
iface = network.WLAN(i)
if iface.active():
print("WebSocket started on ws://%s:%d" % (iface.ifconfig()[0], port))
def _accept_conn(self, listen_sock):
cl, remote_addr = listen_sock.accept()
print("Client connection from:", remote_addr)
if len(self._clients) >= self._max_connections:
# Maximum connections limit reached
cl.setblocking(True)
cl.sendall("HTTP/1.1 503 Too many connections\n\n")
cl.sendall("\n")
#TODO: Make sure the data is sent before closing
sleep(0.1)
cl.close()
return
try:
websocket_helper.server_handshake(cl)
except OSError:
# Not a websocket connection, serve webpage
self._serve_page(cl)
return
self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection)))
def _make_client(self, conn):
return WebSocketClient(conn)
def _serve_page(self, sock):
try:
sock.sendall('HTTP/1.1 200 OK\nConnection: close\nServer: WebSocket Server\nContent-Type: text/html\n')
length = os.stat(self._page)[6]
sock.sendall('Content-Length: {}\n\n'.format(length))
# Process page by lines to avoid large strings
with open(self._page, 'r') as f:
for line in f:
sock.sendall(line)
except OSError:
# Error while serving webpage
pass
sock.close()
def stop(self):
if self._listen_s:
self._listen_s.close()
self._listen_s = None
for client in self._clients:
client.connection.close()
print("Stopped WebSocket server.")
def start(self, port=80):
if self._listen_s:
self.stop()
self._setup_conn(port, self._accept_conn)
print("Started WebSocket server.")
def process_all(self):
for client in self._clients:
client.process()
def remove_connection(self, conn):
for client in self._clients:
if client.connection is conn:
self._clients.remove(client)
return
握手的代码在_accept_conn(self, listen_sock)函数中:websocket_helper.server_handshake(cl)这一句。它调用了websocket_helper的握手函数。这个websocket_helper模块来自Micropython原生封装好的库
https://github.com/micropython/micropython/blob/4d9e657f0e/extmod/webrepl/websocket_helper.py
它长这样:
try:
import usys as sys
except ImportError:
import sys
try:
import ubinascii as binascii
except:
import binascii
try:
import uhashlib as hashlib
except:
import hashlib
DEBUG = 0
def server_handshake(sock):
clr = sock.makefile("rwb", 0)
l = clr.readline()
# sys.stdout.write(repr(l))
webkey = None
while 1:
l = clr.readline()
if not l:
raise OSError("EOF in headers")
if l == b"\r\n":
break
# sys.stdout.write(l)
h, v = [x.strip() for x in l.split(b":", 1)]
if DEBUG:
print((h, v))
if h == b"Sec-WebSocket-Key":
webkey = v
if not webkey:
raise OSError("Not a websocket request")
if DEBUG:
print("Sec-WebSocket-Key:", webkey, len(webkey))
d = hashlib.sha1(webkey)
d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
respkey = d.digest()
respkey = binascii.b2a_base64(respkey)[:-1]
if DEBUG:
print("respkey:", respkey)
sock.send(
b"""\
HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: """
)
sock.send(respkey)
sock.send("\r\n\r\n")
# Very simplified client handshake, works for MicroPython's
# websocket server implementation, but probably not for other
# servers.
def client_handshake(sock):
cl = sock.makefile("rwb", 0)
cl.write(
b"""\
GET / HTTP/1.1\r
Host: echo.websocket.org\r
Connection: Upgrade\r
Upgrade: websocket\r
Sec-WebSocket-Key: foo\r
\r
"""
)
l = cl.readline()
# print(l)
while 1:
l = cl.readline()
if l == b"\r\n":
break
# sys.stdout.write(l)
© 2021 GitHub, Inc.
我们把这个文件下载下来,放到8266上,用一个新的名字比如websocket_helper_new.py,然后在用到它的地方把头文件改成
import websocket_helper_new as websocket_helper
再把websocket_helper_new.py文件中的DEBUG从0改成1,这样就会在握手时打印出相关的信息。
我打印出信息发现,它缺少了Sec-WebSocket-Key这个字段及后面的字段,它只有下面这部分
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
这说明server解析client的协议升级请求时就出错了,因为握手不成功。可是为什么协议会只读到了前面的,后面的却缺失了呢?是client在发送的时候就发送的不完整么?
为了验证,我用wireshark抓包,设置捕获过滤器的过滤条件为:
dst net 192.168.1.111
它的意思是,只捕捉destination ip为192.168.1.111(即ws server)的数据包。抓包后发现,client发送的协议升级请求包是完整的,然而8266接收到的socket是不完整的。
这时候我真的没办法了,于是我把js代码改成了ts代码,用ts-node运行,同样的错误;改成html文件内嵌js,在浏览器运行,chrome和edge都可以。这下就奇怪了。同样的js怎么我用node运行就不行,用浏览器就可以?
我去翻micropython源码,以为是它的解包部分有问题,但是也没有问题。最后我真的一筹莫展了。
在做这个小实验的同时,我还在做一个vue的项目。在安装那个dev依赖包的时候,其中一个包提示我我的node engine版本太低,必须要13.0以上的node才可以使用那个包。我突然想到,既然浏览器可以跑js,node不可以,会不会也是我的node引擎版本问题,和当前esp8266上烧的固件不匹配?
于是我去官网下载了最新的14.17.1,然后重新运行原来的js client代码,发现!成功了!所以其实卡了我两天的问题,是node版本的问题。
经常搞板子的朋友可能会遇到过一些库在某些版本的固件上没法运行的情况,这就说明了可能不同版本的固件能够支持的库有一些细微的差别。同时,在客户端这边,为什么我一开始没有想到是node的问题,是因为我跑两个node,一个server一个client,能够成功;跑一个node server,一个浏览器client,能够成功;一个浏览器client,一个8266 server,能够成功;唯独跑一个node client,一个8266 server失败了,所以我没想过是我本地node和板子固件不匹配的原因。
就一条,时刻关注版本不兼容导致的种种问题。这类问题通常难以察觉其根本原因,因为它表现出来的是各种各样的其他错误,但是定位错误却很难。所以有的时候当你已经排查完所有可能的错误后,不妨升级一下对应的软硬件版本,也许问题就会迎刃而解。