Давайте разберемся с потоками, их работой и некоторыми классическими задачами.

Освоение веб-разработки - это жестокий и бесконечный путь. Частью этого стремления является понимание антиутопического мира, в котором находится вся магия - в конце концов, все сводится к байтам.

Давайте перейдем к некоторым низкоуровневым концепциям. В конце статьи я предлагаю вам написать собственный веб-сервер с нуля, используя чистый C.

Всего 3 строчки кода на Node.JS

Использовать netcat для открытия потока очень просто. Чтобы прослушать соединения, просто введите nc -l ‹port›, а чтобы подключиться к нему, откройте второй терминал (который может быть на втором компьютере) и введите nc ‹host› ‹Port›.

Наслаждайтесь трансляцией.

Ничего страшного. На Node.JS можно построить эхо-сервер за секунду. Этот фрагмент даст нам точно такое же поведение:

Подождите, там есть. pipe (). Это та же концепция канала, которую мы используем в Linux, когда нам нужно объединить процессы? да. В Unix все является файловым дескриптором, поэтому потоковая передача из / в стандартный ввод-вывод, сетевые сокеты или файлы на диске - это всего лишь вопрос перенаправления.

Рассечение пайпа Node.JS

Итак, давайте посмотрим, какое поведение реализует метод pipe. Взгляните на этот код:

Каждый раз, когда клиент отправляет некоторые данные в наш сокет, запускается событие readable, и socket.read вызывается хотя бы один раз, пока внутренний буфер не будет полностью опустошен.

Этот код раскрывает два интересных момента о конвейере в Node.JS. Во-первых, он явно использует разблокирующий характер событий. Во-вторых, он собирает данные по частям, давайте посмотрим, чем это может быть полезно.

Загрузка больших файлов

Разрешение вашему клиенту загружать большие объекты может быть не очень хорошо для вашего сервера.

Большинство языков программирования имеют дело только с объектами, полностью загруженными в память. Давайте немного изменим наш код, чтобы предотвратить истощение ресурсов нашего сервера при использовании Node.JS.

Это гарантирует, что Node.JS не будет обрабатывать более CHUNK_SIZE байтов в любой момент времени.

Строительные блоки в C

Node.JS предлагает потрясающую абстракцию низкоуровневого ввода-вывода. На данный момент мы всего на один дюйм отстают от другой хорошо известной абстракции, которая даст нам больше понимания того, как работает сетевое взаимодействие: Berkeley Sockets.

Этот сайт - отличный ресурс для примеров кода, где вы увидите эти функции в более подробном контексте. Я буду использовать это (бесплатное!) Руководство в качестве справочника.

Во-первых, давайте посмотрим, какая функция нам нужна для объявления сокета.

Мы будем использовать несколько макросов для придания формы нашему сокету. Вы можете выбрать для своего домена PF_INET или PF_INET6, чтобы выбрать IPv4 или IPv6. Для вашего типа и протокола мы будем использовать SOCK_STREAM и IPPROTO_TCP. Это просто потому, что TCP - это протокол, предназначенный для обеспечения соединений, подобных сокетам.

Теперь давайте воспользуемся файловым дескриптором, который вы только что получили от SO. Пока вы строите сервер, давайте привяжем этот сокет к порту:

Первый аргумент - это возвращаемое значение функции сокета - в C дескриптор файлов представлен целыми числами. Вторая - это структура, содержащая семейство адресов, адрес и порт, а третья - размер этой структуры.

Теперь у нас есть эквивалент вызова net.createServer на Node.JS. Итак, клиенты терпеливо ждут в очереди, чтобы их приняли. Давайте их примем!

sockfd - это прослушивающий сокет. С другой стороны, addr - это структура, которая поможет нам идентифицировать клиента, которого мы принимаем. Теперь самое интересное: accept () вернет новый файловый дескриптор, так что мы можем поговорить с этим клиентом наедине. Правило простое: для каждого нового клиента новый сокет.

Потрясающий. Ожидание прибытия клиента и его принятия по умолчанию является операцией блокировки. А потом пора поговорить с этим клиентом, используя эти две функции:

send () ing и recv () ing очень похожи. Вам просто нужно использовать дескриптор файла, который был предоставлен при принятии клиента, и предоставить указатель на первый байт, который вы хотите отправить (или записать), а также количество байтов, которое вы хотите отправить (или получить. ).

Просто чтобы провести параллель между этим и тем, что мы сделали с помощью Node.JS - убедитесь, что у нас есть контроль над шириной нашего потока, так же, как мы это делали с CHUNK_SIZE!

В общем, вот как будет выглядеть наш эхо-сервер на C:

Ошибка Heartbleed

Что, если я скажу, что теперь вы можете понять возможно, самую серьезную уязвимость, обнаруженную с тех пор, как в Интернете начал течь коммерческий трафик?

Heart beat был протоколом расширения OpenSSL. Он действует как эхо-сервер. Когда получатель получает сообщение HeartbeatRequest, получатель должен отправить обратно точную копию полученного сообщения в сообщении HeartbeatResponse. Если содержимое такое же, безопасное соединение будет сохранено.

Я подскажу, в чем ошибка. Прочтите следующий рисунок xkcd и взгляните на интерфейсы send () и recv () в предыдущем разделе.

Точно. Вполне возможно, что по ошибке или вызванной ошибкой вы отправите больше байтов, чем имеется в вашем фактическом буфере. Проиллюстрируем это:

Это известно как переполнение буфера, которое обычно ассоциируется с такими языками, как C и C ++. Сделайте глубокий вдох и поблагодарите Node.JS за то, что этот вид эксплойтов стал намного менее вероятен для ваших приложений.

Но, честно говоря, C есть такие инструменты, как Valgrind, чтобы предотвратить появление в приложении таких ошибок памяти, нам просто нужно правильно их протестировать! Кстати, если вы хотите узнать больше об этой ошибке, загляните на свой сайт и этот аккуратный pdf от IBM.

Написание собственного веб-сервера

Прекрасная работа! Теперь, когда вы знаете анатомию соединений сокетов, реализация протокола приложения, такого как HTTP, может быть довольно простой.

Например, вы можете реализовать небольшое подмножество протокола HTTP 1.0. Проанализируйте заголовок запроса GET вместе с любыми параметрами пути, которые он может содержать, и верните правильный заголовок после того, что было запрошено - это может быть как простой текст, так и двоичный файл.

Естественно возникнут некоторые вопросы.

Как бы вы разветвили (или распределили) свое приложение C, чтобы принимать и обрабатывать несколько клиентов? В чем практическая разница между простым разветвлением / потоковой передачей и наличием цикла событий, как в Node.JS? Каковы будут накладные расходы при потоковой передаче по HTTP (S)? Как улучшить TTFB (время до первого байта)? Как выглядит архитектура сетей CDN?

Поиск этих ответов, безусловно, сделает вас лучшим веб-разработчиком. 😉