朱莉娅 网络服务


我们继续考虑使用技术Julia。 今天,我们将讨论为构建Web服务而设计的软件包。 朱莉娅语言的主要市场是高性能计算,这已经不是什么秘密了。 因此,一个合理的步骤是直接创建能够按需执行这些计算的Web服务。 当然,Web服务并不是在网络环境中进行通信的唯一方法。 但是,由于它们现在是分布式系统中使用最广泛的,因此我们将考虑创建服务于HTTP请求的服务。


请注意,由于朱莉娅(Julia)的年轻,因此有一组竞争性软件包。 因此,我们将尝试弄清楚如何以及为什么使用它们。 在此过程中,我们将同一个JSON Web服务的实现与他们的帮助进行了比较。


Julia的基础架构在最近一两年中一直在积极发展。 而且,在这种情况下,这不仅是刻在文本上漂亮的开始的在线短语,而且还强调了一切都在剧烈变化的事实,而几年前相关的东西现在已经过时了。 但是,我们将尝试突出显示稳定的程序包,并在其帮助下提供有关如何实施Web服务的建议。 为了确定起见,我们将创建一个Web服务,该服务接受具有以下格式的JSON数据的POST请求:


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

我们假设我们创建的服务不是RESTful的。 我们的主要任务是精确考虑描述路由和请求处理程序的方法。


HTTP.jl包


该软件包是Julia中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协议命令名称匹配的方法。 在这种情况下,我们使用getpost 。 可选的命名参数verbose允许您设置要输出的调试信息的数量。 因此,例如, verbose=1将产生:


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

verbose=3的情况下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方法(但是,与所有其他标头一样)。


对于健康测试,我们将执行三个查询:


  • GET请求到根路由;
  • GET请求,格式为/用户/名称,其中名称为传输的名称;
  • 带有JSON对象的POST请求/资源/进程。 我们希望收到相同的文档,但是添加了server_mark字段。

我们将使用此客户端代码来测试所有服务器实现选项。


HTTP服务器


确定客户端之后,就该开始实施服务器了。 首先,我们将仅在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.jlHTTP.jl提供自动的方法来标识路由中的模式。


process_resource处理程序中,我们解析服务接受的JSON。


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

通过req.body字段访问数据。 请注意,数据采用字节数组格式。 因此,要将它们作为字符串使用,必须执行对字符串的显式转换。 JSON.parse方法是JSON.jl包方法,用于反序列化数据并构建对象。 由于在这种情况下对象是关联数组(Dict),因此我们可以轻松地向其添加新键。 弦乐


 message["server_mark"] = "confirmed" 

添加server_mark密钥并confirmed值。


该服务在执行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 WorldHello Jemand"server_mark":"confirmed"


查看服务代码后,自然而然地出现了一个问题-如果HTTP中的所有内容都很简单,为什么还要使用所有其他软件包。 有一个非常简单的答案。 HTTP-允许注册动态处理程序,但是即使是从目录读取静态图片文件的基本实现也需要单独的实现。 因此,我们还考虑了专注于创建Web应用程序的软件包。


Mux.jl软件包


该软件包被定位为在Julia上实现的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方法来描述路由。 使用@app宏声明Web应用程序。 page方法的参数是路由和处理程序。 可以将处理程序指定为接受请求作为输入的函数,也可以将其指定为lambda函数。 在其他有用的功能中,提供了Mux.notfound()来发送指定的“ Not found响应。 而且,应该发送到客户端的结果不需要像前面示例中那样打包在HTTP.Response中,因为Mux会自己完成。 但是,您仍然需要自己进行JSON解析,为响应序列化对象JSON.json(message)


  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中组织等待完成的选项之一是调用代码:


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

否则,该服务与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上实现,并且是Ruby社区在Elixir上的Web构建思想的实现。 该项目正在非常积极地进行开发,并且被定位为用于创建RESTful API和轻量级Web应用程序的工具。 有一些功能可以简化JSON序列化和反序列化。 HTTP.jlMux.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.parserender(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声明一个路段。 换句话说,表达式的变量部分可以通过模板中指定的名称进行访问。 在语法上被指定为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上的“ Rails”非常清晰可见,包括由生成器创建的目录结构。 该项目正在开发中,但是由于未知原因,此软件包未包含在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!", "/") 

也就是说,处理程序函数在第一位,路由在第二位。 但就我们而言,让我们离开红宝石风格。


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,则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的功能并不止于此,但我们不需要它们来实现网络服务。


控制服务的响应:


 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 

通常,我们可以说在Julia中已经有足够的资金来创建Web服务。 也就是说,无需“重新发明轮子”来编写它们。 下一步是评估Julia在有人愿意承担的情况下如何承受现有基准中的负担。 但是,现在让我们来讨论一下该评论。


参考文献


Source: https://habr.com/ru/post/zh-CN449338/


All Articles