Большой Проект
Итак, предполагая, что вы читали предыдущие части этой серии, вы должны иметь несколько практических знаний о различных частях целлулоида.
Но до сих пор мы не раскрыли, как все эти части объединяются в единое приложение. Мы собираемся сделать это здесь!
Итак, давайте погрузимся!
Что это?
Какова будет наша цель? На самом деле все довольно просто: мы создадим HTTP-сервер.
Точнее, мы создадим очень неполный, но легко расширяемый HTTP-сервер, написанный на Ruby, с библиотекой Celluoid.
Не ожидайте, что это будет что-то вроде Apache или Thin — но мы узнаем немало о процессе Celluloid и HTTP, который (возможно) является важным сетевым протоколом для разработчиков.
Немного о HTTP
Конечно, если мы хотим создать HTTP-сервер, мы должны знать, что такое HTTP и как он работает.
В случае, если вы не знаете, HTTP — это протокол, который используется для передачи HTML с сервера на клиент (это не всегда так, но это наиболее распространенный вариант), и наиболее распространенный, когда клиент сообщает Сервер, чтобы делать определенные вещи.
Протокол основан на запросах. Таким образом, клиент может отправить на сервер запрос «GET», чтобы получить HTML-код для определенной страницы. Кроме того, клиент может выполнить запрос «POST» для предоставления некоторых данных серверу.
Важным моментом в HTTP является то, что сам протокол не имеет состояния . Это означает, что каждый запрос не знает ни о каких предыдущих запросах.
Мы будем реализовывать только запрос GET; Это по двум причинам. Во-первых, это основная функция HTTP-сервера. Во-вторых, это очень легко реализовать.
Но мы напишем наш код так, чтобы он был достаточно модульным, чтобы другие методы можно было легко добавить.
Начиная
Прежде всего, давайте создадим быстрый и грязный прототип.
В Ruby встроена поддержка связи через сокеты, что достигается простым оператором «require ‘socket” ». Используя это, и всю магию из целлулоида, вот наш прототип:
require ‘socket’ | |
require ‘celluloid’ | |
class HTTPServer | |
include Celluloid | |
def initialize(port) | |
@port = port | |
end | |
def start | |
@server = TCPServer.new(@port) | |
loop { | |
client = @server.accept | |
headers = «HTTP/1.1 200 OK\r\nDate: Tue, 14 Dec 2010 10:48:45 GMT\r\nServer: Ruby\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n« | |
client.puts headers | |
client.puts «<html></html>» | |
client.close | |
} | |
end | |
end | |
hs = HTTPServer.new 3000 | |
hs.start |
У нас есть класс HTTPServer
Этот метод просто запускает сервер, используя класс TCPServer
Затем мы делаем @server.accept
Это очень важно, потому что это блокирующий вызов. Это означает, что работа не будет продвигаться дальше, пока не придет клиент.
Затем мы просто пишем заголовки HTTP и HTML (независимо от того, какой тип запроса мы получили).
Конечно, есть очевидная проблема. Это не одновременно. Поскольку все вызовы, которые мы используем, блокируют, мы просто выполняем каждый клиент синхронно. Это не хорошо!
Принимая это асинхронно
Решение этой дилеммы приходит в форме написания другого актера.
Вот код:
require ‘socket’ | |
require ‘celluloid’ | |
class AnswerActor | |
include Celluloid | |
def initialize(client) | |
@client = client | |
end | |
def start | |
@client.puts | |
headers = «HTTP/1.1 200 OK\r\nDate: Tue, 14 Dec 2010 10:48:45 GMT\r\nServer: Ruby\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n« | |
@client.puts headers | |
@client.puts «<html></html>» | |
loop { | |
#kind of just hang around and block the actor | |
} | |
end | |
end | |
class HTTPServer | |
include Celluloid | |
def initialize(port) | |
@port = port | |
end | |
def start | |
@server = TCPServer.new(@port) | |
loop { | |
aa = AnswerActor.new @server.accept | |
puts «waddup» | |
aa.start! | |
} | |
end | |
end | |
hs = HTTPServer.new 3000 | |
hs.start |
Это довольно большой процесс, но мы просто сделаем это шаг за шагом.
AnswerActor
Настоящая работа происходит в методе start
Здесь мы берем клиентский сокет (с помощью которого мы можем общаться с клиентом) и записываем в него заголовок и HTML, а затем просто ждем.
В классе HTTPServer
aa.start!
Это означает, что start
Чтобы проверить, действительно ли это работает, откройте два сеанса telnet (или netcat, который мне больше всего нравится) на нашем доморощенном HTTP-сервере, и вы должны увидеть, что вы получите ответ на оба (что также является причиной того, что у нас есть цикл в конце метода start в классе AnswerActor — вы должны видеть параллельные соединения, чтобы верить им!)
Но мы не слишком эффективны с этим. Наши актеры просто сидят без дела, как только они созданы, съедая ресурсы. Что может сделать целлулоид для нас?
Идти плавать
Мы можем использовать бассейны! Получите, вот почему я назвал этот раздел «плавать»! Нет? Ну, к коду:
require ‘socket’ | |
require ‘celluloid’ | |
class AnswerWorker | |
include Celluloid | |
def start(client) | |
client.puts | |
headers = «HTTP/1.1 200 OK\r\nDate: Tue, 14 Dec 2010 10:48:45 GMT\r\nServer: Ruby\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n» | |
client.puts headers | |
client.puts «<html></html>» | |
loop { | |
#kind of just hang around and block the actor | |
} | |
end | |
end | |
class HTTPServer | |
include Celluloid | |
def initialize(port) | |
@port = port | |
end | |
def start | |
@server = TCPServer.new(@port) | |
client = nil | |
pool = AnswerWorker.pool(size: 50) | |
loop { | |
client = @server.accept | |
pool.start! client | |
} | |
end | |
end | |
hs = HTTPServer.new 3000 | |
hs.start |
Если вы посмотрите на измененный код в HTTPServer.start
Мы просто создаем пул из 50 актеров (и, следовательно, 50 потоков), которые мы затем можем назначить на работу. Все это с одной строкой кода. Это потрясающе!
Почему 50? Ну, просто случайное число, которое я выбрал. Если вам интересно, за подбор оптимальных размеров пулов потоков была проделана хорошая работа .
В случае возникновения исключения для субъекта в пуле, он будет перезапущен автоматически и готов к использованию в следующий раз, когда это необходимо!
Отвечая на запросы
Пока что мы выплюнули только пару тегов html — на самом деле мы не слушали то, что запрашивает пользователь. Давайте работать над этим.
Вуаля:
require ‘socket’ | |
require ‘celluloid’ | |
class Query | |
attr_accessor :type, :url, :other | |
def initialize (query_string) | |
@type, @url, @other = query_string.split » « | |
end | |
end | |
class AnswerWorker | |
include Celluloid | |
def process_get | |
... | |
end | |
def start(client) | |
@client = client | |
client.puts | |
headers = «HTTP/1.1 200 OK\r\nDate: Tue, 14 Dec 2010 10:48:45 GMT\r\nServer: Ruby\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n« | |
client.puts headers | |
loop { | |
query = Query.new client.readline | |
process_get if query.type == «GET» | |
} | |
end | |
end | |
class HTTPServer | |
include Celluloid | |
def initialize(port) | |
@port = port | |
end | |
def start | |
@server = TCPServer.new(@port) | |
client = nil | |
pool = AnswerWorker.pool(size: 50) | |
loop { | |
client = @server.accept | |
pool.start! client | |
} | |
end | |
end | |
hs = HTTPServer.new 1777 | |
hs.start |
Опять же, есть ряд изменений. Мы добавили класс Query
Например, запрос GET выглядит как «GET /index.html HTTP / 1.1», где /index.html — запрашиваемый URL-адрес, а HTTP / 1.1 — используемый протокол (в отличие от HTTP / 1.0).
Затем в AnswerWorker
process_get
Но мы не определили, что именно должен делать process_get
Отвечая на GETs
Осторожно, это ужасная реализация process_get
Вот:
require ‘socket’ | |
require ‘celluloid’ | |
class Query | |
attr_accessor :type, :url, :other | |
def initialize (query_string) | |
@type, @url, @other = query_string.split » « | |
end | |
end | |
class AnswerWorker | |
include Celluloid | |
def process_get(query) | |
dir_path = «server» | |
filepath = dir_path + query.url | |
if File.exists? filepath | |
@client.puts (File.open(filepath).read) | |
else | |
@client.puts «Can’t find file» | |
end | |
end | |
def process_req(query) | |
process_get query if query.type == «GET» | |
end | |
def start(client) | |
@client = client | |
headers = «HTTP/1.1 200 OK\r\nDate: Tue, 14 Dec 2010 10:48:45 GMT\r\nServer: Ruby\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n« | |
client.puts headers | |
loop { | |
query = Query.new client.readline | |
process_req query | |
} | |
end | |
end | |
class HTTPServer | |
include Celluloid | |
def initialize(port) | |
@port = port | |
end | |
def start | |
@server = TCPServer.new(@port) | |
client = nil | |
pool = AnswerWorker.pool(size: 50) | |
loop { | |
client = @server.accept | |
pool.start! client | |
} | |
end | |
end | |
hs = HTTPServer.new 3000 | |
hs.start |
Он просто берет папку «server» (в той же папке, что и сам исходный код HTTP-сервера) и возвращает содержимое данного файла.
И мы также выяснили, как мы обрабатываем, какой тип запроса был отправлен клиентом, чтобы облегчить расширение сервера.
Обратите внимание, что из-за простого способа сделать это, прямое подключение 127.0.0.1:3000 к вашему браузеру не будет работать — вам придется использовать 127.0.0.1:3000/index.html или другое имя файла!
Завершение
Мы создали (очень) элементарный HTTP-сервер, используя Celluloid.
При этом мы увидели, как актеры могут работать вместе в простых ситуациях.
Конечно, вы будете писать приложения большего размера, чем это, используя Celluloid (я очень на это надеюсь!), И вы, вероятно, будете использовать более продвинутые функции. Помните, однако, что основы очень важны. Я надеюсь, что объединение некоторых концепций облегчит изучение.
Если вам понравилась статья, сделайте Tweet ее своим подписчикам. Спасибо за прочтение!