Перейти к содержанию

Задание №5

Задание

Необходимо написать простой web-сервер для обработки GET и POST http запросов средствами Python и библиотеки socket.

Базовый класс для простейшей реализации web-сервера доступен по ссылке.

Задание: сделать сервер, который может:

  • Принять и записать информацию о дисциплине и оценке по дисциплине.
  • Отдать информацию обо всех оценах по дсициплине в виде html-страницы.
server.py
import socket
import json
import sys
from collections import defaultdict
from typing import Any


class MyHTTPServer:
    data = defaultdict(list)

    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port

    def serve_forever(self) -> None:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind((self.host, self.port))
            s.listen(10)
            s.settimeout(0.5)
            while True:
                try:
                    conn, address = s.accept()
                    with conn:
                        conn.settimeout(0.5)
                        try:
                            self.serve_client(conn)
                        except socket.timeout:
                            pass
                except socket.timeout:
                    pass

    def serve_client(self, conn: socket.socket) -> None:
        request = conn.recv(1024).decode()
        if request:
            method, path, version = self.parse_request(request)
            headers = self.parse_headers(request)

            if method == "GET":
                self.handle_get_request(path, conn)
            elif method == "POST":
                body = self.parse_body(request)
                self.handle_post_request(path, body, conn)

    @staticmethod
    def parse_request(request: str) -> tuple[str, str, str]:
        lines = request.split("\r\n")
        method, path, version = lines[0].split(" ")
        return method, path, version

    @staticmethod
    def parse_headers(request: str) -> dict[str, str]:
        lines = request.split("\r\n")
        headers = {}
        for line in lines[1:]:
            if line == "":
                break
            key, value = line.split(": ")
            headers[key] = value
        return headers

    @staticmethod
    def parse_body(request: str) -> dict[Any]:
        lines = request.split("\r\n")
        i = lines.index("")
        if len(lines) > i + 1:
            body = "\r\n".join(lines[i + 1 :])
            return json.loads(body)
        return {}

    def handle_get_request(self, path: str, conn: socket.socket) -> None:
        if path == "/":
            with open("index.html", encoding="utf-8") as f:
                html_file = f.read()

            grades = "<br>".join(f"{discipline}: {', '.join(grades)}" for discipline, grades in self.data.items())
            html_file = html_file.replace("GRADES", grades)
            self.send_response(conn, html_file)
        else:
            with open("not_found.html", encoding="utf-8") as f:
                html_file = f.read()
            self.send_response(conn, html_file, status_code="404 Not Found")

    def handle_post_request(self, path: str, body: dict, conn: socket.socket) -> None:
        if path == "/":
            discipline = body.get("discipline", "")
            grade = body.get("grade", "")
            self.data[discipline].append(grade)

            self.send_response(conn, "")
        else:
            with open("not_found.html", encoding="utf-8") as f:
                html_file = f.read()
            self.send_response(conn, html_file, status_code="404 Not Found")

    @staticmethod
    def send_response(conn, response: str, status_code: str = "200 OK") -> None:
        response_headers = {
            "Content-Type": "text/html; charset=utf-8",
            "Connection": "close",
        }
        response_headers_raw = "".join(f"{k}: {v}\r\n" for k, v in response_headers.items())
        conn.sendall((f"""HTTP/1.1 {status_code}\r\n""" + response_headers_raw + "\r\n" + response).encode("utf-8"))


if __name__ == "__main__":
    host = "127.0.0.1"
    port = 8080
    serv = MyHTTPServer(host, port)
    try:
        serv.serve_forever()
    except KeyboardInterrupt:
        sys.exit(1)

Немного отошёл от структуры базового класса: отказался от переменной name, потому что не нашёл для неё применений, добавил метод parse_body для парсинга тела запроса, а также разделил метод handle_request на 2: отдельно для GET и POST запросов.

В переменной класса инициализируем два объекта: data для хранения списка оценок и Grade, namedtuple для хранения информации о каждой оценке.

В __init__ создаём 2 атрибута: хост и порт сокета сервера.

В методе serve_forever на этот раз с помощью контекстного менеджера создаём и настраиваем сокет сервера и запускаем цикл while, который принимает подключения клиентов. Сокет клиента так же с помощью контекстного менеджера отправляется в метод serve_client.

Метод serve_client принимает запрос пользователя. Проверяет, является ли он пустым. Если нет - парсит его HTTP метод, путь и версию HTTP c помощью метода parse_request и парсит его заголовки с помощью метода parse_headers. Затем, проверяя метод, либо отправляет путь запроса и сокет клиента в метод handle_get_request, либо дополнительно с помощью метода parse_body парсит тело запроса и отправляет путь, тело и сокет в метод handle_post_request.

Метод handle_get_request проверяет путь запроса, если он совпадает с нужным (в моём случае со стартовой страницей), получает содержимое файла index.html, объединяет все оценки из словаря data, заменяет оценками шаблон в HTML-файле и отправляет полученное будущее тело запроса в метод send_response. Если путь не удовлетворил требованиям, в метод send_response отправится содержимое файла not_found.html вместе со статус кодом 404 Not Found.

Метод handle_post_request так же проверяет путь запроса и в случае совпадения проверит содержимое тела запроса, получит оттуда название дисциплины и оценку и добавит эту информацию в словарь data. Затем передаст пустое тело запроса в метод send_response. Если путь не удовлетворил требованиям, то так же отправляет содержимое файла not_found.html.

В методе send_response создаётся словарь заголовков, "трансформируется" в строку, затем объединяется с версией HTTP, статус кодом и телом запроса и отправляется клиенту.

index.html
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Журнал оценок</title>
    <script>
        function validateGradeInput(){
            let grade = document.getElementById('grade');
            if (grade.value < 1) {
                grade.value = 1;
            } else if (grade.value > 5) {
                grade.value = 5;
            }
        }

        function sendPostRequest(event) {
            event.preventDefault();
            let discipline = document.getElementById('discipline').value;
            let grade = document.getElementById('grade').value;

            fetch('/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ discipline: discipline, grade: grade }),
            })
                .then(response => response.text())
                .then(data => {
                    console.log(data);
                    location.reload();
                })
                .catch((error) => console.error('Error:', error));
        }
    </script>
</head>
<body>
GRADES
<form onsubmit="sendPostRequest(event)">
    <input type="text" id="discipline" name="discipline" placeholder="Дисциплина" required>
    <input type="number" id="grade" name="grade" min="1" max="5" placeholder="Оценка" onchange="validateGradeInput()" style="width: 60px;" required>
    <button type="submit">Отправить</button>
</form>
</body>
</html>
not_found.html
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Дисциплина не найдена!</title>
</head>
<body>
Дисциплина не найдена!
</body>
</html>

В файле index.html создаём базовую форму с 2 полями для ввода (дисциплина и оценка) и кнопкой отправить.

На 2 поле настроена валидация, реагирующая на ивент onchange и проверяющая, что введённое значение находится в допустимых пределах.

При нажатии на кнопку Отправить вызывается функция, которая собирает значения этих 2 полей, объединяет их в JSON и отправляет POST-запросом на сервер, затем обновляет страницу.

Отображение в браузере
Отображение в браузере