Если вы разработчик на php вы наверняка уже сталкивались с HTTP-серверами, такими как: apache, nginx или OpenServer.
Зачем?
Причин может быть несколько, но самая основная это обучение. Чтобы расширить своё понимание протокола HTTP, и как с ним можно работать.
Как работает HTTP-сервер?
Любой веб-сервер делает три вещи:
Слушает порт.
Принимает соединения и читает сырой HTTP-запрос.
Отправляет ответ.
В PHP для этого есть функция stream_socket_server()
Теория
HTTP Текстовый формат, все заголовки разделяются переносом строки, двойной перенос сообщает о начале тела ответ/запроса. Пример запроса:
GET /dfgdfg HTTP/1.1 Host: lumetas.ru:8000 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: ru-RU,ru;q=0.9 Cookie: property=value;property1=value1
В первой строке указывается метод и путь запроса, а так же версия HTTP, далее идут заголовки и куки которые отдаёт клиент, разделённые через точку с запятой
После сервер возвращает похожий текст, например:
HTTP/1.1 200 OK Date: Thu, 29 Jul 2021 19:20:01 GMT Content-Type: text/html; charset=utf-8 Content-Length: 15 Connection: close Server: gunicorn/19.9.0 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly <div>text</div>
Здесь указывается статус и версия протокола http тип контента, указывает браузеру на то какого формата данные, их длина. И другие произвольные данные, так же вы можете видеть как устанавливаются cookie
Прослушивание
Реализуем класс и метод, который будет прослушивать входящие запросы
class SimpleHttpServer { private $socket; public function __construct(private string $host = '0.0.0.0', private int $port = 8000) {} // Устанавливаем свойства класса public function listen(callable $requestHandler): void // Создаём метод который будет использоваться нами в дальнейшем для работы с классом { $this->createSocket(); // используем метод создание сокета echo "Server running on http://{$this->host}:{$this->port}\n"; //Выводим в консоль информацию о том что сервер запущен while ($conn = stream_socket_accept($this->socket, -1)) {// Ждём запрос $request = fread($conn, 8192); // Читаем 8КБ данных из запроса $requestInfo = $this->parseRequest($request); // Парсим запрос $response = $requestHandler($requestInfo); // Передаём в нашу callback функцию $this->sendResponse($conn, $response); // возвращаем ответ fclose($conn); } } }
Далее методы для создания сокета и отправки ответа:
private function createSocket(): void // Метод для создания сокета { $this->socket = stream_socket_server( "tcp://{$this->host}:{$this->port}", // Указываем хост и порт для сокета // Переменные для ошибок $errno, $errstr ); if (!$this->socket) { throw new RuntimeException("Failed to create socket: $errstr ($errno)"); // Выкидываем исключение в случае ошибки создания сокета } } private function sendResponse($conn, array|string $response): void // Метод для отправки ответ { if (is_array($response)) { // Если на вход передан массив то формируем заголовки $status = $response['status'] ?? 200; // Статус 200 - успех $headers = $response['headers'] ?? ['Content-Type' => 'text/plain']; // Массив заголовков $body = $response['body'] ?? ''; // Тело ответа $headerString = "HTTP/1.1 $status OK\r\n"; // Строка заголовка foreach ($headers as $name => $value) { // Перебираем массив заголовков и составляем строку $headerString .= "$name: $value\r\n"; } fwrite($conn, $headerString . "\r\n" . $body); // В конце добавляем ещё перенос строки так чтобы получилась пустая строка и записываем в сокет } else { fwrite($conn, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n$response"); // Если передана строка возвращаем } }
Теперь мы можем отдать ответ пользователю, но формироваться он должен на основе запроса, так что напишем методы для парсинга запроса:
private function parseRequest(string $request): array { $lines = explode("\r\n", $request); // Разбиваем запрос по переносы строки $firstLine = explode(' ', $lines[0]); // Разбиваем первую строку по пробелу return [ // Возвращаем распаршенный запрос 'method' => $firstLine[0] ?? 'GET', // Определяем метод, если он не указан, пишем GET 'path' => $firstLine[1] ?? '/', // Определяем запрашиваемую локацию 'headers' => $this->extractHeaders($lines), // Извлекаем заголовки 'body' => $this->extractBody($lines), // Извлекаем тело запроса 'raw' => $request // И добавляем сырой запрос ]; } private function extractHeaders(array $lines): array { $headers = []; // Создаём массив заголовков foreach ($lines as $line) { // Перебираем строки if (strpos($line, ':') !== false) { // Если есть двоеточие [$name, $value] = explode(':', $line, 2); // Тогда сохраняем имя заголовка и значение $headers[trim($name)] = trim($value); // Добавляем в массив, обрезая пустые символы } } return $headers; // Возвращаем } private function extractBody(array $lines): string { $bodyStart = array_search('', $lines) + 1; // Ищем строку с которой начинается тело, по пустой строке return implode("\r\n", array_slice($lines, $bodyStart)); //Соеденяем всё тело как было и возвращаем }
Класс сервера готов, теперь можем его использовать:
<?php require "SimpleHttpServer.php"; $server = new SimpleHttpServer(); $server->listen(function($request) { echo "$request[method] $request[path]\n"; if ($request['path'] === '/hello') { return "Hello World!"; } return [ 'status' => 404, 'body' => 'Not Found' ]; });
Запустить можно запустив главный файл php index.php
Заключение
Мы написали рабочий HTTP сервер на PHP и разобрались с протоколом. Финальный код с комментариями доступен на github