网络服务器里有什么?
网络服务器是一个接受TCP套接字,从套接字中读取信息,解析HTTP头信息,并以HTTP正文进行响应的东西。它是一个基于文本的协议,很容易实现。
下面是一个请求示例(你从套接字中读出的内容)。
GET / HTTP/1.1
Host: localhost:10000
User-Agent: curl/7.64.1
Accept: */*
和一个响应样本(你会写什么)。
HTTP/1.1 200
Content-Type: text/html
Hello world
我们将从AppSignal的[Building a 30 line HTTP server in Ruby]帖子中抓取一个Gist。
require 'socket'
server = TCPServer.new(8080)
while session = server.accept
request = session.gets
puts request
session.print "HTTP/1.1 200\r\n"
session.print "Content-Type: text/html\r\n"
session.print "\r\n"
session.print "Hello world! The time is #{Time.now}"
session.close
end
开始使用Ractor
现在,让我们把上面的例子包装成Ractor。
require 'socket'
server = TCPServer.new(8080)
CPU_COUNT = 4
workers = CPU_COUNT.times.map do
Ractor.new do
loop do
# receive TCPSocket
s = Ractor.recv
request = s.gets
puts request
s.print "HTTP/1.1 200\r\n"
s.print "Content-Type: text/html\r\n"
s.print "\r\n"
s.print "Hello world! The time is #{Time.now}\n"
s.close
end
end
end
loop do
conn, _ = server.accept
# pass TCPSocket to one of the workers
workers.sample.send(conn, move: true)
end
我们启动相当于CPU数量的worker,让主线程监听套接字上的连接,并将接受的连接发送到一个随机的Ractor。我们可以通过使用curl 进行请求来验证它的工作是否符合预期。
然而,使用workers.sample ,在工作者之间分配请求并不是很有效。那个随机的工作器可能还在忙着为之前的请求提供服务。我们宁愿让工人从一个共享队列中抽出,在那里我们会发送所有的请求。
我想把这部分做得更好,但我没有找到任何适合Ractor的队列实现。然而,文档中建议使用一个像队列一样的管道。让我们试试吧!
require 'socket'
# pipe aka a queue
pipe = Ractor.new do
loop do
Ractor.yield(Ractor.recv, move: true)
end
end
CPU_COUNT = 4
workers = CPU_COUNT.times.map do
Ractor.new(pipe) do |pipe|
loop do
s = pipe.take
data = s.recv(1024)
puts data.inspect
s.print "HTTP/1.1 200\r\n"
s.print "Content-Type: text/html\r\n"
s.print "\r\n"
s.print "Hello world!\n"
s.close
end
end
end
server = TCPServer.new(8080)
loop do
conn, _ = server.accept
pipe.send(conn, move: true)
end
它成功了!通过使用管道,我能够让所有的工作者拉动套接字,这改善了负载平衡的部分。
但不太妙的是,没有任何东西可以监控工作者,以防其中一个工作者意外死亡。与Puma的架构类似,如果有一个单独的线程来等待套接字准备好,然后再把它们传递给实际的工作者,会更有效率。
我能够把监听器移到它自己的Ractor中,并让主线程观察所有的Ractor。
require 'socket'
pipe = Ractor.new do
loop do
Ractor.yield(Ractor.recv, move: true)
end
end
CPU_COUNT = 4
workers = CPU_COUNT.times.map do
Ractor.new(pipe) do |pipe|
loop do
s = pipe.take
puts "taken from pipe by #{Ractor.current}"
data = s.recv(1024)
puts data.inspect
s.print "HTTP/1.1 200\r\n"
s.print "Content-Type: text/html\r\n"
s.print "\r\n"
s.print "Hello world!\n"
s.close
end
end
end
listener = Ractor.new(pipe) do |pipe|
server = TCPServer.new(8080)
loop do
conn, _ = server.accept
pipe.send(conn, move: true)
end
end
loop do
Ractor.select(listener, *workers)
# if the line above returned, one of the workers or the listener has crashed
end
又一次,它成功了!
实现Web服务器的下一步是建立一个HTTP解析器来读取请求头。有一个http-parsergem,它使用C语言扩展,我听说Ractor还不支持这个。
我找到了一个HTTP解析器,作为WEBrick的一部分,它是Ruby标准库中的一部分。
我试了一下下面的片段。
require 'webrick'
CPU_COUNT = 4
workers = CPU_COUNT.times.map do
Ractor.new(pipe) do |pipe|
loop do
s = pipe.take
# raises "can not access non-sharable objects in constant HTTP by non-main Ractors (NameError)"
req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
req.parse(s)
s.print "HTTP/1.1 200\r\n"
s.print "Content-Type: text/html\r\n"
s.print "\r\n"
s.print "Hello world!\n"
s.close
end
end
end
WEBrick::Config::HTTP 变成了一个带有一些配置对象的可变哈希值。由于该常量和哈希值是在主线程中初始化的,所以不允许从ractors中安全使用。我通过内联哈希定义来解决这个问题,但后来我遇到了另一个从WEBrick代码中引用的不可共享的常量,这个常量也不容易内联。
这可能是很快会在上游改进的部分。毕竟,这是最早的Ractor实现。
结束语
Ractor模型似乎很强大,并且已经准备好进行实验性使用。在接下来的6个月内(Ruby 3.0版本计划在12月发布),我预见到基于Ractor的网络服务器将会出现,以利用这一特性并最大限度地利用服务器CPU。这是一个学习并发编程和为Ruby社区作出贡献的好机会。