Lua Web快速开发指南(7) - 高效的接口调用 - httpc库

httpc库基于cf框架都内部实现的socket编写的http client库.

httpc库内置SSL支持, 在不使用代理的情况下就可以请求第三方接口.

httpc支持header、args、body、timeout请求设置, 完美支持各种httpc调用方式.

API介绍

httpc库使用前需要手动导入httpc库: local httpc = require "httpc".

httpc.get(domain, HEADER, ARGS, TIMEOUT)

调用get方法将会对domain发起一次HTTP GET请求.

domain是一个符合URL定义规范的字符串;

HEADER是一个key-value数组, 一般用于添加自定义头部;

ARGS为请求参数的key-value数组, 对于GET方法将会自动格式化为:args[n][1]=args[n][2]&args[n+1][1]=args[n+1][2];

TIMEOUT为httpc请求的最大超时时间;

httpc.post(domain, HEADER, BODY, TIMEOUT)

调用post方法将会对domain发起一次HTTP POST请求, 此方法的content-type会被设置为:application/x-www-form-urlencoded.

domain是一个符合URL定义规范的字符串;

HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置;

BODY是一个key-value数组, 对于POST方法将会自动格式化为:body[n][1]=body[n][2]&body[n+1][1]=body[n+1][2];

TIMEOUT为httpc请求的最大超时时间;

httpc.json(domain, HEADER, JSON, TIMEOUT)

json方法将会对domain发起一次http POST请求. 此方法的content-type会被设置为:application/json.

HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置;

JSON必须是一个字符串类型;

TIMEOUT为httpc请求的最大超时时间;

httpc.file(domain, HEADER, FILES, TIMEOUT)

file方法将会对domain发起一次http POST请求.

HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置;

FILES是一个key-value数组, 每个item包含: name(名称), filename(文件名), file(文件内容), type(文件类型)等属性. 文件类型可选.

TIMEOUT为httpc请求的最大超时时间;

httpc 返回值

所有httpc请求接口均会有2个返回值: code, response. code为http协议状态码, response为回应body(字符串类型).

参数不正确, 连接被断开等其它错误, code将会为nil, response为错误信息.

"一次性HTTP请求"

什么是一次性httpc请求呢?

每次我们使用httpc库在请求第三方http接口的时候, 都会在接口返回后关闭连接. 这在日常使用中一边也没什么问题.

但是当我们需要多次请求同一个接口的时候, 每次请求完毕就关闭连接显然不是那么高效, 现在我们尝试使用一个http class对象来解决这个问题.

注意: httpc class对象不能对不同域名的接口使用同一个连接, 这会返回一个错误调用给使用者.

httpc库的class对象使用介绍

要使用httpc的class需要导入httpc的class库, 导入方式为: local httpc = require "httpc.class".

当需要使用httpc发起请求之前, 需要先创建一个httpc的对象, 如: local hc = httpc:new {}. httpc对象创建与初始化完毕后, 使用方式同上述API所示.

hchttpc拥有相同的API, 但是需要使用不同的调用方式. 如: hc:gethc:posthc:jsonhc:file.

一旦hc使用完毕时, 需要显示的调用hc:close()方法来关闭创建的httpc对象并销毁httpc的连接.

开始实践

现在, 让我们将上面学到的API使用方式运用到实践中.

1. 启动一个httpd库的web server

main.lua中启动一个httpd的server.

  1. local httpd = require "httpd"
  2. local json = require "json"
  3. local app = httpd:new("httpd")
  4. app:listen("", 8080)
  5. app:run()

1. 增加一个API路由用于ip地址归属地查询

我们先利用httpd库启动一个server服务, 并且对外提供IP归属地查询接口

  1. app:api('/ip', function(content)
  2. local httpc = require "httpc"
  3. local args = content.args
  4. if not args or not args['ip'] then
  5. return json.encode({
  6. code = 400,
  7. msg = "错误的接口调用方式",
  8. data = json.null,
  9. })
  10. end
  11. local code, response = httpc.get("http://freeapi.ipip.net/"..args["ip"])
  12. if code ~= 200 then
  13. return json.encode({
  14. code = 401,
  15. msg = "获取数据失败",
  16. data = json.null,
  17. })
  18. end
  19. return response
  20. end)

现在代码已经完成! 让我们打开浏览器输入:http://localhost:8080/ip?ip=8.8.8.8查看返回数据.

2. 查询多个IP地址的归属地

一个请求对应一次回是HTTP协议的本质! 但是我们经常会遇到批量请求的业务场景, 我们就以此来设计一个批量请求/返回的例子.

让我们假设客户端将会发送一次POST请求, body为json类型并且里面包含一个IP数组: ip_list = {1.1.1.1, 8.8.8.8, 114.114.114.114}.

服务端在接受到这个数组之后, 需要将这ip的归属地信息一次性返回给客户端.

  1. app:api('/ips', function(content)
  2. local httpc = require "httpc.class"
  3. if not content.json then
  4. return json.encode({
  5. code = 400,
  6. msg = "错误的调用参数",
  7. data = json.null,
  8. })
  9. end
  10. local args = json.decode(content.body)
  11. if type(args) ~= 'table' or type(args['ip_list']) ~= 'table' then
  12. return json.encode({
  13. code = 400,
  14. msg = "错误的参数类型",
  15. data = json.null,
  16. })
  17. end
  18. local hc = httpc:new {}
  19. local ret = { code = 200 , data = {}}
  20. for _, ip in ipairs(args['ip_list']) do
  21. local code, response = hc:get("http://freeapi.ipip.net/"..ip)
  22. ret['data'][#ret['data']+1] = json.decode(response)
  23. end
  24. return json.encode(ret)
  25. end)

由于普通浏览器POST无法发送json, 让我们使用curl命令行工具进行测试:

  1. curl -H "Content-Type: application/json" -X POST -d '{"ip_list":["1.1.1.1","8.8.8.8","114.114.114.114"]}' http://localhost:8080/ip

返回数据如下:

  1. {"code":200,"data":[["CLOUDFLARE.COM","CLOUDFLARE.COM","","",""],["GOOGLE.COM","GOOGLE.COM","","","level3.com"],["114DNS.COM","114DNS.COM","","",""]]}

3. 持续优化.

上述例子似乎已经非常完美! 我们利用连接保持的方式进行了3次请求, 这样已经缩短了请求50%的连接消耗(TCP握手).

但是对于非常需要性能的我们来说: 每次请求需要等到上一个请求处理完毕后才能继续发起新的请求, 这样的方式显然还不足以满足我们.

这样的情况下, httpc库提供了一个叫multi_request的方法. 具体使用方法在这里.

这个方法可以让我们同时发送几十上百个请求来解决单个连接阻塞的问题.

4. 并发请求

现在, 让我使用httpc库的multi_request方法来并发请求多个接口, 减少连接阻塞带来的问题.

  1. app:api('/ips_multi', function (content)
  2. local httpc = require "httpc"
  3. if not content.json then
  4. return json.encode({
  5. code = 400,
  6. msg = "错误的调用参数",
  7. data = json.null,
  8. })
  9. end
  10. local args = json.decode(content.body)
  11. if type(args) ~= 'table' or type(args['ip_list']) ~= 'table' then
  12. return json.encode({
  13. code = 400,
  14. msg = "错误的参数类型",
  15. data = json.null,
  16. })
  17. end
  18. local requests = {}
  19. local responses = { code = 200, data = {}}
  20. for _, ip in ipairs(args["ip_list"]) do
  21. requests[#requests+1] = {
  22. domain = "http://freeapi.ipip.net/"..ip,
  23. method = "get",
  24. }
  25. end
  26. local ok, ret = httpc.multi_request(requests)
  27. for _, res in ipairs(ret) do
  28. responses['data'][#responses['data'] + 1] = res
  29. end
  30. return json.encode(responses)
  31. end)

好的, 现在让我们再次使用curl工具进行测试:

  1. curl -H "Content-Type: application/json" -X POST -d '{"ip_list":["1.1.1.1","8.8.8.8","114.114.114.114"]}' http://localhost:8080/ips_multi

我们可以从cf的请求回应时间看到, 响应时间消耗再次降低了50%.

  1. [candy@MacBookPro:~/Documents/core_framework] $ ./cfadmin
  2. [2019/06/16 17:45:21] [INFO] httpd正在监听: 0.0.0.0:8080
  3. [2019/06/16 17:45:21] [INFO] httpd正在运行Web Server服务...
  4. [2019/06/16 17:45:23] - ::1 - ::1 - /ips_multi - POST - 200 - req_time: 0.140253/Sec
  5. [2019/06/16 17:45:38] - ::1 - ::1 - /ips - POST - 200 - req_time: 0.288286/Sec

完整的代码

  1. local httpd = require "httpd"
  2. local json = require "json"
  3. local app = httpd:new("httpd")
  4. app:api('/ip', function(content)
  5. local httpc = require "httpc"
  6. local args = content.args
  7. if not args or not args['ip'] then
  8. return json.encode({
  9. code = 400,
  10. msg = "错误的接口调用方式",
  11. data = json.null,
  12. })
  13. end
  14. local code, response = httpc.get("http://freeapi.ipip.net/"..args["ip"])
  15. if code ~= 200 then
  16. return json.encode({
  17. code = 401,
  18. msg = "获取数据失败",
  19. data = json.null,
  20. })
  21. end
  22. return response
  23. end)
  24. app:api('/ips', function(content)
  25. local httpc = require "httpc.class"
  26. if not content.json then
  27. return json.encode({
  28. code = 400,
  29. msg = "错误的调用参数",
  30. data = json.null,
  31. })
  32. end
  33. local args = json.decode(content.body)
  34. if type(args) ~= 'table' or type(args['ip_list']) ~= 'table' then
  35. return json.encode({
  36. code = 400,
  37. msg = "错误的参数类型",
  38. data = json.null,
  39. })
  40. end
  41. local hc = httpc:new {}
  42. local ret = { code = 200 , data = {}}
  43. for _, ip in ipairs(args['ip_list']) do
  44. local code, response = hc:get("http://freeapi.ipip.net/"..ip)
  45. ret['data'][#ret['data']+1] = json.decode(response)
  46. end
  47. return json.encode(ret)
  48. end)
  49. app:api('/ips_multi', function (content)
  50. local httpc = require "httpc"
  51. if not content.json then
  52. return json.encode({
  53. code = 400,
  54. msg = "错误的调用参数",
  55. data = json.null,
  56. })
  57. end
  58. local args = json.decode(content.body)
  59. if type(args) ~= 'table' or type(args['ip_list']) ~= 'table' then
  60. return json.encode({
  61. code = 400,
  62. msg = "错误的参数类型",
  63. data = json.null,
  64. })
  65. end
  66. local requests = {}
  67. local responses = { code = 200, data = {}}
  68. for _, ip in ipairs(args["ip_list"]) do
  69. requests[#requests+1] = {
  70. domain = "http://freeapi.ipip.net/"..ip,
  71. method = "get",
  72. }
  73. end
  74. local ok, ret = httpc.multi_request(requests)
  75. for _, res in ipairs(ret) do
  76. responses['data'][#responses['data'] + 1] = res
  77. end
  78. return json.encode(responses)
  79. end)
  80. app:listen("", 8080)
  81. app:run()

继续学习

下一章节我们将学习如何使用httpd库编写Websocket.