Ich dachte an eine alternative Spezifikation für Rack und WSGI (die Protokollspezifikation, nicht die Bibliothek (Rack.rb oder wsgiref.py)). Bitte beachten Sie, dass es möglicherweise nicht organisiert ist, weil ich gerade meine Ideen aufgeschrieben habe.
Ich denke, dieser Artikel wird in Zukunft mehrmals überarbeitet. Fühlen Sie sich frei zu kommentieren, wenn Sie irgendwelche Kommentare haben.
Rubys Rack und Pythons WSGI sind Spezifikationen, die HTTP-Anforderungen und -Antworten abstrahieren.
Zum Beispiel in Rack:
class RackApp
def call(env) #env ist ein Hash-Objekt, das die Anforderung darstellt
status = 200 #Statuscode
headers = {"Content-Type"=>"text/plain"} #Header
body = "Hello" #Körper
return status, headers, [body] #Diese drei repräsentieren die Antwort
end
end
Die Spezifikationen, die HTTP-Anforderungen und -Antworten auf diese Weise abstrahieren, sind Rack für Ruby und WSGI für Python.
Auf diese Weise können Webanwendungen mit jedem Anwendungsserver (WEBrick, Unicorn, Puma, UWSGI, Kellnerin) verwendet werden, der Rack oder WSGI unterstützt. Sie können beispielsweise problemlos zwischen der Verwendung von WEBrick und der Kellnerin, die während der Entwicklung einfach zu verwenden sind, und der Verwendung von Unicorn, Puma und UWSGI in einer Produktionsumgebung wechseln.
Rack und WSGI wurden auch entwickelt, um das Hinzufügen von Funktionen mithilfe von sogenannten Dekorationsmustern zu vereinfachen. Zum Beispiel
Sie können dies tun, ohne Ihre Webanwendung zu ändern.
##Original Rack-Anwendung
app = RackApp()
##Fügen Sie beispielsweise Sitzungsfunktionen hinzu
require 'rack/sesison/cookie'
app = Rack::Session::Cookie.new(app,
:key => 'rack.session', :path=>'/',
:expire_after => 3600,
:secret => '54vYjDUSB0z7NO0ck8ZeylJN0rAX3C')
##Zeigen Sie beispielsweise detaillierte Fehler nur in einer Entwicklungsumgebung an
if ENV['RACK_ENV'] == "development"
require 'rack/showexceptions'
app = Rack::ShowExceptions(app)
end
Wrapper-Objekte zum Hinzufügen von Funktionen zur ursprünglichen Webanwendung auf diese Weise werden in Rack und WSGI als "Middleware" bezeichnet. Im obigen Beispiel sind "Rack :: Session :: Cookie" und "Rack :: ShowException" Middleware.
WSGI ist die ursprüngliche Spezifikation für Rack. Ohne WSGI wäre Rack nicht geboren worden.
Als WSGI zum ersten Mal erschien, gab es ein ähnliches Java-Servlet. Die Servlet-Spezifikationen waren jedoch recht komplex und schwer zu implementieren [^ 1]. Aufgrund der Komplexität der Spezifikationen kann sich das Verhalten von Anwendungsserver zu Anwendungsserver geringfügig unterscheiden. Letztendlich war jeder in der Lage, die Spezifikationen durch Ausführen von Tomcat, einer Referenzimplementierung, zu überprüfen, ohne die Spezifikationen zu berücksichtigen.
Deshalb kam WSGI als sehr einfache Sache mit völlig anderen Spezifikationen heraus, obwohl ich mit der Idee von Servlet sympathisiere.
[^ 1]: Java und IBM sind gut darin, Dinge unnötig kompliziert zu machen.
Schauen wir uns den spezifischen Code an. Unten finden Sie den WSGI-Beispielcode.
class WSGIApp(object):
##environ ist ein Hash, der die Anforderung darstellt(Wörterbuch)Objekt
def __call__(self, environ, start_response):
status = "200 OK" #Zeichenfolgen, keine Zahlen
headers = [ #Liste der Schlüssel und Werte, keine Hashes
('Content-Type', 'text/plain'),
]
start_response(status, headers) #Starten Sie eine Antwort
return [b"Hello World"] #Gib den Körper zurück
Wenn Sie sich das ansehen, können Sie sehen, dass es ganz anders ist als Rack.
Meiner Meinung nach ist das größte Problem bei WSGI die Existenz einer Rückruffunktion namens "start_response ()". Aus diesem Grund müssen Anfänger zuerst "Funktionen, die Funktionen empfangen (Funktionen höherer Ordnung)" verstehen, um WSGI zu verstehen, das eine hohe Schwelle darstellt [^ 2].
[^ 2]: Fortgeschrittene Benutzer, die sagen: "Sie können Funktionen höherer Ordnung leicht verstehen", sind grundsätzlich nicht in der Lage zu verstehen, wo Anfänger stolpern. Sie sind also Funktionstypen, ohne sich mit Anfängern befassen zu müssen. Bitte kehren Sie in die Welt der Sprachen zurück. Kein großartiger Spieler oder Manager. Eine Person, die vielseitig im Sport ist, ist nicht für das Unterrichten von Onchi-Übungen geeignet.
Das Aufrufen einer WSGI-Anwendung wird auch wegen start_response ()
verschwendet. Das ist wirklich mühsam.
##Wenn Sie so etwas nicht einzeln vorbereiten
class StartResponse(object):
def __call__(self, status, headers):
self.status = status
self.headers = headers
##WSGI-Anwendung kann nicht aufgerufen werden
app = WSGIApplication()
environ = {'REQUEST_METHOD': 'GET', ...(snip)... }
start_response = StartResponse()
body = app.__call__(environ, start_response)
print(start_response.status)
print(start_response.headers)
(Tatsächlich wurde für WSGI (PEP-333) in der Vergangenheit eine Spezifikation namens Web3 (PEP-444) vorgeschlagen, die diesen Punkt verbessert. In diesem Web3 wurde die Rückruffunktion abgeschafft und ähnelt Rack. Es wurde entwickelt, um "Status, Header, Body" zurückzugeben. Ich persönlich habe es erwartet, aber es wurde am Ende nicht übernommen. Es tut mir leid.)
Bei WSGI ist es außerdem etwas ärgerlich, dass der Antwortheader eine Liste von Schlüsseln und Werten anstelle eines Hash-Objekts (Wörterbuchs) enthält. Das liegt daran, dass Sie die Liste jedes Mal durchsuchen müssen, wenn Sie einen Header festlegen.
##Zum Beispiel, wenn Sie einen solchen Antwortheader haben
resp_headers = [
('Content-Type', "text/html"),
('Content-Disposition', "attachment;filename=index.html"),
('Content-Encoding', "gzip"),
]
##Sie müssen die Liste einzeln durchsuchen, um den Wert festzulegen
key = 'Content-Length'
val = str(len(content))
for i, (k, v) in enumerate(resp_headers):
if k == key: # or k.tolower() == key.tolower()
break
else:
i = -1
if i >= 0: #Überschreiben, falls vorhanden
resp_headers[i] = (key, val)
else: #Wenn nicht, fügen Sie hinzu
resp_headers.append((key, val))
Das ist ein Ärger. Es wäre schön, eine dedizierte Dienstprogrammfunktion zu definieren, aber es war trotzdem besser, ein Hash-Objekt (Wörterbuch) zu verwenden.
##Hash-Objekt(Wörterbuchobjekt)Dann ...
resp_headers = {
'Content-Type': "text/html",
'Content-Disposition': "attachment;filename=index.html",
'Content-Encoding': "gzip",
]
##Sehr einfach, den Wert einzustellen!
## (Es wird jedoch davon ausgegangen, dass der Fall des Schlüsselnamens einheitlich ist.)
resp_headers['Content-Length'] = str(len(content))
Rack (Ruby) ist eine Spezifikation, die unter Bezugnahme auf WSGI (Python) festgelegt wurde. Das Rack ist dem WSGI sehr ähnlich, wurde jedoch verbessert, um es einfacher zu machen.
class RackApp
def call(env) #env ist ein Hash-Objekt, das die Anforderung darstellt
status = 200
headers = {
'Content-Type' => 'text/plain;charset=utf-8',
}
body = "Hello World"
return status, headers, [body] #Diese drei repräsentieren die Antwort
end
end
Die spezifischen Unterschiede sind wie folgt.
In Rack wird der Antwortheader nun durch ein Hash-Objekt dargestellt. Was ist in diesem Fall mit Headern, die mehrfach angezeigt werden können, z. B. "Set-Cookie"?
In Rack-Spezifikationen gibt es die folgende Beschreibung.
The values of the header must be Strings, consisting of lines (for multiple header values, e.g. multiple Set-Cookie values) separated by "\n".
Mit anderen Worten, wenn der Wert des Headers eine mehrzeilige Zeichenfolge ist, wird davon ausgegangen, dass der Header mehrmals angezeigt wurde.
Aber was ist mit dieser Spezifikation? Das liegt daran, dass wir herausfinden müssen, ob jeder Antwortheader ein Unterbrechungszeichen enthält. Dies verringert die Leistung.
headers.each do |k, v|
v.split(/\n/).each do |s| #← Doppelschleife;-(
puts "#{k}: #{s}"
end
end
Stattdessen scheint die Angabe, dass "mehrfach angezeigter Header den Wert zu einem Array macht", besser zu sein.
headers.each do |k, v|
if v.is_a?(Array) #← Das ist besser
v.each {|s| puts "#{k}: #{s}" }
else
puts "#{k}: #{v}"
end
end
Alternativ können Sie nur den Set-Cookie-Header speziell behandeln. Der einzige Header, der mehrmals angezeigt werden kann, ist Set-Cookie [^ 3], daher ist diese Spezifikation auch nicht schlecht.
set_cookie = "Set-Cookie"
headers.each do |k, v|
if k == set_cookie # ← Set-Sonderbehandlung nur für Kekse
v.split(/\n/).each {|s| puts "#{k}: #{s}" }
else
puts "#{k}: #{v}"
end
end
[^ 3]: Ich denke, es gab einen anderen Via-Header, der jedoch nicht in der Kategorie Rack oder WSGI behandelt wird. Sie sollten daher nur Set-Cooki in Betracht ziehen.
Ein weiterer Punkt betrifft die Methode close () des Antwortkörpers. Die Rack- und WSGI-Spezifikationen geben an, dass der Anwendungsserver "close ()" aufruft, wenn ein Antworttextobjekt eine Methode namens "close ()" hat, wenn die Antwort auf den Client abgeschlossen ist. Dies ist eine Spezifikation, die hauptsächlich davon ausgeht, dass der Antworttext ein Dateiobjekt ist.
def call(env)
filename = "logo.png "
headers = {'Content-Type' => "image/png",
'Content-Length' => File.size(filename).to_s}
##Öffne die Datei
body = File.open(filename, 'rb')
##Die geöffnete Datei wird vom App-Server gesendet, wenn die Antwort abgeschlossen ist.
##Automatisch schließen()Wird genannt
return [200, headers, body]
end
Dies scheint jedoch alles zu sein, was Sie tun müssen, ist die Datei am Ende der each ()
-Methode zu schließen.
class AutoClose
def initialize(file)
@file = file
end
def each
##Dies ist nicht effizient, da es Zeile für Zeile gelesen wird
#@file.each |line|
# yield line
#end
##Es ist effizienter, in einer größeren Größe zu lesen
while (s = @file.read(8192))
yield s
end
ensure #Wenn Sie alle Dateien gelesen haben oder wenn ein Fehler vorliegt
@file.close() #Automatisch schließen
end
end
Diese Spezifikation des Aufrufs, wenn es eine "close ()" - Methode gibt, kann in Fällen erforderlich sein, in denen die "each ()" - Methode des Antwortkörpers niemals aufgerufen wird. Persönlich denke ich, ich hätte über die Bereinigungsspezifikationen wie teardown ()
in xUnit nachdenken sollen, anstatt über die Spezifikationen "Ich denke nur an Dateiobjekte" (obwohl). Ich habe auch keine gute Idee.
Sowohl in Rack als auch in WSGI werden HTTP-Anforderungen als Hash-Objekte (Wörterbuchobjekte) dargestellt. Dies wird in den Rack- und WSGI-Spezifikationen als Umgebung bezeichnet.
Mal sehen, wie das aussieht.
## Filename: sample1.ru
require 'rack'
class SampleApp
## Inspect Environment data
def call(env)
status = 200
headers = {'Content-Type' => "text/plain;charset=utf-8"}
body = env.map {|k, v| "%-25s: %s\n" % [k.inspect, v.inspect] }.join()
return status, headers, [body]
end
end
app = SampleApp.new
run app
Als ich dies mit rampup sample1.ru -E Production -s puma -p 9292
ausführte und in meinem Browser auf http: // localhost: 9292 / index? X = 1 zugegriffen habe, habe ich beispielsweise das folgende Ergebnis erhalten. Dies ist der Inhalt der Umgebung.
"rack.version" : [1, 3]
"rack.errors" : #<IO:<STDERR>>
"rack.multithread" : true
"rack.multiprocess" : false
"rack.run_once" : false
"SCRIPT_NAME" : ""
"QUERY_STRING" : "x=1"
"SERVER_PROTOCOL" : "HTTP/1.1"
"SERVER_SOFTWARE" : "2.15.3"
"GATEWAY_INTERFACE" : "CGI/1.2"
"REQUEST_METHOD" : "GET"
"REQUEST_PATH" : "/index"
"REQUEST_URI" : "/index?x=1"
"HTTP_VERSION" : "HTTP/1.1"
"HTTP_HOST" : "localhost:9292"
"HTTP_CACHE_CONTROL" : "max-age=0"
"HTTP_COOKIE" : "_ga=GA1.1.1305719166.1445760613"
"HTTP_CONNECTION" : "keep-alive"
"HTTP_ACCEPT" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"HTTP_USER_AGENT" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9"
"HTTP_ACCEPT_LANGUAGE" : "ja-jp"
"HTTP_ACCEPT_ENCODING" : "gzip, deflate"
"HTTP_DNT" : "1"
"SERVER_NAME" : "localhost"
"SERVER_PORT" : "9292"
"PATH_INFO" : "/index"
"REMOTE_ADDR" : "::1"
"puma.socket" : #<TCPSocket:fd 14>
"rack.hijack?" : true
"rack.hijack" : #<Puma::Client:0x3fd60649ac48 @ready=true>
"rack.input" : #<Puma::NullIO:0x007fac0c896060>
"rack.url_scheme" : "http"
"rack.after_reply" : []
(Rack.hijack ist eine neue Funktion, die in Rack 1.5 eingeführt wurde. Weitere Informationen finden Sie hier.)
Diese Umgebung enthält drei Arten von Daten.
Umwelt ist eine Sammlung dieser Elemente. Persönlich mag ich diese Art von Spezifikation nicht und ich möchte, dass Sie zumindest den Anforderungsheader und den Rest trennen.
Der Grund für diese Spezifikation ist, dass sie auf der CGI-Spezifikation basiert. Ich glaube nicht, dass junge Leute heute etwas über CGI wissen, aber deshalb wurde es in der Vergangenheit sehr oft verwendet. WSGI hat diese CGI-Spezifikation ausgeliehen, um die Umgebungsspezifikation zu bestimmen, und Rack erbt sie. Daher kann es für diejenigen, die CGI nicht kennen, seltsam aussehen. Möglicherweise erhalten Sie den Eindruck "Warum wird der User-Agent-Header in HTTP_USER_AGENT geändert? Sie können einfach die User-Agent-Zeichenfolge verwenden."
Wie wir bereits gesehen haben, ist ein Umgebungsobjekt ein Hash-Objekt, das Dutzende von Elementen enthält.
Unter Leistungsgesichtspunkten ist das Erstellen eines Hash-Objekts mit Dutzenden von Elementen nicht wünschenswert, da der Betrieb in Ruby und Python recht teuer ist. Mit Keight.rb, einem Framework, das 100-mal schneller als Ruby on Rails ist, ** kann das Generieren eines Umgebungsobjekts ** möglicherweise länger dauern als das Verarbeiten einer Anforderung **.
Lassen Sie es uns tatsächlich mit einem Benchmark-Skript überprüfen.
# -*- coding: utf-8 -*-
require 'rack'
require 'keight'
require 'benchmark/ips'
##Aktionsklasse(Controller in MVC)Erstellen
class API < K8::Action
mapping '/hello', :GET=>:say_hello
def say_hello()
return "<h1>Hello, World!</h1>"
end
end
##Erstellen Sie eine Rack-Anwendung und weisen Sie eine Aktionsklasse zu
mapping = [
['/api', API],
]
rack_app = K8::RackApplication.new(mapping)
##Ausführungsbeispiel
expected = [
200,
{"Content-Length"=>"22", "Content-Type"=>"text/html; charset=utf-8"},
["<h1>Hello, World!</h1>"]
]
actual = rack_app.call(Rack::MockRequest.env_for("/api/hello"))
actual == expected or raise "assertion failed"
## GET /api/Umgebungsobjekt, das Hallo darstellt
env = Rack::MockRequest.env_for("/api/hello")
##Benchmark
Benchmark.ips do |x|
x.config(:time => 5, :warmup => 1)
##Erstellen Sie ein neues Umgebungsobjekt(eine Kopie machen)
x.report("just copy env") do |n|
i = 0
while (i += 1) <= n
env.dup()
end
end
##Erstellen Sie ein Umgebungsobjekt, um die Anforderung zu verarbeiten
x.report("Keight (copy env)") do |n|
i = 0
while (i += 1) <= n
actual = rack_app.call(env.dup)
end
actual == expected or raise "assertion failed"
end
##Verwenden Sie Umgebungsobjekte erneut, um Anforderungen zu verarbeiten
x.report("Keight (reuse env)") do |n|
i = 0
while (i += 1) <= n
actual = rack_app.call(env)
end
actual == expected or raise "assertion failed"
end
x.compare!
end
Als ich dies ausführte, erhielt ich zum Beispiel die folgenden Ergebnisse (Ruby 2.3, Keight.rb 0.2, OSX El Capitan):
Calculating -------------------------------------
just copy env 12.910k i/100ms
Keight (copy env) 5.523k i/100ms
Keight (reuse env) 12.390k i/100ms
-------------------------------------------------
just copy env 147.818k (± 8.0%) i/s - 735.870k
Keight (copy env) 76.103k (± 4.4%) i/s - 381.087k
Keight (reuse env) 183.065k (± 4.8%) i/s - 916.860k
Comparison:
Keight (reuse env): 183064.5 i/s
just copy env: 147818.2 i/s - 1.24x slower
Keight (copy env): 76102.8 i/s - 2.41x slower
Aus den letzten drei Zeilen können wir Folgendes ersehen:
In dieser Situation wird die Anwendung durch eine weitere Beschleunigung des Frameworks nicht viel schneller. Um diesen Deadlock zu überwinden, sollten die Rack-Spezifikationen selbst verbessert werden.
(TODO)
Nun, kommen Sie endlich zum Hauptthema.
Um die Probleme zu lösen, die ich bisher beschrieben habe, habe ich eine Alternative zum aktuellen Rack und WSGI in Betracht gezogen. Sogenannt "Meine Gedanken zu Saikyo no Raku".
Die neue Spezifikation ändert nichts an der Abstraktion von HTTP-Anforderungen und -Antworten. Also werde ich mich darauf konzentrieren, wie man diese beiden abstrahiert.
Außerdem erben das aktuelle Rack und WSGI teilweise die CGI-Spezifikationen. CGI ist jedoch eine altmodische Spezifikation, bei der davon ausgegangen wird, dass Daten über Umgebungsvariablen übertragen werden. Es ist nicht für diese Ära geeignet, daher können Sie die CGI-Spezifikationen vergessen.
HTTP-Anforderungen sind in folgende Elemente unterteilt:
Die Anforderungsmethode kann eine obere Zeichenfolge oder ein Symbol sein. Das Symbol scheint in Bezug auf die Leistung besser zu sein.
meth = :GET
Der Anforderungspfad kann eine Zeichenfolge sein. Das Rack muss sowohl SCRIPT_NAME als auch PATH_INFO berücksichtigen, aber jetzt, da niemand SCRIPT_NAME verwenden wird, werden wir nur das Äquivalent von PATH_INFO berücksichtigen.
path = "/index.html"
Der Anforderungsheader kann ein Hash-Objekt sein. Außerdem möchte ich nicht von User-Agent zu HTTP_USER_AGENT konvertieren, aber HTTP / 2 scheint niedrigere Headernamen zu haben, daher werde ich wahrscheinlich damit übereinstimmen.
headers = {
"host" => "www.example.com",
"user-agent" => "Mozilla/5.0 ....(snip)....",
....(snip)....,
}
Der Abfrageparameter ist entweder "nil" oder eine Zeichenfolge. Wenn es kein "?" Gibt, wird es zu "null", und wenn es existiert, wird es zu einer Zeichenfolge (es kann ein leeres Zeichen sein).
query = "x=1"
E / A-bezogen (Rack.Eingang und Rack.Fehler und Rack.hijack oder Puma.Socket) sollten sich in einem Array befinden. Dies sind nur die Äquivalente von stdin, stderr und stdout ... nicht wahr? Vielleicht dient der Socket auch als Rack-Eingang, aber ich bin nicht damit vertraut, deshalb werde ich ihn hier trennen.
ios = [
StringIO.new(), # rack.input
$stderr, # rack.errors
puma_socket,
]
Der Wert anderer Anforderungsinformationen ändert sich für jede Anforderung. Dies sollte ein Hash-Objekt sein.
options = {
http: "1.1", # HTTP_VERSION
client: "::1", # REMOTE_ADDR
protocol: "http", # rack.url_scheme
}
Die letzten Serverinformationen sollten sich nur ändern, wenn sich der Anwendungsserver geändert hat. Sobald Sie es als Hash-Objekt erstellt haben, können Sie es wiederverwenden.
server = {
name: "localhost".freeze, # SERVER_NAME
port: "9292".freeze, # SERVER_PORT
'rack.version': [1, 3].freeze,
'rack.multithread': true,
'rack.multiprocess': false,
'rack.run_once': false,
}.freeze
Stellen Sie sich eine Rack-Anwendung vor, die diese empfängt.
class RackApp
def call(meth, path, headers, query, ios, options, server)
input, errors, socket = ios
...
end
end
Wow, es hat 7 Argumente. Das ist ein bisschen cool, nicht wahr? Die ersten drei (meth, path und headers) bilden den Kern der Anforderung. Wenn Sie sie als Argumente, Abfragen und ios in Ruhe lassen, werden sie wahrscheinlich in Optionen gruppiert.
options = {
query: "x=1", # QUERY_STRING
#
input: StringIO.new, # rack.input,
error: $stderr, # rack.erros,
socket: puma_socket, # rack.hijack or puma.socket
#
http: "1.1", # HTTP_VERSION
client: "::1", # REMOTE_ADDR
protocol: "http", # rack.url_scheme
}
Dadurch wird die Anzahl der Argumente von sieben auf fünf reduziert.
class RackApp
def call(meth, path, headers, options, server)
query = options[:query]
input = options[:input]
error = options[:error]
socket = options[:socket] # or :output ?
...
end
end
Nun, ich denke es ist okay das zu benutzen.
Die HTTP-Antwort kann weiterhin durch Status, Header und Body dargestellt werden.
def call(meth, path, headers, options, server)
status = 200
headers = {"content-type"=>"application/json"},
body = '{"message":"Hello!"}'
return status, headers, body
end
Ich denke jedoch, dass der Content-Type-Header speziell behandelt werden kann. Denn in heutigen Rack-Anwendungen gibt es nur Content-Type-Header wie "{" Content-Type "=>" text / html} "und" {"Content-Type" => "application / json"} ". Dies liegt daran, dass es viele Fälle gibt, in denen es nicht enthalten ist. Wenn daher nur der Inhaltstyp speziell behandelt und unabhängig gemacht wird, ist dies etwas einfacher.
def call(meth, path, headers, options, server)
##Als das
return 200, {"Content-Type"=>"text/plain"}, ["Hello"]
##Dies ist prägnanter
return 200, "text/plain", {}, ["Hello"]
end
Es gibt noch einige andere Probleme.
Da die meisten Antworten einen String als Body zurückgeben, ist es sinnlos, ihn einzeln in ein Array zu packen. Wenn möglich, sollte der Body eine "Zeichenfolge" oder ein Objekt sein, das eine Zeichenfolge mit "each ()" zurückgibt. </ dd>
Aber was wirklich wünschenswert ist, ist das Äquivalent von "teardown ()". Schade, dass mir keine konkreten Spezifikationen einfallen [^ 4]. </ dd>
[^ 4]: Ich dachte, dass "ack.after_reply "das war, aber es scheint eine einzigartige Funktion von Puma zu sein.
(TODO)
(TODO)
(TODO)
Wir würden gerne die Meinungen von Experten hören.
Ich habe gerade eine Frage in Racks Mailingliste zur HTTP2-Unterstützung erhalten (https://groups.google.com/forum/#!topic/rack-devel/_sPwu9vVsYA). Es gab ein kleines Gespräch über Rack2, also habe ich verschiedene Dinge durchgearbeitet.
Recommended Posts