--- tags: minimeet, Ruby --- # 使用 Ruby 建立 HTTP Server > 參考資料來源:https://gorails.com/episodes/ruby-http-server-from-scratch 我們想建立像 Puma、Thin 或 Unicorn 這樣 Web Server 這些 Web Server 會監聽 TCP socket 端的訊息 我們可以用 HTTP protocol 解析訊息,然後回應 HTTP response # NetCat Tool ```bash! $ nc google.com 80 $ hello # invalid request 400 Bad Request # response ``` ```bash! $ nc google.com 80 $ GET / HTTP/1.1 $ 200 OK ``` 上面是 TCP connection 實際上工作的方式 我們在服務器之間發送和接收訊息 而這些訊息需要以 HTTP protocol 的特定格式編寫 [HTTP Request format](https://notfalse.net/39/http-message-format) ![](https://i.imgur.com/WYiONxZ.png) # Setup TCPserver Ruby 的 [Socket](https://ruby-doc.org/stdlib-3.0.3/libdoc/socket/rdoc/Socket.html) library 提供了存取底層作業系統 socket 的實作 socket 是雙向通信通道的端點 [TCPServer](https://ruby-doc.org/stdlib-3.0.3/libdoc/socket/rdoc/TCPServer.html) ```ruby! # server.rb require 'socket' port = ENV.fetch('PORT', 2000).to_i server = TCPServer.new port # Server bind to port 2000 loop do client = server.accept # Wait for a client to connect client.puts 'Hello World' client.puts "It is #{Time.now}" client.close end ``` 當我們執行 `server.rb` 並且訪問時: ```bash! $ nc localhost 2000 Hello World It is 2022-08-25 14:00:57 +0800 ``` ## Multi Connections at the same time ```ruby! # server.rb require 'socket' port = ENV.fetch('PORT', 2000).to_i server = TCPServer.new port puts "Listenting on port #{port}..." loop do client = server.accept client.puts 'Hello World' client.puts "It is #{Time.now}" sleep 5 client.close end ``` 目前為止我們的 server 是單線程的 一次只能處理一個 connection 而其它的 connections 則必須等待前一個 connection 執行完畢 因此我們可以使用 [Thread](https://ruby-doc.org/core-3.0.3/Thread.html) 讓 server 能多線程執行 ```ruby! # server.rb require 'socket' port = ENV.fetch('PORT', 2000).to_i server = TCPServer.new port puts "Listenting on port #{port}..." loop do Thread.start(server.accept) do |client| client.puts 'Hello World' client.puts "It is #{Time.now}" sleep 5 client.close end end ``` # Get Request Messages 接下來我們需要解析對方傳送過來的 request 訊息 ```ruby! # server.rb require 'socket' port = ENV.fetch('PORT', 2000).to_i server = TCPServer.new port puts "Listenting on port #{port}..." loop do Thread.start(server.accept) do |client| request = client.readpartial(2048) p request client.close end end ``` ```bash! $ curl -h -v, --verbose Make the operation more talkative ``` ```bash! $ curl -v localhost:2000 * Trying 127.0.0.1:2000... * Connected to localhost (127.0.0.1) port 2000 (#0) > GET / HTTP/1.1 > Host: localhost:2000 > User-Agent: curl/7.79.1 > Accept: */* > * Empty reply from server * Closing connection 0 curl: (52) Empty reply from server ``` 利用 [IO#readpartial](https://ruby-doc.org/core-3.0.3/IO.html#method-i-readpartial) 我們可以拿到用戶端的 request 字串 ```bash! $ ruby server.rb Listenting on port 2000... "GET / HTTP/1.1\r\nHost: localhost:2000\r\nUser-Agent: curl/7.79.1\r\nAccept: */*\r\n\r\n" ``` 再來可以使用 [String#lines](https://ruby-doc.org/core-3.0.3/String.html#method-i-lines) 將 request 訊息以 `\r\n` 做分割 ```ruby! $ irb > request = "GET / HTTP/1.1\r\nHost: localhost:2000\r\nUser-Agent: curl /7.79.1\r\nAccept: */*\r\n\r\n" => "GET / HTTP/1.1\r\nHost: localhost:2000\r\nUser-Agent: curl/7.79.1\r\nAc... > request.lines => ["GET / HTTP/1.1\r\n", "Host: localhost:2000\r\n", "User-Agent: curl/7.79.1\r\n", "Accept: */*\r\n", "\r\n"] ``` 我們知道的第一行是一個請求,包含 HTTP method、path 和 HTTP protocol 然後緊接著是 headers 最後是 `\r\n` 空行 所以我們可以依照需求去處理每一行 並確保我們已經建立了相對應的 HTTP method、path 和 headers 讓我們來看看帶有 body 資料的 POST request ```bash! $ curl -h -d, --data <data> HTTP POST data ``` ```bash! $ curl -v localhost:2000 -d 'Hello world' * Trying 127.0.0.1:2000... * Connected to localhost (127.0.0.1) port 2000 (#0) > POST / HTTP/1.1 > Host: localhost:2000 > User-Agent: curl/7.79.1 > Accept: */* > Content-Length: 11 > Content-Type: application/x-www-form-urlencoded > * Empty reply from server * Closing connection 0 curl: (52) Empty reply from server ``` 這是一個 POST request 與 GET 有點不同 headers 多出了 `Content-Type`、`Content-Length` 以及空行之後的 body `Hello world` 我們可以看到 `Hello world` 被放在最後一行 因此當發出 POST request 時 實際上將送出:請求、headers、空行,最後才是資料 ```bash! $ ruby server.rb Listenting on port 2000... "POST / HTTP/1.1\r\nHost: localhost:2000\r\nUser-Agent: curl/7.79.1\r\nAccept: */*\r\nContent-Length: 11\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nHello world" ``` ```ruby! $ irb > request = "POST / HTTP/1.1\r\nHost: localhost:2000\r\nUser-Agent: cur l/7.79.1\r\nAccept: */*\r\nContent-Length: 11\r\nContent-Type: application/x-www-f orm-urlencoded\r\n\r\nHello world" => "POST / HTTP/1.1\r\nHost: localhost:2000\r\nUser-Agent: curl/7.79.1\r\nA... > request.lines => ["POST / HTTP/1.1\r\n", "Host: localhost:2000\r\n", "User-Agent: curl/7.79.1\r\n", "Accept: */*\r\n", "Content-Length: 11\r\n", "Content-Type: application/x-www-form-urlencoded\r\n", "\r\n", "Hello world"] ```