Как мне превратить этот простой эхо-сервер эликсира в простой веб-сервер? - PullRequest
0 голосов
/ 15 мая 2019

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

Я пытался посмотреть, как другие делают свою минимальную сборку для работающего сервера в Elixir, такую ​​как https://www.jungledisk.com/blog/2018/03/19/tutorial-a-simple-http-server-in-elixir/,, казалось многообещающим, но я все еще не знаю, как реализовать это в этом.

EchoServer, который был предоставлен

defmodule EchoServer do
  require Logger

  def accept(port) do
    {:ok, socket} = :gen_tcp.listen(port,
      [:binary, packet: :line, active: false, reuseaddr: true])
    Logger.info "Accepting connections on port #{port}"
    loop_acceptor(socket)
  end

  defp loop_acceptor(socket) do
    {:ok, client} = :gen_tcp.accept(socket)
    Task.start_link(fn -> serve(client) end)
    loop_acceptor(socket)
  end

  defp serve(socket) do
    socket |> read_line() |> write_line(socket)
    :ok = :gen_tcp.close(socket)
  end

  defp read_line(socket) do
    {:ok, data} = :gen_tcp.recv(socket, 0)
    data
  end

  defp write_line(line, socket) do
    :gen_tcp.send(socket, line)
  end

  def main(args \\ []) do
    accept(9999)
  end
end

1 Ответ

2 голосов
/ 16 мая 2019

Давайте проясним некоторые вещи. Сервер может вернуть HTML, но это браузер, который отображает HTML. Например, если вы вводите адрес в адресную строку браузера, это заставляет браузер отправлять запрос на сервер, а сервер отправляет обратно ответ - содержащий некоторый html в теле ответа - который анализирует браузер. из ответа и интерпретируется как цветной текст, картинки и т. д., который известен как , отображающий HTML .

Обратите внимание, что HTML - это просто текст, который отформатирован определенным образом, например <div>hello</div>, поэтому для сервера возврат html-файла ничем не отличается от возврата текстового файла. Сервер фактически возвращает содержимое файла - и содержимое не имеет расширения. Вы должны понимать, что это всего лишь текст, который отправляется «по проводам».

Вот что вам нужно сделать:

  1. Создайте tcp-сервер, который прослушивает какой-либо порт и возвращает html при поступлении запроса.

  2. В адресной строке браузера укажите адрес, на котором слушает сервер (например, localhost, который является адресом 127.0.0.1, который является адресом на вашем компьютере), а также порт, на котором находится сервер. прослушивание, например http://localhost:3456.

Есть одна большая проблема: не существует универсального протокола для взаимодействия серверов и клиентов (например, браузеров). Проблема вызвана тем, как текст отправляется в сокет TCP. Когда вы отправляете некоторый текст в сокет tcp, вы не представляете, будет ли текст разбиваться на куски, и как долго будет длиться каждый кусок. Данные могут быть отправлены как один кусок, или данные могут быть разделены на десять частей. Это создает проблему для получателя данных: как получатель узнает, когда он должен прекратить попытки считывать данные из сокета, потому что данные больше не поступают?

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

Или, протокол может быть, когда в данных встречается слово «конец» - тогда получатель должен прекратить попытки чтения из сокета, когда он читает «конец». Но вот пример того, как это может вызвать проблемы:

msg = "I finished the end of the book.  It was great!"
to_send = msg <> "end"

Получатель будет думать, что оно дошло до конца сообщения, как только будет написано «Я закончил конец», что является только частью сообщения. Конечный маркер типа **&&=>END1234<=!!** будет работать лучше. Точно так же протокол для обозначения конца данных может быть новой строкой ("\ n"). Например, опция прослушивания сокета packet: :line устанавливает все так, что :get_tcp.recv() будет читать одну строку из сокета.

Еще один протокол - использовать первые 4 байта данных, чтобы указать целое число, представляющее собой число байтов, которое другая сторона должна впоследствии прочитать из сокета. Получатель ожидает, пока он не прочитает 4 байта из сокета, затем получатель считывает дополнительные N байтов (целое число, содержащееся в первых 4 байтах), и как только будет прочитано еще N байтов, получатель знает, что это конец данных.

Протокол, согласованный веб-браузерами и серверами, - это протокол http request и http response. Вы можете увидеть некоторые примеры http запроса и формата ответа здесь . Поскольку вы кодируете tcp-сервер, вы можете упростить ситуацию, полностью игнорируя запрос (поэтому вам не нужен формат) и возвращая тот же ответ для любого входящего запроса. Кроме того, это избавляет от необходимости выяснять когда сервер должен прекратить попытки чтения из сокета.

Ответ, отправляемый вашим tcp-сервером, должен подчиняться протоколу HTTP-ответа, потому что вам потребуется браузер для получения ответа, чтобы увидеть обработанный html, то есть вы хотите ввести http://localhost:33444 в адресной строке браузера.

Ниже приведен пример изменения эхо-сервера таким образом, чтобы он соответствовал протоколу HTTP-ответа и также возвращал html в теле ответа:

~/elixir_programs/tcp_server$ tree .
.
├── page.html
├── resp_header.txt
└── s1.ex

s1.ex

defmodule HtmlServer do
  require Logger

  def accept(port) do
    {:ok, socket} = :gen_tcp.listen(
        port,
        [:binary, 
         packet: :line, 
         active: false, 
         reuseaddr: true]
    )
    Logger.info "Accepting connections on port #{port}"
    loop_acceptor(socket)
  end

  defp loop_acceptor(socket) do
    {:ok, client} = :gen_tcp.accept(socket)
    Task.start_link(fn -> serve(client) end)
    loop_acceptor(socket)
  end

  defp serve(socket) do
    line = read_socket(socket)  #blocks until something is read from the socket
    IO.puts "[ME] Got some data! #{line}"

    resp_header = File.read! "./resp_header.txt"
    resp_body = File.read! "./page.html" 
    content_len = String.length(resp_body)

    resp = 
        resp_header <> 
        "Content-Length: #{content_len}\n" <>
        "\n" <>
        resp_body

    #IO.inspect resp

    write_socket(resp, socket)
    :ok = :gen_tcp.close(socket)
  end

  defp read_socket(socket) do
    {:ok, data} = :gen_tcp.recv(socket, 0)
    data
  end

  defp write_socket(data, socket) do
    :gen_tcp.send(socket, data)
  end

  def start() do
    accept(9999)
  end
end

resp_header.txt

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

page.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello</title>
    <style>
      .greeting {
        color: green;
      }
    </style>
  </head>
  <body>
    <div class="greeting">Hello World</div>
  </body>
</html>

В iex:

~/elixir_programs/tcp_server$ iex s1.ex 
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> HtmlServer.start

10:08:34.016 [info]  Accepting connections on port 9999

Затем откройте окно браузера и вставьте следующий адрес в адресную строку браузера:

http://localhost:9999
...