Статьи

Введение в целлулоид, часть III

Большой Проект

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

Но до сих пор мы не раскрыли, как все эти части объединяются в единое приложение. Мы собираемся сделать это здесь!

Итак, давайте погрузимся!

Что это?

Какова будет наша цель? На самом деле все довольно просто: мы создадим 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

view raw
gistfile1.rb
hosted with ❤ by GitHub

У нас есть класс 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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Это довольно большой процесс, но мы просто сделаем это шаг за шагом.

AnswerActor Настоящая работа происходит в методе start

Здесь мы берем клиентский сокет (с помощью которого мы можем общаться с клиентом) и записываем в него заголовок и HTML, а затем просто ждем.

В классе HTTPServeraa.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

view raw
gistfile1.txt
hosted with ❤ by GitHub

Если вы посмотрите на измененный код в 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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Опять же, есть ряд изменений. Мы добавили класс Query

Например, запрос GET выглядит как «GET /index.html HTTP / 1.1», где /index.html — запрашиваемый URL-адрес, а HTTP / 1.1 — используемый протокол (в отличие от HTTP / 1.0).

Затем в AnswerWorkerprocess_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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Он просто берет папку «server» (в той же папке, что и сам исходный код HTTP-сервера) и возвращает содержимое данного файла.

И мы также выяснили, как мы обрабатываем, какой тип запроса был отправлен клиентом, чтобы облегчить расширение сервера.

Обратите внимание, что из-за простого способа сделать это, прямое подключение 127.0.0.1:3000 к вашему браузеру не будет работать — вам придется использовать 127.0.0.1:3000/index.html или другое имя файла!

Завершение

Мы создали (очень) элементарный HTTP-сервер, используя Celluloid.

При этом мы увидели, как актеры могут работать вместе в простых ситуациях.

Конечно, вы будете писать приложения большего размера, чем это, используя Celluloid (я очень на это надеюсь!), И вы, вероятно, будете использовать более продвинутые функции. Помните, однако, что основы очень важны. Я надеюсь, что объединение некоторых концепций облегчит изучение.

Если вам понравилась статья, сделайте Tweet ее своим подписчикам. Спасибо за прочтение!