Задание №5
Задание
Необходимо написать простой web-сервер для обработки GET и POST http запросов средствами Python и библиотеки socket.
Базовый класс для простейшей реализации web-сервера доступен по ссылке.
Задание: сделать сервер, который может:
- Принять и записать информацию о дисциплине и оценке по дисциплине.
- Отдать информацию обо всех оценах по дсициплине в виде html-страницы.
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,
статус кодом и телом запроса и отправляется клиенту.
<!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>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Дисциплина не найдена!</title>
</head>
<body>
Дисциплина не найдена!
</body>
</html>
В файле index.html
создаём базовую форму с 2 полями для ввода (дисциплина и оценка) и кнопкой отправить.
На 2 поле настроена валидация, реагирующая на ивент onchange и проверяющая, что введённое значение находится в допустимых пределах.
При нажатии на кнопку Отправить вызывается функция, которая собирает значения этих 2 полей, объединяет их в JSON и отправляет POST-запросом на сервер, затем обновляет страницу.