实验介绍

最近做了一个小实验,在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, WebSocketClientclass TestClient(WebSocketClient):def __init__(self, conn):super().__init__(conn)def process(self):try:msg = self.connection.read()if not msg:returnmsg = 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:291throw er; // Unhandled 'error' event^Error: Unexpected server response: 200at ClientRequest.<anonymous> (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.<anonymous> (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协议解析

协议详解:

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 = Noneself._clients = []self._max_connections = max_connectionsself._page = pagedef _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 reachedcl.setblocking(True)cl.sendall("HTTP/1.1 503 Too many connections\n\n")cl.sendall("\n")#TODO: Make sure the data is sent before closingsleep(0.1)cl.close()returntry:websocket_helper.server_handshake(cl)except OSError:# Not a websocket connection, serve webpageself._serve_page(cl)returnself._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 stringswith open(self._page, 'r') as f:for line in f:sock.sendall(line)except OSError:# Error while serving webpagepasssock.close()def stop(self):if self._listen_s:self._listen_s.close()self._listen_s = Nonefor 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 systry:import ubinascii as binascii
except:import binascii
try:import uhashlib as hashlib
except:import hashlibDEBUG = 0def server_handshake(sock):clr = sock.makefile("rwb", 0)l = clr.readline()# sys.stdout.write(repr(l))webkey = Nonewhile 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 = vif 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和板子固件不匹配的原因。

经验教训

就一条,时刻关注版本不兼容导致的种种问题。这类问题通常难以察觉其根本原因,因为它表现出来的是各种各样的其他错误,但是定位错误却很难。所以有的时候当你已经排查完所有可能的错误后,不妨升级一下对应的软硬件版本,也许问题就会迎刃而解。

【esp8266】【Node.js】【Websocket】esp8266和Node.js通过websocket进行通信,实验记录及踩坑记录相关推荐

  1. js vue 设置excel单元格样式_vue项目使用xlsx-style实现前端导出Excel样式修改(添加标题,边框等),并且上传npm踩坑记录...

    前段时间,我们项目提出一个前端导出Excel表格的需求, 这个很简单,利用xlsx,file-saver很容易实现(网上很多教程). 后来需要加入标题,标题居中显示,加入边框等等样式需求,这就给我很多 ...

  2. 安装robot.js踩坑记录【含出坑指南】

    最近在写一个node的小项目,要用到模拟鼠标键盘的操作,于是找到了robot.js. 安装库的时候遇到了一些问题,由于官方文档里并没有详细指引,故自行解决后记录下来,希望能帮助到别人. 1. robo ...

  3. node 对接微信支付的踩坑记录(服务端)

    因项目需要,对接了微信支付,微信支付对于网页来说没有什么工作量,申请了商户号后,直接将收款码放到网页上就可以,但是小程序需要调起微信支付直接付款,于是认真翻阅了官网要针对小程序做微信支付的对接. 准备 ...

  4. node link 踩坑记录

    问题说明:使用npm link 之后.依然无法在其他地方使用 背景描述: 想在本地开发一款cli 工具,使用npm link 之后,一直无法在本地执行,都显示 command not found. 一 ...

  5. js控制浏览器全屏踩坑记录

    需求 点击以下按钮后,页面进入全屏. 进入全屏后,按钮样式改变为 恢复非全屏时,按钮自动恢复. 难点 需求很简单,但是实际上手发现有几个难点: 1.浏览器在全屏状态下按F11和Esc可以退出全屏,且全 ...

  6. Three.js《踩坑日记1》

    Three.js可以说是未来物联网无人工厂的网上实体店,很多线下的实体店都通通的搬到了线上.可是对于很多商家看着数据实不如看着 门店来的自然.有了需求自然就会有供给这两个兄弟,所以现在太多太多可视化数 ...

  7. 哪个websocket库与Node.js一起使用? [关闭]

    本文翻译自:Which websocket library to use with Node.js? [closed] Currently there is a plethora of websock ...

  8. node js 非阻塞io_Node Express JS:套接字IO模块示例

    node js 非阻塞io Before reading post, please go through my previous posts: "Express JS WebApplicat ...

  9. 视频教程-Node.JS - socket.io教程-Node.js

    Node.JS - socket.io教程 全栈开发工程师,现职于北京一家学院的全栈教学主任. 8年前端开发经验.4年移动端开发经验.4年UI设计经验.3年一线教学经验. 精通Node.JS.PHP. ...

  10. node.js的开发流程_Node.js子流程:您需要了解的一切

    node.js的开发流程 by Samer Buna 通过Samer Buna Node.js子流程:您需要了解的一切 (Node.js Child Processes: Everything you ...

最新文章

  1. 16.matlab并行处理,调用CPU得多核
  2. 在python子程序中、使用关键字_Python 的控制和函数
  3. 一条语句获得一年所有月份
  4. java乱码怎么解决_如何解决java乱码
  5. 异形3×3魔方还原教程_【理论篇】三阶魔方4.33千亿亿种变化是怎么计算出来的?...
  6. java 生成bat_java实现生成windows可执行的批处理文件(.bat)
  7. python设置excel的格式_python使用xlrd与xlwt对excel的读写和格式设定
  8. CYQ.Data.Orm.DBFast 新增类介绍(含类的源码及新版本配置工具源码)
  9. 彻底解决 Jenkins Slaver 节点无法执行 Git-LFS 命令
  10. linux 重定向_Linux视频教程分享,零基础在家你也可以学的会
  11. RecyclerView.Adapter的封装(RecyclerAdapter)
  12. android减少动态效果,【技巧】手机运行变慢?试试这些办法!
  13. java 104规约_电网104规约解包(java)
  14. linux之调试触摸屏驱动
  15. html网页设计大赛作品介绍,html简单网页设计作品
  16. 蚂蜂窝VS穷游最世界-自由行类App分析
  17. 一个有效的面试——善用STAR法则
  18. python正整数平方根_Python3算法之四:x的平方根
  19. 3-Coloring(奇偶涂色)
  20. 使用BIOS进行键盘输入和磁盘读写

热门文章

  1. ROS机器人更换新雷达需要重新配置carto和navigation的哪些参数
  2. 计算机基础和Linux安装
  3. 带有vlan tag的报文与网卡的交互关系
  4. excel文件因服务器未响应无法打开,excel打开显示兼容模式(未响应)
  5. 光纤节点 劫持检测,细数宽带运营商常见的几种http劫持行为
  6. Ubuntu 20.04 创建桌面快捷方式
  7. golang 微信小程序登录
  8. python自动语音电话_用 Python 实现自己的智能语音助理(百度语音 + 图灵机器人)...
  9. Android TextView水平跑马灯
  10. c语言零基础自学软件下载,C语言入门学习下载-C语言入门学习app下载v1.0-52PK下载中心...