如何将这个简单的Elixir回显服务器转换为简单的Web服务器?

时间:2019-05-15 09:31:06

标签: elixir elixir-framework

我正在尝试为elixir设置回显服务器,为了简单起见,它采用简单的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 个答案:

答案 0 :(得分:2)

让我们弄清楚一些事情。服务器可以返回一些html,但是它是显示html的浏览器。例如,如果您在浏览器的地址栏中键入一个地址,这将导致浏览器向服务器发送请求,然后服务器将响应(在响应正文中包含一些html)发送回浏览器进行解析从响应中提取出来并解释为彩色文本,图片等,称为 呈现html

请注意html只是以某种方式设置格式的文本,例如<div>hello</div>,因此对于服务器而言,返回html文件与返回文本文件没有什么不同。服务器实际上正在返回文件的内容-内容没有扩展名。您需要了解,所有文本都是通过“有线”发送的。

这是您需要做的:

  1. 创建一个侦听某个端口的tcp服务器,并在请求进入时返回一些html。

  2. 在浏览器的地址栏中,指定服务器正在侦听的地址(例如localhost,这是地址127.0.0.1,这是您的计算机上的地址),以及指定服务器正在监听,例如http://localhost:3456

有一个很大的障碍:服务器和客户端(例如浏览器)之间无法通信的通用协议。问题是由文本发送到TCP套接字的方式引起的。当您将一些文本发送到tcp套接字时,您不知道文本是否会分解成块,也不清楚每个块将持续多长时间。数据可以作为一个块发送,或者数据可以切成十个块。这给数据的接收者带来了一个问题:接收者如何知道什么时候应该停止尝试从套接字读取数据,因为没有更多的数据了?

要解决该问题,客户端和服务器端都必须就 协议 达成共识,这是数据的商定格式,以便另一端可以轻松地解析数据,以及已达成一致的信号,即已到达数据末尾,以便另一端可以停止尝试从套接字读取数据。最简单的协议是发送方在完成数据发送后关闭其套接字。然后,接收方将尝试继续从套接字读取数据,直到收到套接字错误,然后才知道不再有数据了。

或者,协议可能是在数据中遇到单词“ end”时-接收器在读取“ end”时应停止尝试从套接字读取。但是,这是一个可能导致问题的示例:

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个字节,接收器便知道数据的结尾。 / p>

Web浏览器和服务器已同意的协议为http requesthttp response协议。您可以看到http请求和响应格式here的一些示例。因为您正在编码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