Черновик асинхронного GET/POST для micropython
Это скорее черновик, если кратко, имеем ESP32-S, PSRAM нет, прошита микропитоном esp32-idf3-20200902-v1.13.bin.
Появилась необходимость использовать методы GET и POST для отправки/получения данных на сервер асинхронно вместе с другими циклами, да ещё с ssl.
И при этом контролировать таймауты в зависимости от важности того или иного подключения.
Готовые варианты или не помещались в память, или как uaiohttpclient.py не поддерживали https.
В итоге получилась примерно такая (как-бы асинхронная) зарисовка на салфетке:
ahttp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | # async http import uasyncio as asyncio import network import ping import time try: import usocket as socket except: import socket station = network.WLAN(network.STA_IF) async def _as_write_get(data, sock): try: n = sock.write(data) except OSError: pass async def _as_write_post(data, pdata, sock): try: pp = sock.write(data) time.sleep_ms(50) pp = sock.write(pdata) except OSError: pass async def _as_write_post(data, pdata, sock): try: pp = sock.write(data) time.sleep_ms(50) pp = sock.write(pdata) except OSError: pass async def _as_read(sock): s_response = '' response_lines = 0 while True: response_lines += 1 data = sock.read(256) if data is not None: # Если сервер не закрывает соединение (например, как bash [nc -l -p 80]), то возвращается s_response = 'empty' s_response += str(data.decode(), 'utf8') if response_lines > 3: # Задача была получить первые 3 строки ответа включая заголовки return s_response break else: s_response = 'empty' return s_response break async def ahttp(url, method, _PDATA, t): print("Fetching:", url) proto, _, host, path = url.split('/', 3) # trailing slash are important! google = '8.8.8.8' # Вариант проверки наличия выхода в сеть, блокирующий процесс на время выполения ping, но контролируется таймаутом # Проверка подключения к сети station.isconnected(), следом за ним пинг, или целевого хоста, или google if station.isconnected(): _pong = ping.ping(google, 2, 1000) # host | google if _pong > 0: pass else: as_response = 'ping error' return as_response else: as_response = 'not connected' return as_response # if proto == "http:": port = 80 elif proto == "https:": import ussl port = 443 addr = socket.getaddrinfo(host, port)[0][-1] s = socket.socket() s.setblocking(0) # s.setblocking(False) s.settimeout(t) # s.settimeout(0.0) try: s.connect(addr) if proto == "https:": s = ussl.wrap_socket(s, server_hostname=host) if method == "GET": data = bytes('GET /%s HTTP/1.0\r\nHost: %s\r\nUser-Agent: ESP32\r\nConnection: close\r\n\r\n' % (path, host), 'utf8') await asyncio.wait_for(_as_write_get(data, sock=s), timeout=t) # отправка заголовков GET if method == "POST": pdata = _PDATA data = bytes('POST /%s HTTP/1.0\r\nHost: %s\r\nUser-Agent: ESP32\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: %s\r\n\r\n' % (path, host, str(len(pdata))), 'utf8') await asyncio.wait_for(_as_write_post(data, pdata, sock=s), timeout=t) # отправка заголовков POST и данных pdata await asyncio.sleep_ms(100) # пауза между отправкой в сокет и чтением из него as_response = await asyncio.wait_for(_as_read(s), timeout=t) # чтение заголовков/ответа сервера, прерывается по таймауту t s.close() except OSError: as_response = 'OSError 1' # if server not respond or slower than defined timeout except ValueError: as_response = 'ValueError' except UnicodeError: as_response = 'UnicodeError' return as_response |
ping.py
1 2 3 4 5 6 7 8 | import uping2 def ping(host, c, to): pong = uping2.ping(host, count=c, timeout=to, interval=100, quiet=True) # uping2.ping('8.8.8.8', count=3, timeout=1000, interval=100, quiet=True) # return 3 return pong |
uping2.py отличается от оригинала только строкой:
1 2 3 | # return (n_trans, n_recv) return n_recv |
Проверяем таймауты
main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | async def print_lines(): count = 0 while True: await asyncio.sleep(1) count += 1 print("l", count) async def send_controller(): count = 0 while True: await asyncio.sleep(5) count += 1 print("send", count) # PDATA = '{"msg":"text","body":"status"}' # rr = await asyncio.create_task(ahttp.ahttp('http://192.168.43.194/post', 'POST', PDATA, 3)) # таймаут 15 секунд rr = await asyncio.create_task(ahttp.ahttp('http://192.168.43.194/ts', 'GET', 'null', 15)) print('>>>>>>>>', rr, '<<<<<<<<<<<<<<') asyncio.create_task(print_lines()) # чисто для визуализации интервалов asyncio.create_task(send_controller()) |
Ловим и держим
Простой вариант симуляции, когда сервер отвечает, но не закрывает соединение
1 2 | while true; do ( echo -e 'HTTP/1.1 200 OK\r\n\r\n'; echo -e 'content\r\n'; ) | nc -l -p 80; done |
Лог консоли:
1 2 3 4 5 6 7 8 9 | do 51
do 52
send 9
Fetching: http://192.168.43.194/ts
do 53
// <ожидание ответа, следует сброс после указанного таймаута, (да блокирует)>
do 54
>>>>>>>> OSError 1 <<<<<<<<<<<<<<
do 55
|
Вариант с нормальным закрытием соединения со стороны сервера
1 2 | while true; do ( echo -e 'HTTP/1.1 200 OK\r\n\r\n'; echo -e 'content\r\n'; ) | nc -l -p 80 -q1; done |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | do 266 do 267 do 268 send 50 Fetching: http://192.168.43.194/ts do 269 >>>>>>>> HTTP/1.1 200 OK content <<<<<<<<<<<<<< do 270 do 271 |
Замедляем интернет
esp <-> роутер <-> ноутбук с сервером, поэтому тормозим wlan0 на ноутбуке.
Используем Linux Traffic Control tc
1 2 3 4 5 6 | # установить задержку в 1 секунду tc qdisc add dev wlan0 root netem delay 1000ms # удалить задержку tc qdisc del dev wlan0 root netem delay 1000ms # просмотр tc qdisc show dev wlan0 |
И устанавливаем 5 секунд tc qdisc add dev wlan0 root netem delay 5000ms
Ответ получен:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | do 502
do 503
do 504
do 505
send 97
Fetching: http://192.168.43.194/ts
// <задержка при отправке 5 секунд>
do 506
// <задержка при получении 5 секунд>
do 507
>>>>>>>> HTTP/1.1 200 OK
content
<<<<<<<<<<<<<<
do 508
do 509
do 510
do 511
|
Устанавливаем 20 секунд tc qdisc add dev wlan0 root netem delay 20000ms
1 2 3 4 5 6 7 8 9 10 11 12 13 | do 679
do 680
do 681
do 682
send 129
Fetching: http://192.168.43.194/ts
do 683
// <ожидание отправки 15 секунд и сброс>
>>>>>>>> OSError 1 <<<<<<<<<<<<<<
do 684
do 685
do 686
do 687
|