---
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)

# 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"]
```