J'ai pensé à une spécification alternative pour Rack et WSGI (la spécification du protocole, pas la bibliothèque (rack.rb ou wsgiref.py)). Veuillez noter que cela peut ne pas être organisé car je viens d'écrire mes idées.
Je pense que cet article sera révisé plusieurs fois dans le futur. N'hésitez pas à commenter si vous avez des commentaires.
Ruby Rack et Python WSGI sont des spécifications qui résument les requêtes et réponses HTTP.
Par exemple dans Rack:
class RackApp
def call(env) #env est un objet Hash qui représente la demande
status = 200 #Code d'état
headers = {"Content-Type"=>"text/plain"} #entête
body = "Hello" #corps
return status, headers, [body] #Ces trois représentent la réponse
end
end
Les spécifications qui résument les requêtes et réponses HTTP de cette manière sont Rack pour Ruby et WSGI pour Python.
Cela permet aux applications Web d'être utilisées avec n'importe quel serveur d'applications (WEBrick, Unicorn, Puma, UWSGI, serveuse) prenant en charge Rack ou WSGI. Par exemple, vous pouvez facilement basculer entre l'utilisation de WEBrick et de la serveuse, qui sont faciles à utiliser pendant le développement, et l'utilisation rapide de Unicorn, Puma et UWSGI dans un environnement de production.
Rack et WSGI sont également conçus pour faciliter l'ajout de fonctionnalités en utilisant des modèles dits décorateurs. Par exemple
Vous pouvez le faire sans modifier votre application Web.
##Application Rack d'origine
app = RackApp()
##Par exemple, ajoutez une fonctionnalité de session
require 'rack/sesison/cookie'
app = Rack::Session::Cookie.new(app,
:key => 'rack.session', :path=>'/',
:expire_after => 3600,
:secret => '54vYjDUSB0z7NO0ck8ZeylJN0rAX3C')
##Par exemple, afficher les erreurs détaillées uniquement dans un environnement de développement
if ENV['RACK_ENV'] == "development"
require 'rack/showexceptions'
app = Rack::ShowExceptions(app)
end
Les objets wrapper permettant d'ajouter des fonctionnalités à l'application Web d'origine de cette manière sont appelés "Middleware" dans Rack et WSGI. Dans l'exemple ci-dessus, Rack :: Session :: Cookie
et Rack :: ShowException
sont des intergiciels.
WSGI est la spécification d'origine pour Rack. Sans WSGI, Rack ne serait pas né.
Lorsque WSGI est apparu pour la première fois, il y avait un servlet Java similaire. Cependant, la spécification Servlet était assez compliquée et difficile à implémenter [^ 1]. De plus, en raison de la complexité des spécifications, le comportement peut légèrement différer d'un serveur d'applications à l'autre, donc au final, tout le monde était en état de vérifier les spécifications en exécutant Tomcat, qui est une implémentation de référence, sans regarder les spécifications.
C'est pourquoi WSGI est sorti comme une chose très simple avec des spécifications complètement différentes, bien que je sympathise avec l'idée de Servlet.
[^ 1]: Java et IBM sont bons pour compliquer inutilement les choses.
Regardons le code spécifique. Voici l'exemple de code WSGI.
class WSGIApp(object):
##environ est un hachage représentant la requête(dictionnaire)objet
def __call__(self, environ, start_response):
status = "200 OK" #Des chaînes, pas des nombres
headers = [ #Liste de clés et de valeurs, pas de hachages
('Content-Type', 'text/plain'),
]
start_response(status, headers) #Lancer une réponse
return [b"Hello World"] #Rendre le corps
Si vous regardez cela, vous pouvez voir que c'est assez différent de Rack.
200
) dans Rack, mais une chaîne (ex:"200 OK"
) dans WSGI.
Ceci est différent si vous utilisez votre propre code de statut.
Par exemple, si vous souhaitez utiliser votre propre code d'état "509 Bandwidth Limit Exceeded", il n'y a pas de problème avec WSGI, mais dans Rack, vous pouvez facilement spécifier "509" mais vous pouvez spécifier "Bandwidth Limit Exceeded" ( Pas dans les spécifications).str
(c'est-à-dire binaire pour Python2, chaîne Unicode pour Python3). Cependant, le corps de la réponse est toujours binaire (une liste de).Maintenant, à mon avis, le plus gros problème avec WSGI est l'existence d'une fonction de rappel appelée start_response ()
. Pour cette raison, les débutants doivent d'abord comprendre «les fonctions qui reçoivent des fonctions (fonctions d'ordre supérieur)» pour comprendre WSGI, qui est un seuil élevé [^ 2].
[^ 2]: Les utilisateurs avancés qui disent: "Vous pouvez facilement comprendre les fonctions d'ordre supérieur" manquent fondamentalement de capacité à comprendre où les débutants trébuchent, ce sont donc des types fonctionnels sans avoir à traiter avec des débutants. Veuillez retourner dans le monde des langues. Pas un grand joueur ou un grand manager. Une personne polyvalente dans le sport n'est pas apte à enseigner les onchi.
L'appel d'une application WSGI est également gaspillé à cause de start_response ()
. C'est vraiment gênant.
##Si vous ne préparez pas quelque chose comme celui-ci un par un
class StartResponse(object):
def __call__(self, status, headers):
self.status = status
self.headers = headers
##Impossible d'appeler l'application WSGI
app = WSGIApplication()
environ = {'REQUEST_METHOD': 'GET', ...(snip)... }
start_response = StartResponse()
body = app.__call__(environ, start_response)
print(start_response.status)
print(start_response.headers)
(En fait, pour WSGI (PEP-333), une spécification appelée Web3 (PEP-444) qui améliorait ce point a été proposée dans le passé. Dans ce Web3, la fonction de rappel est abolie et elle est similaire à Rack. Il a été conçu pour renvoyer "status, headers, body" à. Personnellement, je m'y attendais, mais il n'a finalement pas été adopté. Je suis désolé.)
WSGI est également un peu ennuyé par le fait que l'en-tête de réponse est une liste de clés et de valeurs au lieu d'un objet de hachage (dictionnaire). C'est parce que vous devez rechercher la liste chaque fois que vous définissez un en-tête.
##Par exemple, si vous avez un en-tête de réponse comme celui-ci
resp_headers = [
('Content-Type', "text/html"),
('Content-Disposition', "attachment;filename=index.html"),
('Content-Encoding', "gzip"),
]
##Vous devez rechercher la liste une par une pour définir la valeur
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: #Écraser s'il y a
resp_headers[i] = (key, val)
else: #Sinon, ajoutez
resp_headers.append((key, val))
C'est un problème. Ce serait bien de définir une fonction utilitaire dédiée, mais il valait quand même mieux utiliser un objet de hachage (dictionnaire).
##Objet de hachage(Objet dictionnaire)Alors ...
resp_headers = {
'Content-Type': "text/html",
'Content-Disposition': "attachment;filename=index.html",
'Content-Encoding': "gzip",
]
##Très facile de définir la valeur!
## (Cependant, on suppose que la casse du nom de clé est unifiée.)
resp_headers['Content-Length'] = str(len(content))
Rack (Ruby) est une spécification déterminée en référence à WSGI (Python). Rack est très similaire à WSGI, mais a été amélioré pour être plus simple.
class RackApp
def call(env) #env est un objet de hachage qui représente la demande
status = 200
headers = {
'Content-Type' => 'text/plain;charset=utf-8',
}
body = "Hello World"
return status, headers, [body] #Ces trois représentent la réponse
end
end
Les différences spécifiques sont les suivantes.
Content-Type '' ou
content-type '', cela peut être un problème, alors gardez-la unifiée. est nécessaire).Désormais, dans Rack, l'en-tête de réponse est représenté par un objet de hachage. Dans ce cas, qu'en est-il des en-têtes qui peuvent apparaître plusieurs fois, tels que «Set-Cookie»?
Dans Spécifications du rack, il y a la description suivante.
The values of the header must be Strings, consisting of lines (for multiple header values, e.g. multiple Set-Cookie values) separated by "\n".
En d'autres termes, si la valeur de l'en-tête est une chaîne multiligne, on considère que l'en-tête est apparu plusieurs fois.
Mais qu'en est-il de cette spécification? C'est parce que nous devons savoir si chaque en-tête de réponse contient un caractère de rupture. Cela réduira les performances.
headers.each do |k, v|
v.split(/\n/).each do |s| #← Double boucle;-(
puts "#{k}: #{s}"
end
end
Plutôt que cela, la spécification selon laquelle "l'en-tête qui apparaît plusieurs fois fait de la valeur un tableau" semble être meilleure.
headers.each do |k, v|
if v.is_a?(Array) #← C'est mieux
v.each {|s| puts "#{k}: #{s}" }
else
puts "#{k}: #{v}"
end
end
Vous pouvez également traiter uniquement l'en-tête Set-Cookie. Le seul en-tête qui peut apparaître plusieurs fois est Set-Cookie [^ 3], donc cette spécification n'est pas mauvaise non plus.
set_cookie = "Set-Cookie"
headers.each do |k, v|
if k == set_cookie # ← Set-Traitement spécial uniquement pour les cookies
v.split(/\n/).each {|s| puts "#{k}: #{s}" }
else
puts "#{k}: #{v}"
end
end
[^ 3]: Je pense qu'il y avait un autre en-tête Via, mais ce n'est pas couvert dans la catégorie Rack ou WSGI, donc vous ne devriez considérer que Set-Cooki.
Un autre point concerne la méthode close () 'du corps de la réponse. Les spécifications Rack et WSGI spécifient que si l'objet corps de la réponse a une méthode appelée
close (), le serveur d'application appellera
close ()` lorsque la réponse au client est terminée. Il s'agit d'une spécification supposant principalement que le corps de la réponse est un objet File.
def call(env)
filename = "logo.png "
headers = {'Content-Type' => "image/png",
'Content-Length' => File.size(filename).to_s}
##Ouvrez le fichier
body = File.open(filename, 'rb')
##Le fichier ouvert sera envoyé par le serveur d'application lorsque la réponse sera terminée.
##Fermer automatiquement()Est appelé
return [200, headers, body]
end
Mais il semble que tout ce que vous ayez à faire est de fermer le fichier à la fin de la méthode ʻeach () `.
class AutoClose
def initialize(file)
@file = file
end
def each
##Ce n'est pas efficace car il est lu ligne par ligne
#@file.each |line|
# yield line
#end
##Il est plus efficace de lire dans une taille plus grande
while (s = @file.read(8192))
yield s
end
ensure #Si vous lisez tous les fichiers ou s'il y a une erreur
@file.close() #Fermer automatiquement
end
end
Cette spécification à appeler s'il y a une méthode close ()
peut être nécessaire dans le cas où la méthode ʻeach () du corps de la réponse n'est jamais appelée. Personnellement, je pense que j'aurais dû envisager une spécification de nettoyage comme
teardown ()` dans xUnit plutôt qu'une spécification "penser uniquement à l'objet File" (bien que). Je n'ai pas non plus une bonne idée).
Dans Rack et WSGI, les requêtes HTTP sont représentées sous forme d'objets de hachage (dictionnaire). C'est ce qu'on appelle l'environnement dans le rack et les spécifications WSGI.
Voyons à quoi cela ressemble.
## 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
Quand j'ai exécuté ceci avec rackup sample1.ru -E production -s puma -p 9292
et que j'ai accédé à http: // localhost: 9292 / index? X = 1 dans mon navigateur, j'ai obtenu le résultat suivant, par exemple. C'est le contenu de l'environnement.
"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 est une nouvelle fonctionnalité introduite dans Rack 1.5. Pour plus d'informations, cliquez ici.)
Cet environnement contient trois types de données.
L'environnement est une collection de ces éléments. Personnellement, je n'aime pas ce genre de spécification, et j'aimerais que vous sépariez au moins l'en-tête de la requête du reste.
La raison de cette spécification est qu'elle est basée sur la spécification CGI. Je ne pense pas que les jeunes d'aujourd'hui connaissent CGI, mais c'est pourquoi il était très souvent utilisé dans le passé. WSGI a emprunté cette spécification CGI pour déterminer la spécification d'environnement, et Rack en hérite. Par conséquent, cela peut sembler étrange à ceux qui ne connaissent pas CGI. Quelqu'un pourrait dire: "Pourquoi l'en-tête User-Agent a-t-il été remplacé par HTTP_USER_AGENT? Je devrais simplement utiliser la chaîne User-Agent."
Comme nous l'avons déjà vu, un objet Environment est un objet de hachage contenant des dizaines d'éléments.
Du point de vue des performances, créer un objet de hachage avec des dizaines d'éléments n'est pas souhaitable car il est assez coûteux à utiliser en Ruby et Python. Par exemple, avec Keight.rb, un framework 100 fois plus rapide que Ruby on Rails, ** la génération d'un objet Environment peut prendre plus de temps que le traitement d'une requête **.
Vérifions-le avec un script de référence.
# -*- coding: utf-8 -*-
require 'rack'
require 'keight'
require 'benchmark/ips'
##Classe d'action(Contrôleur dans MVC)Créer
class API < K8::Action
mapping '/hello', :GET=>:say_hello
def say_hello()
return "<h1>Hello, World!</h1>"
end
end
##Créer une application Rack et attribuer une classe d'action
mapping = [
['/api', API],
]
rack_app = K8::RackApplication.new(mapping)
##Exemple d'exécution
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/Objet d'environnement qui représente bonjour
env = Rack::MockRequest.env_for("/api/hello")
##référence
Benchmark.ips do |x|
x.config(:time => 5, :warmup => 1)
##Créer un nouvel objet Environnement(faire une copie)
x.report("just copy env") do |n|
i = 0
while (i += 1) <= n
env.dup()
end
end
##Créer un objet Environnement pour gérer la demande
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
##Réutiliser les objets d'environnement pour gérer les demandes
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
Lorsque j'ai exécuté ceci, j'ai obtenu les résultats suivants, par exemple (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
À partir des trois dernières lignes, nous pouvons voir que:
Dans cette situation, accélérer davantage le cadre ne rendra pas l'application beaucoup plus rapide. Pour surmonter cette impasse, il semble bon d'améliorer les spécifications du Rack elles-mêmes.
(TODO)
Eh bien, entrez enfin dans le sujet principal.
Afin de résoudre les problèmes décrits jusqu'à présent, j'ai pensé à une alternative aux actuels Rack et WSGI. Soi-disant, "Mes pensées sur Saikyo no Raku".
La nouvelle spécification ne change pas l'abstraction des requêtes et réponses HTTP. Je vais donc me concentrer sur la façon d'abstraire ces deux.
De plus, le Rack et le WSGI actuels héritent partiellement des spécifications CGI. Cependant, CGI est une spécification à l'ancienne qui suppose que les données sont transmises via des variables d'environnement. Il ne convient pas à cette époque, vous pouvez donc oublier les spécifications CGI.
Les requêtes HTTP sont divisées en éléments suivants:
La méthode de demande peut être une chaîne supérieure ou un symbole. Le symbole semble être meilleur en termes de performances.
meth = :GET
Le chemin de la demande peut être une chaîne. Rack doit prendre en compte SCRIPT_NAME ainsi que PATH_INFO, mais maintenant que personne n'utilisera SCRIPT_NAME, nous allons simplement considérer l'équivalent de PATH_INFO.
path = "/index.html"
L'en-tête de la demande peut être un objet de hachage. De plus, je ne veux pas convertir de User-Agent en HTTP_USER_AGENT, mais HTTP / 2 semble avoir des noms d'en-tête inférieurs, donc je vais probablement le faire correspondre.
headers = {
"host" => "www.example.com",
"user-agent" => "Mozilla/5.0 ....(snip)....",
....(snip)....,
}
Le paramètre de requête est «nil» ou une chaîne. S'il n'y a pas de «?», Cela devient «nil», et s'il existe, cela devient une chaîne de caractères (il peut s'agir d'un caractère vide).
query = "x=1"
Les E / S liées (rack.input et rack.errors et rack.hijack ou puma.socket) doivent être dans un seul tableau. Ce ne sont que les équivalents de stdin, stderr et stdout ... n'est-ce pas? Peut-être que socket sert aussi de rack.input, mais je ne suis pas familier avec cela, donc je vais le séparer ici.
ios = [
StringIO.new(), # rack.input
$stderr, # rack.errors
puma_socket,
]
La valeur des autres informations de demande change pour chaque demande. Cela devrait être un objet de hachage.
options = {
http: "1.1", # HTTP_VERSION
client: "::1", # REMOTE_ADDR
protocol: "http", # rack.url_scheme
}
Les dernières informations du serveur ne doivent pas changer à moins que le serveur d'applications n'ait changé. Ainsi, une fois que vous l'avez créé en tant qu'objet de hachage, vous pouvez le réutiliser.
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
Considérez une application Rack qui les reçoit.
class RackApp
def call(meth, path, headers, query, ios, options, server)
input, errors, socket = ios
...
end
end
Wow, il a 7 arguments. C'est un peu cool, non? Les trois premiers (meth, chemin et en-têtes) sont au cœur de la requête, donc en les laissant seuls en tant qu'arguments, la requête et ios sont susceptibles d'être regroupés en options.
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
}
Cela réduira le nombre d'arguments de sept à cinq.
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
Eh bien, je pense que c'est correct d'utiliser ça.
La réponse HTTP peut toujours être représentée par l'état, l'en-tête et le corps.
def call(meth, path, headers, options, server)
status = 200
headers = {"content-type"=>"application/json"},
body = '{"message":"Hello!"}'
return status, headers, body
end
Cependant, je pense que l'en-tête Content-Type peut être traité spécialement. Parce que dans les applications Rack actuelles, il n'y a que des en-têtes Content-Type, tels que {" Content-Type "=>" text / html} "
et {" Content-Type "=>" application / json "}
. En effet, dans de nombreux cas, il n'est pas inclus. Par conséquent, si seul Content-Type est traité spécialement et rendu indépendant, ce sera un peu plus simple.
def call(meth, path, headers, options, server)
##Que ça
return 200, {"Content-Type"=>"text/plain"}, ["Hello"]
##C'est plus concis
return 200, "text/plain", {}, ["Hello"]
end
Il y a aussi d'autres problèmes.
Cependant, comme la plupart des réponses renvoient une chaîne en tant que corps, il est inutile de l'envelopper dans un tableau un par un. Si possible, le corps doit être une "chaîne, ou un objet qui renvoie une chaîne avec ʻeach ()` ". </ jj>
Mais ce qui est vraiment souhaitable, c'est d'avoir l'équivalent de teardown ()
. C'est dommage que je ne puisse penser à aucune spécification spécifique [^ 4]. </ jj>
[^ 4]: Je pensais que rack.after_reply
était cela, mais cela semble être une fonction unique de Puma.
(TODO)
(TODO)
(TODO)
Nous aimerions entendre les opinions d'experts.
Juste sur la liste de diffusion de Rack J'ai une question sur le support HTTP2. Il y a eu une petite discussion sur Rack2 à ce sujet, alors j'ai traversé diverses choses.
Recommended Posts