• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

Micropython HTTP сервер

evgeny2k

New member
Всем привет. Хочу поделиться с сообществом своей наработкой, а именно модулем HTTPServer. Сервер работает в блокирующем режиме, но для себя использовал и неблокирующую версию. При написании сервера использовал вот эту статью:
https://andreymal.org/socket3
Рекомендую почитать для полного понимания.
Во вложении файл сервера и пример использования. Также дублирую пример здесь. Сервер работает только с get-запросами, но принимает и параметры. Предложения по доработке и добрая критика приветствуются.
Код:
# -*- coding: utf-8 -*-

from HTTPServer import Server
from machine import Pin
import gc

led = Pin(2, Pin.OUT) # Присваиваем переменной led GPIO2 и назначаем его выходом
led.high() # Переводим порт в состояние 1

def test1(path): # Пример функции, принимающей только параметр path..
    print('this is func test1 with path='+path)
    return 'this is func test1 with path='+path

def test2(path,params): # Эта фуекция принимает на вход path и params, где params имеет вид словаря вида {'param_name':'param_value'}
    print('this is func test2 with path='+path+' and params='+str(params))
    return 'this is func test2 with path='+path+' and params='+str(params)

def stop(): # Пример ф-ии без входных параметров. Остановка сервера.
    test.stop = True
    return 'stoped'

def free(): # Пример ф-ии без параметров. Показывает в браузере состояние памяти.
    result = "mem free:"+str(gc.mem_free())+" mem allocated:"+str(gc.mem_alloc())
    return result

def switch(): # Ф-ия без параметров. Просто переключает состояние светодиода на ESP
    led.value(not led.value())
    return 'Pin 2 status '+str(led.value())

test = Server(8266) # Создаём объект сервера с указанием номера порта. По умолчанию используется порт 8080
test.RouteAdd('/test1', test1) # Добавляем обрабатывающую ф-ию test1 для маршрута /test1
test.RouteAdd('/test2', test2) # Так поступаем и для остальных маршрутов и их функций
test.RouteAdd('/stop', stop)
test.RouteAdd('/free', free)
test.RouteAdd('/switch', switch)
test.Run() # Запускаем сервер

upd:
Что-то архив не могу залить. Даю листинг здесь.
Код:
# -*- coding: utf-8 -*-
import socket

class Server:
    def send_answer(self, conn, status="200 OK", typ="text/plain; charset=utf-8", data=""):
        data = data.encode("utf-8")
        conn.send(b"HTTP/1.1 " + status.encode("utf-8") + b"\r\n")
        conn.send(b"Server: simplehttp\r\n")
        conn.send(b"Connection: close\r\n")
        conn.send(b"Content-Type: " + typ.encode("utf-8") + b"\r\n")
        conn.send(b"Content-Length: " + bytes(len(data)) + b"\r\n")
        conn.send(b"\r\n")# после пустой строки в HTTP начинаются данные
        conn.send(data)

    def RouteAdd(self, path, funcname):
        if path not in self.routes:
            self.routes[path] = funcname

    def parse(self, conn, addr):# обработка соединения в отдельной функции
        data = b""
        while not b"\r\n" in data: # ждём первую строку
            tmp = conn.recv(1024)
            if not tmp: # сокет закрыли, пустой объект
                break
            else:
                data += tmp

            if not data: # данные не пришли
                return # не обрабатываем

            udata = data.decode("utf-8")
            # берём только первую строку
            udata = udata.split("\r\n", 1)[0]
            # разбиваем по пробелам нашу строку
            method, string, protocol = udata.split(" ", 2)
            if string.find('?') != -1:
                address = string.split('?')[0]
                params = dict(b.split('=') for b in string.split('?')[1].split('&'))
            else:
                address = string
                params = {}
            if method != "GET":
                self.send_answer(conn, "404 Not Found", data="Page not found")
                return
            if address in self.routes:
                if len(params) > 0:
                    self.send_answer(conn, typ="text/html; charset=utf-8", data=self.routes[address](address, params))
                else:
                    try:
                        self.send_answer(conn, typ="text/html; charset=utf-8", data=self.routes[address]())
                    except:
                        self.send_answer(conn, typ="text/html; charset=utf-8", data=self.routes[address](address))
                return

            else:
                self.send_answer(conn, "404 Not Found", data="Page not found")
                return

    def __init__(self,port=8080):
        self.routes = {}
        self.stop = False
        self.sock = socket.socket()
        self.sock.bind( ("", port) )
        self.sock.settimeout(2)
        self.sock.listen(5)

    def Run(self):
        try:
            while 1: # работаем постоянно
                try:
                    if self.stop: break
                    conn, addr = self.sock.accept()
                    #print("New connection from " + addr[0])
                except:
                    continue
                try:
                    self.parse(conn, addr)
                except:
                    self.send_answer(conn, "500 Internal Server Error", data="Error")
                finally:
                # так при любой ошибке
                # сокет закроем корректно
                    conn.close()
        finally:
            self.sock.close()
            # так при возникновении любой ошибки сокет
            # всегда закроется корректно и будет всё хорошо
 
Последнее редактирование:

straga

New member
А сколько RAM памяти он потребляет на ESP?

А можно не блокирующую версию посмотреть тоже.
 
Последнее редактирование:

corpse

New member
Добрый день! А как быть с передачей параметров? Не могу разобраться. :(
test.RouteAdd('/test2', test2) - здесь мы передаём в качестве второго параметра метод. Но если мне нужно будет в этом методе добавить обработку хэдеров, урла?
 

Cosmatos

New member
при попытке импорта выдает ошибку. Говорит не знает такого модуля.
ImportError: no module named 'HTTPServer'
 

__ab__

New member
если использовать select, можно написать очень простой вариант сервера, который не будет особо мешать выполнению чего-то еще:
Код:
import socket,select

def handle_http(client, client_addr):
    client.send("HTTP/1.0 200 OK\r\n\r\nHelloWorld!!!\r\n  %s" % str(client_addr))
    client.close()

def serv(port=80):
    http = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = (socket.getaddrinfo("0.0.0.0", port))[0][-1]
    http.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    http.bind(addr)
    http.listen(4)

    while True:
        r, w, err = select.select((http,), (), (), 1)
        if r:
            for readable in r:
                client, client_addr = http.accept()
                handle_http(client, client_addr)
    # a cюда можно вставить обработку еще-чего-то
    # а можно вставить такую обработку по таймеру

serv()
 
Последнее редактирование:

lex.golubtsov

New member
если использовать select, можно написать очень простой вариант сервера, который не будет особо мешать выполнению чего-то еще:
Код:
import socket,select

def handle_http(client, client_addr):
    client.send("HTTP/1.0 200 OK\r\n\r\nHelloWorld!!!\r\n  %s" % str(client_addr))
    client.close()

..........

serv()
Привет спаситель!
Специально зарегистрировался чтобы сказать СПАСИБО за пример!
Питоном и сокетами раньше не пользовался, примеры в интернете сильно навороченные, нихрена не понятно, эта страница попадалась, но там хрень с блокировкой из примеров в низ не сразу прокрутил. Ну вот как-то так.

PS. Если выкладываешь куда-нибудь свои скрипты для микроконтроллеров, напиши пожалуйста сюда
 

__ab__

New member
Привет!
Рад что помог )
Выкладываю, обычно, в рабочие репозитории - на заказ..
В основном для компьютеров, а не для контроллеров )
 

BigStupidBeast

New member
Привет!
Рад что помог )
Выкладываю, обычно, в рабочие репозитории - на заказ..
В основном для компьютеров, а не для контроллеров )
Тоже зарегался плюсануть и вопрос задать.
А где прочитать/посмотреть про сокеты серверы? для чайников. Пока всё что попадалось как-то напоминает тот мем про сову. Можно на буржуинском
 

__ab__

New member
Сокетам уже очень много лет..
Поэтому документации по ним - море.
Можно просто читать доку по Python3 потом пробовать в Micropython
Поэтому главный сайт: https://www.python.org/
Тут легко найти доки на микропитон для esp: http://docs.micropython.org/en/latest/
Тут доки по сокетам: https://docs.python.org/3/library/socket.html
Всё вобщем-то легко ищется...
 

oxidizer

New member
Добрый день.
Опробовал этот код.
И вот в чём проблема:
Если текст HTML страницы не велик - то всё в порядке.
Но если к примеру более 1500 символов - то в браузере firefox мелькает страница на долю секунды а затем появляется сообщение:
Соединение было сброшено
Во время загрузки страницы соединение с сервером было сброшено.

А chrom в первый раз открывает пустую страницу а после обновления отображает нормально

IE говрит:
Не удается открыть эту страницу

Вот код который работает:
Код:
import socket,select

html = """
<!DOCTYPE html>
<html>
<head>
<title>ESP Web Server</title>
</head>
<body>
<h1>ESP Web Server</h1>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
</body>
</html>
"""

def handle_http(client, client_addr):
    client.send('HTTP/1.0 200 OK\r\ncontent-type: text/html; charset=UTF-8\r\n\r\n' + html)
    client.close()

def serv(port=80):
    http = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    addr = (socket.getaddrinfo("0.0.0.0", port))[0][-1]
    http.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    http.bind(addr)
    http.listen(4)

    while True:
        r, w, err = select.select((http,), (), (), 1)
        if r:
            for readable in r:
                client, client_addr = http.accept()
                handle_http(client, client_addr)
serv()
Но если увеличить
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>

в два раза:
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>

Происходит вышеописанная проблема.
Может кто нибудь подскажет в чём дело?
Можте с заголовком что то не так?
Прошивки пробовал разные.
Сейчас прошивка: esp8266-20191220-v1.12.bin (MicroPython)
 

__ab__

New member
Я бы не рекомендовал передавать с железки больше, чем по килобайт.
def handle_http(client, client_addr):
data = b"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Test Web Server</h1>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<p>text text text text text text text text text text text text text text</p>
<hr>
Is OK!
</body>
</html>"""
data = b'HTTP/1.0 200 OK\r\ncontent-Length: %s\r\ncontent-type: text/html; charset=UTF-8\r\n\r\n%s' % (str(len(data)).encode('ascii'), data)
client.send(data)
client.close()

На 8266 вряд-ли можно сделать серьёзный веб-сервер ))

Скорее стоит с контроллера отдавать данные на сервер (например через MQTT), а уже на сервере делать вебку.
Если нагрузка 1-2 пользователя, в качестве такого сервера вполне сойдет Raspberry Pi, например.
При большей нагрузке стоит подумать о том, чтобы веб-сервер разместить на компьютере.
 

oxidizer

New member
Этот код так же не работает.
В хроме недогружает весь текст.

Screenshot_1.png

В фаерфокс вот такая беда:

Screenshot_2.png
 
Сверху Снизу