ジュリア。 Webサービス







Juliaテクノロジーについては引き続き検討します。 今日は、Webサービスを構築するために設計されたパッケージについて説明します。 ジュリア言語の主なニッチが高性能コンピューティングであることは秘密ではありません。 したがって、かなり論理的なステップは、これらの計算をオンデマンドで実行できるWebサービスを直接作成することです。 もちろん、ネットワーク環境で通信する方法はWebサービスだけではありません。 ただし、現在は分散システムで最も広く使用されているため、HTTP要求に対応するサービスの作成を検討します。







Juliaの若さのために、競合するパッケージのセットがあることに注意してください。 したがって、それらをどのように、そしてなぜ使用するのかを理解しようとします。 途中で、同じJSON Webサービスの実装とそれらのヘルプを比較します。







ジュリアのインフラストラクチャは、ここ1〜2年で積極的に開発されています。 そして、この場合、これはテキストの美しい始まりに刻まれたオンラインのフレーズではなく、すべてが集中的に変化しているという事実に重点を置いており、数年前に関連していたものは現在古くなっています。 ただし、安定したパッケージを強調表示し、Webサービスを実装する方法についての推奨事項を提供しようとします。 明確にするために、次の形式のJSONデータでPOSTリクエストを受け入れるWebサービスを作成します。







{ "title": "something", "body": "something" }
      
      





作成するサービスはRESTfulではないと想定しています。 私たちの主なタスクは、ルートとリクエストハンドラを記述するためのメソッドを正確に検討することです。







HTTP.jlパッケージ



このパッケージは、ジュリアのHTTPプロトコルの主要な実装であり、次第に新しい機能が増えてきています。 このパッケージは、クライアントHTTP要求を実行するための典型的な構造と機能を実装することに加えて、HTTPサーバーを作成するための機能も実装します。 同時に、開発中のパッケージには、プログラマーがハンドラーを登録して典型的なサービスを構築するのを非常に快適にする関数が用意されています。 また、最新バージョンでは、WebSocketプロトコルのサポートが組み込まれています。その実装は、以前は別のパッケージWebSocket.jlの一部として作成されていました。 つまり、HTTP.jlは現在、プログラマーのほとんどのニーズを満たすことができます。 いくつかの例を詳しく見てみましょう。







HTTPクライアント



クライアントコードを使用して実装を開始し、これを使用して操作性を確認します。







 #!/usr/bin/env julia --project=@. import HTTP import JSON.json const PORT = "8080" const HOST = "127.0.0.1" const NAME = "Jemand" #    struct Document title::String body::String end #         Base.show(r::HTTP.Messages.Response) = println(r.status == 200 ? String(r.body) : "Error: " * r.status) #    r = HTTP.get("http://$(HOST):$(PORT)") show(r) #   /user/:name r = HTTP.get("http://$(HOST):$(PORT)/user/$(NAME)"; verbose=1) show(r) #  JSON- POST- doc = Document("Some document", "Test document with some content.") r = HTTP.post( "http://$(HOST):$(PORT)/resource/process", [("Content-Type" => "application/json")], json(doc); verbose=3) show(r)
      
      





HTTPパッケージは、HTTPプロトコルコマンドの名前に一致するメソッドを提供します。 この場合、 get



およびpost



を使用しget



。 オプションの名前付き引数verbose



使用すると、出力するデバッグ情報の量を設定できます。 したがって、たとえば、 verbose=1



は次のようになります







 GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1)
      
      





また、 verbose=3



場合、送信データと受信データの完全なセットがすでに取得されています。







 DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "POST /resource/process HTTP/1.1\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "Content-Type: application/json\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "Host: 127.0.0.1\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "Content-Length: 67\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 e1c6 ️-> "{\"title\":\"Some document\",\"body\":\"Test document with some content.\"}" (unsafe_write) DEBUG: 2019-04-21T22:40:40.963 eb4f ️<- "HTTP/1.1 200 OK\r\n" (readuntil) DEBUG: "Content-Type: application/json\r\n" DEBUG: "Transfer-Encoding: chunked\r\n" DEBUG: "\r\n" DEBUG: 2019-04-21T22:40:40.963 eb4f ️<- "5d\r\n" (readuntil) DEBUG: 2019-04-21T22:40:40.963 eb4f ️<- "{\"body\":\"Test document with some content.\",\"server_mark\":\"confirmed\",\"title\":\"Some document\"}" (unsafe_read) DEBUG: 2019-04-21T22:40:40.968 eb4f ️<- "\r\n" (readuntil) DEBUG: "0\r\n" DEBUG: 2019-04-21T22:40:40.968 eb4f ️<- "\r\n" (readuntil)
      
      





将来、何が起こっているかについて最小限の情報のみを見るために、 verbose=1



のみを使用します。







コードに関するいくつかのコメント。







 doc = Document("Some document", "Test document with some content.")
      
      





以前にDocument構造(および不変)を宣言したため、デフォルトでコンストラクターを使用できます。その引数は、構造の宣言されたフィールドに対応しています。 JSONに変換するには、 JSON.jl



パッケージとそのjson(doc)



メソッドを使用します。

フラグメントに注意してください:







 r = HTTP.post( "http://$(HOST):$(PORT)/resource/process", [("Content-Type" => "application/json")], json(doc); verbose=3)
      
      





JSONを渡すため、 Content-Type



ヘッダーでapplication/json



タイプを明示的に指定する必要があります。 ヘッダーは、ヘッダー名と値のペアを含む配列(ベクトルではなく、Dictではない)を使用して、 HTTP.post



メソッドに渡されます(ただし、他のすべてと同様)。







ヘルステストでは、3つのクエリを実行します。









このクライアントコードを使用して、すべてのサーバー実装オプションをテストします。







HTTPサーバー



クライアントを理解したら、サーバーの実装を開始します。 そもそも、他のパッケージのインストールを必要としない基本バージョンとして保持するために、 HTTP.jl



を使用してのみサービスを作成します。 HTTP.jl



、他のすべてのパッケージはHTTP.jl



使用しHTTP.jl









 #!/usr/bin/env julia --project=@. import Sockets import HTTP import JSON #    #    index(req::HTTP.Request) = HTTP.Response(200, "Hello World") #     function welcome_user(req::HTTP.Request) # dump(req) user = "" if (m = match( r".*/user/([[:alpha:]]+)", req.target)) != nothing user = m[1] end return HTTP.Response(200, "Hello " * user) end #  JSON function process_resource(req::HTTP.Request) # dump(req) message = JSON.parse(String(req.body)) @info message message["server_mark"] = "confirmed" return HTTP.Response(200, JSON.json(message)) end #      const ROUTER = HTTP.Router() HTTP.@register(ROUTER, "GET", "/", index) HTTP.@register(ROUTER, "GET", "/user/*", welcome_user) HTTP.@register(ROUTER, "POST", "/resource/process", process_resource) HTTP.serve(ROUTER, Sockets.localhost, 8080)
      
      





この例では、次のコードに注意する必要があります。







 dump(req)
      
      





オブジェクトが認識しているすべてをコンソールに出力します。 データ型、値、すべてのネストされたフィールドとその値を含みます。 この方法は、ライブラリの調査とデバッグの両方に役立ちます。







ひも







 (m = match( r".*/user/([[:alpha:]]+)", req.target))
      
      





ハンドラーが登録されているルートを解析する正規表現です。 HTTP.jl



パッケージHTTP.jl



、ルート内のパターンを自動的に識別する方法を提供HTTP.jl



ん。







process_resource



ハンドラー内で、サービスによって受け入れられるJSONを解析します。







 message = JSON.parse(String(req.body))
      
      





データには、 req.body



フィールドを介してアクセスします。 データはバイト配列形式であることに注意してください。 したがって、それらを文字列として使用するには、文字列への明示的な変換が実行されます。 JSON.parse



メソッドは、データを逆シリアル化し、オブジェクトを構築するJSON.jl



パッケージメソッドです。 この場合のオブジェクトは連想配列(Dict)であるため、新しいキーを簡単に追加できます。 ひも







 message["server_mark"] = "confirmed"
      
      





値がconfirmed



server_mark



キーを追加します。







HTTP.serve(ROUTER, Sockets.localhost, 8080)



行がHTTP.serve(ROUTER, Sockets.localhost, 8080)



、サービスHTTP.serve(ROUTER, Sockets.localhost, 8080)









HTTP.jlに基づくサービスの制御応答( verbose=1



クライアントコードを実行するときに取得):







 GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) Hello World GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) Hello Jemand POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
      
      





verbose=1



のデバッグ情報の背景に対して、 Hello World



の行を明確に見ることができます: Hello World



Hello Jemand



"server_mark":"confirmed"









サービスコードを表示した後、自然な疑問が生じます。HTTPですべてが非常に単純な場合、なぜ他のすべてのパッケージが必要なのでしょうか。 これには非常に簡単な答えがあります。 HTTP-動的ハンドラーを登録できますが、ディレクトリから静的画像ファイルを読み込む基本的な実装でさえ、別個の実装が必要です。 したがって、Webアプリケーションの作成に焦点を当てたパッケージも検討します。







Mux.jlパッケージ



このパッケージは、ジュリアに実装されたWebアプリケーションの中間層として位置付けられています。 その実装は非常に軽量です。 主な目的は、ハンドラーを簡単に記述する方法を提供することです。 これは、プロジェクトが開発中ではないということではなく、ゆっくりと開発しているということです。 ただし、同じルートを提供するサービスのコードを見てください。







 #!/usr/bin/env julia --project=@. using Mux using JSON @app test = ( Mux.defaults, page(respond("<h1>Hello World!</h1>")), page("/user/:user", req -> "<h1>Hello, $(req[:params][:user])!</h1>"), route("/resource/process", req -> begin message = JSON.parse(String(req[:data])) @info message message["server_mark"] = "confirmed" return Dict( :body => JSON.json(message), :headers => [("Content-Type" => "application/json")] ) end), Mux.notfound() ) serve(test, 8080) Base.JLOptions().isinteractive == 0 && wait()
      
      





ここでは、 page



メソッドを使用してルートを説明しpage



。 Webアプリケーションは、 @app



マクロを使用して宣言されます。 page



メソッドの引数は、ルートとハンドラーです。 ハンドラーは、要求を入力として受け入れる関数として指定することも、ラムダ関数として指定することもできます。 追加の便利な関数のうち、 Mux.notfound()



、指定されたNot found



応答を送信するために存在します。 そして、クライアントに送信されるべき結果は、Muxが自分で行うため、前の例で行ったようにHTTP.Response



にパッケージ化する必要はありません。 ただし、応答用のオブジェクトJSON.json(message)



シリアル化と同様に、JSONの解析を自分で行う必要があります。







  message = JSON.parse(String(req[:data])) message["server_mark"] = "confirmed" return Dict( :body => JSON.json(message), :headers => [("Content-Type" => "application/json")] )
      
      





応答は、フィールド:body



:headers



持つ連想配列として送信されます。







serve(test, 8080)



メソッドを使用してサーバーを起動するのは非同期であるため、Juliaの完了待ちを整理するオプションの1つはコードを呼び出すことです。







 Base.JLOptions().isinteractive == 0 && wait()
      
      





それ以外の場合、サービスはHTTP.jl



以前のバージョンと同じようにHTTP.jl









サービスの制御応答:







 GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) <h1>Hello World!</h1> GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) <h1>Hello, Jemand!</h1> POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
      
      





パッケージBukdu.jl



このパッケージは、Phoenixフレームワークの影響下で開発されました。このフレームワークは、Elixirに実装され、ElixirでのプロジェクションでのRubyコミュニティからのWeb構築アイデアの実装です。 このプロジェクトは非常に活発に開発されており、RESTful APIと軽量Webアプリケーションを作成するためのツールとして位置付けられています。 JSONのシリアル化と逆シリアル化を簡素化する関数があります。 これはHTTP.jl



およびMux.jl



ありMux.jl



Webサービスの実装を見てみましょう。







 #!/usr/bin/env julia --project=@. using Bukdu using JSON #   struct WelcomeController <: ApplicationController conn::Conn end #   index(c::WelcomeController) = render(JSON, "Hello World") welcome_user(c::WelcomeController) = render(JSON, "Hello " * c.params.user) function process_resource(c::WelcomeController) message = JSON.parse(String(c.conn.request.body)) @info message message["server_mark"] = "confirmed" render(JSON, message) end #   routes() do get("/", WelcomeController, index) get("/user/:user", WelcomeController, welcome_user, :user => String) post("/resource/process", WelcomeController, process_resource) end #   Bukdu.start(8080) Base.JLOptions().isinteractive == 0 && wait()
      
      





最初に注意すべきことは、コントローラーの状態を保存するための構造体の宣言です。







 struct WelcomeController <: ApplicationController conn::Conn end
      
      





この場合、抽象型ApplicationController



子孫として作成された具象型です。







コントローラーのメソッドは、以前の実装と同様の方法で宣言されます。 JSONオブジェクトのハンドラーにはわずかな違いがあります。







 function process_resource(c::WelcomeController) message = JSON.parse(String(c.conn.request.body)) @info message message["server_mark"] = "confirmed" render(JSON, message) end
      
      





ご覧のとおり、デシリアライズもJSON.parse



メソッドを使用してJSON.parse



実行JSON.parse



JSON.parse



JSON.parse



render(JSON, message)



メソッドを使用して応答をシリアライズします。







ルートの宣言は、 do...end



ブロックの使用を含め、ルービストの伝統的なスタイルで行われます。







 routes() do get("/", WelcomeController, index) get("/user/:user", WelcomeController, welcome_user, :user => String) post("/resource/process", WelcomeController, process_resource) end
      
      





また、ルービストの従来の方法では、セグメントはルートライン/user/:user



宣言され/user/:user



。 つまり、式の変数部分であり、テンプレートで指定された名前でアクセスできます。 タイプSymbol



代表として構文的に指定されています。 ちなみに、Juliaの場合、 Symbol



型は、本質的にはRubyの場合と同じことを意味します。これは、メモリ内で単一のインスタンスで表される不変の文字列です。







したがって、変数部分でルートを宣言し、この変数部分のタイプも示した後、割り当てられた名前で既に解析されたデータを参照できます。 要求を処理するメソッドでは、 c.params.user



の形式のドットを介してフィールドにアクセスするだけです。







 welcome_user(c::WelcomeController) = render(JSON, "Hello " * c.params.user)
      
      





サービスの制御応答:







 GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) "Hello World" GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) "Hello Jemand" POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
      
      





コンソールへのサービスの結論:







 >./bukdu_json.jl INFO: Bukdu Listening on 127.0.0.1:8080 INFO: GET WelcomeController index 200 / INFO: GET WelcomeController welcome_user 200 /user/Jemand INFO: Dict{String,Any}("body"=>"Test document with some content.","title"=>"Some document") INFO: POST WelcomeController process_resource200 /resource/process
      
      





パッケージGenie.jl



MVC Webフレームワークとして位置づけられた野心的なプロジェクト。 彼のアプローチでは、ジェネレータによって作成されたディレクトリ構造を含め、ジュリアの「レール」が非常にはっきりと見えます。 プロジェクトは開発中ですが、理由は不明ですが、このパッケージはJuliaパッケージリポジトリに含まれていません。 つまり、次のコマンドを使用してgitリポジトリからのみインストールできます。







 julia>] # switch to pkg> mode pkg> add https://github.com/essenciary/Genie.jl
      
      





Genieでのサービスのコードは次のとおりです(ジェネレーターは使用しません)。







 #!/usr/bin/env julia --project=@. #     import Genie import Genie.Router: route, @params, POST import Genie.Requests: jsonpayload, rawpayload import Genie.Renderer: json! #      route("/") do "Hello World!" end route("/user/:user") do "Hello " * @params(:user) end route("/resource/process", method = POST) do message = jsonpayload() # if message == nothing # dump(Genie.Requests.rawpayload()) # end message["server_mark"] = "confirmed" return message |> json! end #   Genie.AppServer.startup(8080) Base.JLOptions().isinteractive == 0 && wait()
      
      





ここでは、宣言の形式に注意を払う必要があります。







 route("/") do "Hello World!" end
      
      





このコードは、Rubyプログラマーにとって非常によく知られています。 do...end



ブロックはハンドラーとして、ルートはメソッドの引数として使用します。 Juliaの場合、このコードは次の形式で書き換えられることに注意してください。







 route(req -> "Hello World!", "/")
      
      





つまり、ハンドラー関数が最初にあり、ルートが2番目にあります。 しかし、私たちの場合は、ルビースタイルのままにします。







Genieは、実行結果をHTTP応答に自動的にパックします。 最小限の場合、正しい型の結果、たとえばStringを返すだけで済みます。 追加のアメニティのうち、入力形式の自動チェックとその分析が実装されています。 たとえば、JSONの場合、 jsonpayload()



メソッドを呼び出すだけです。







 route("/resource/process", method = POST) do message = jsonpayload() # if message == nothing # dump(Genie.Requests.rawpayload()) # end message["server_mark"] = "confirmed" return message |> json! end
      
      





ここでコメントアウトされているコードフラグメントに注意してください。 何らかの理由で入力形式がJSONとして認識されnothing



場合、 jsonpayload()



メソッドはnothing



返しnothing



。 これだけのために、ヘッダー[("Content-Type" => "application/json")]



HTTPクライアントに追加されることに注意してください。そうしないと、GenieはデータをJSONとして解析しさえしません。 理解できない何かが来た場合、 rawpayload()



を見てそれが何であるかを見ると便利です。 ただし、これは単なるデバッグ段階であるため、コード内に残してはなりません。







また、フォーマットmessage |> json!



結果を返すことに注意する必要がありmessage |> json!



。 ここでは、 json!(str)



メソッドがパイプラインの最後に配置されています。 JSON形式へのデータのシリアル化を提供し、Genieが正しいContent-Type



追加することも保証します。 また、上記の例のほとんどの場合、単語のreturn



は冗長であることに注意してください。 たとえば、JuliaはRubyと同様に、常に最後の操作の結果または最後に指定された式の値を返します。 つまり、単語return



はオプションです。







Genieの機能はこれで終わりではありませんが、Webサービスを実装するために必要なわけではありません。







サービスの制御応答:







 GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) Hello World! GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) Hello Jemand POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
      
      





コンソールへのサービスの結論:







 >./genie_json.jl [ Info: Ready! 2019-04-24 17:18:51:DEBUG:Main: Web Server starting at http://127.0.0.1:8080 2019-04-24 17:18:51:DEBUG:Main: Web Server running at http://127.0.0.1:8080 2019-04-24 17:19:21:INFO:Main: / 200 2019-04-24 17:19:21:INFO:Main: /user/Jemand 200 2019-04-24 17:19:22:INFO:Main: /resource/process 200
      
      





パッケージJuliaWebAPI.jl



このパッケージは、HTTP.jlがプロトコルを実装するライブラリに過ぎなかった当時、Webアプリケーションを作成するための中間層として位置付けられていました。 このパッケージの作成者は、Swagger仕様(OpenAPIおよびhttp://editor.swagger.io/ )に基づくサーバーコードジェネレーターも実装しました。プロジェクトhttps://github.com/JuliaComputing/Swagger.jlを参照してください 。このジェネレーターはJuliaWebAPIを使用しました.jl。 ただし、JuliaWebAPI.jlの問題は、POST要求を通じてサーバーに送信された要求の本文(JSONなど)を処理する機能を実装していないことです。 著者は、キーと値の形式でパラメータを渡すことはすべての場合に適していると信じていました...このパッケージの将来は明確ではありません。 その機能はすべて、HTTP.jlを含む他の多くのパッケージに既に実装されています。 Swagger.jlパッケージも使用しなくなりました。







WebSockets.jl



WebSocketプロトコルの初期実装。 このパッケージは、このプロトコルの主要な実装として長い間使用されてきましたが、現在のところ、その実装はHTTP.jlパッケージに含まれています。 WebSockets.jlパッケージ自体はHTTP.jlを使用して接続を確立しますが、現在では新しい開発で使用する価値はありません。 互換性のためのパッケージと見なす必要があります。







おわりに



このレビューでは、JuliaにWebサービスを実装するさまざまな方法を示します。 最も簡単で最も普遍的な方法は、HTTP.jlパッケージを直接使用することです。 また、Bukdu.jlおよびGenie.jlパッケージは非常に便利です。 少なくとも、それらの開発を監視する必要があります。 Mux.jlパッケージに関しては、HTTP.jlの背景からその利点が解消されています。 したがって、個人的な意見はそれを使用しないことです。 Genie.jlは非常に有望なフレームワークです。 ただし、使用を開始する前に、少なくとも作成者が公式パッケージとして登録しない理由を理解する必要があります。







例のJSONデシリアライゼーションコードはエラー処理なしで使用されていることに注意してください。 Genieを除くすべてのケースで、解析エラーを処理し、これについてユーザーに通知する必要があります。 HTTP.jlのこのようなコードの例:







  local message = nothing local body = IOBuffer(HTTP.payload(req)) try message = JSON.parse(body) catch err @error err.msg return HTTP.Response(400, string(err.msg)) end
      
      





一般に、ジュリアでWebサービスを作成するための資金はすでに十分であると言えます。 つまり、執筆のための「車輪の再発明」は必要ありません。 次のステップは、誰かがそれを受け入れる準備ができている場合、ジュリアが既存のベンチマークの負荷にどのように耐えることができるかを評価することです。 ただし、当面はこのレビューについて説明します。







参照資料






All Articles