协议扩展 - 用例

协议扩展可用于以下用例。

  • 基于消息的负载均衡 (MBLB)
  • 直播
  • 基于令牌的负载均衡
  • 负载平衡持久性
  • 基于 TCP 连接的负载均衡
  • 基于内容的负载平衡
  • SSL
  • 修改流量
  • 向客户端或服务器发起流量
  • 处理有关建立连接的数据

基于消息的负载均衡

协议扩展支持基于消息的负载平衡 (MBLB),它可以解析 NetScaler 设备上的任何协议,并对到达一个客户端连接的协议消息进行负载平衡,即通过多个服务器连接分发消息。MBLB 是通过解析客户端 TCP 数据流的用户代码实现的。

TCP 数据流被传递给 on_data 回调以用于客户端和服务器行为。TCP 数据流可通过类似 Lua 字符串的接口提供给扩展函数。您可以使用类似于 Lua 字符串 API 的 API 来解析 TCP 数据流。

有用的 API 包括:

data:len()

data:find()

data:byte()

data:sub()

data:split()

将 TCP 数据流解析为协议消息后,用户代码只需将协议消息发送到传递给客户端 on_data 回调的上下文中的下一个可用上下文,即可实现负载平衡。

ns.send () API 用于将消息发送到其他处理模块。除了目标上下文外,发送 API 还使用事件名称和可选负载作为参数。事件名称和行为的回调函数名称之间存在一对应关系。<event_name>事件的回调名为 on_。回调名称仅使用小写。

例如,TCP 客户端和服务器 on_data 回调是名为“DATA”的事件的用户定义处理程序。要在一次发送调用中发送整个协议消息,使用 EOM 事件。EOM 代表 end of message,表示向 LB 上下文下游的协议消息结束,因此需要为该消息之后的数据做出新的负载平衡决策。

在 on_data 事件中,扩展代码有时可能无法收到完整的协议消息。在这种情况下,可以使用 ctxt: hold () API 来保存数据。hold API 可用于 TCP-Client 和服务器回调上下文。当调用“保存数据”时,数据存储在上下文中。当在同一上下文中接收到更多数据时,新接收到的数据将附加到先前存储的数据中,并使用合并的数据再次调用 on_data 回调函数。

注意: 使用的负载平衡方法取决于与负载平衡上下文相对应的负载平衡虚拟服务器的配置。

以下代码片段显示了如何使用发送 API 发送解析后的协议消息。

示例:

    function client.on_data(ctxt, payload)
        --
        -- code to parse payload.data into protocol message comes here
        --
        -- sending the message to lb
        ns.send(ctxt.output, "EOM", {data = message})
    end -- client.on_data

    function server.on_data(ctxt, payload)
        --
        -- code to parse payload.data into protocol message comes here
        --
        -- sending the message to client
        ns.send(ctxt.output, "EOM", {data = message})

    end -- server.on_data
<!--NeedCopy-->

直播

在某些情况下,可能没有必要在收集到整个协议消息之前保留 TCP 数据流。实际上,除非需要,否则不建议这样做。保存数据会增加 NetScaler 设备上的内存使用量,并且由于许多连接上的协议消息不完整,会耗尽 NetScaler 设备上的内存,从而使设备容易受到 DDoS 攻击。

用户可以使用发送 API 在扩展回调处理程序中实现 TCP 数据的流式传输。数据可以分块发送,而不是在收集到整条消息之前保存数据。使用 DATA 事件向 ctxt.output 发送数据会发送部分协议消息。随之而来的是更多的 DATA 事件。必须发送 EOM 事件以标记协议消息的结束。下游负载平衡上下文对接收到的第一个数据做出负载平衡决策。在收到 EOM 消息后做出新的负载平衡决策。

要流式传输协议消息数据,请先发送多个 DATA 事件,然后再发送一个 EOM 事件。连续的 DATA 事件和以下 EOM 事件将发送到由负载平衡决策为序列中的第一个 DATA 事件选择的同一个服务器连接。

对于发送到客户端上下文,EOM 和 DATA 事件实际上是相同的,因为下游的客户端上下文对 EOM 事件没有特殊处理。

基于令牌的负载均衡

对于原生支持的协议,NetScaler 设备支持基于令牌的负载平衡方法,该方法使用 PI 表达式创建令牌。对于扩展,协议是事先未知的,因此不能使用 PI 表达式。对于基于令牌的负载平衡,必须将默认负载平衡虚拟服务器设置为使用 USER_TOKEN 负载平衡方法,并通过使用 user_token 字段调用 send API 来提供扩展代码中的令牌值。如果令牌值是从发送 API 发送的,并且在默认负载平衡虚拟服务器上配置了 USER_TOKEN 负载平衡方法,则通过根据令牌值计算哈希来做出负载平衡决策。令牌值的最大长度为 64 字节。

add lb vserver v\_mqttlb USER\_TCP –lbMethod USER\_TOKEN

以下示例中的代码片段使用发送 API 发送 LB 令牌值。

示例:

        -- send the message to lb




        -- user_token is set to do LB based on clientID




        ns.send(ctxt.output, "EOM", {data = message,

                                 user_token = token_info})
<!--NeedCopy-->

负载平衡持久性

负载平衡持久性与基于令牌的负载平衡密切相关。用户必须能够以编程方式计算持久性会话值并将其用于负载平衡持久性。发送 API 用于发送持久性参数。要使用负载平衡持久性,您必须在默认的负载平衡虚拟服务器上设置 USERSESSION 持久性类型,并通过调用带有 user_session 字段的 send API 来提供扩展代码中的持久性参数。持久性参数值的最大长度为 64 字节。

如果自定义协议需要多种类型的持久性,则必须定义用户持久性类型并进行配置。用于配置虚拟服务器的参数的名称由协议实现者决定。参数的配置值也可用于扩展代码。

以下 CLI 和代码段显示了使用发送 API 来支持负载平衡持久性。 mqtt.lua 的代码清单部分中的代码清单 还说明了 user_session 字段的使用情况。

对于持久性,您必须在负载平衡虚拟服务器上指定 USERSISE 持久性类型,并从 ns.send API 传递 user_session 值。

add lb vserver v\_mqttlb USER\_TCP –persistencetype USERSESSION

将 MQTT 消息发送到负载均衡器,在负载中将 user_session 字段设置为 clientID。

示例:

-- send the data so far to lb

-- user_session is set to clientID as well (it will be used to persist session)

ns.send(ctxt.output, “DATA”, {data = data, user_session = clientID})
<!--NeedCopy-->

基于 TCP 连接的负载均衡

对于某些协议,可能不需要 MBLB。相反,您可能需要基于 TCP 连接的负载平衡。例如,MQTT 协议必须解析 TCP 流的初始部分,以确定用于负载平衡的令牌。而且,同一 TCP 连接上的所有 MQTT 消息都必须发送到同一个服务器连接。

通过使用仅包含 DATA 事件而不发送任何 EOM 的发送 API,可以实现基于 TCP 连接的负载平衡。这样,下游负载平衡上下文首先根据接收到的数据做出负载平衡决策,然后将所有后续数据发送到负载平衡决策选择的同一个服务器连接。

此外,某些用例可能需要在做出负载平衡决定后能够绕过扩展处理。绕过扩展调用可以提高性能,因为流量纯粹由原生代码处理。可以使用 ns.pipe () API 来完成绕过操作。调用 pipe () API 扩展代码可以将输入上下文连接到输出上下文。调用 pipe () 后,所有来自输入上下文的事件都直接进入输出上下文。实际上,发出 pipe () 调用的模块已从管道中删除。

以下代码段显示了流式处理和使用 pipe () API 绕过模块的情况。 mqtt.lua 的代码清单部分中的 代码清单还说明了如何进行流媒体以及如何使用 pipe () API 绕过该模块以获取连接中的其余流量。

示例:

        -- send the data so far to lb
        ns.send(ctxt.output, "DATA", {data = data,
                                       user_token = clientID})
        -- pipe the subsequent traffic to the lb - to bypass the client on_data handler
        ns.pipe(ctxt.input, ctxt.output)
<!--NeedCopy-->

基于内容的负载平衡

对于本机协议,支持内容切换,例如协议扩展的功能。使用此功能,您可以将数据发送到选定的负载均衡器,而不是将数据发送到默认负载均衡器。

协议扩展的内容交换功能是通过使用 ctxt: lb_connect () API 实现的。<lbname> 此 API 可用于 TCP 客户端上下文。使用此 API,扩展代码可以获得与已配置的负载平衡虚拟服务器对应的负载平衡上下文。然后,您可以将发送 API 与由此获得的负载平衡上下文一起使用。

有时 lb 上下文可能为 NULL:

  • 虚拟服务器不存在
  • 虚拟服务器不是用户协议类型
  • 虚拟服务器的状态未启动
  • 虚拟服务器是用户虚拟服务器,而不是负载平衡虚拟服务器

如果您在使用目标负载平衡虚拟服务器时将其删除,则与该负载平衡虚拟服务器相关的所有连接都将被重置。

以下代码片段显示了 lb_connect () API 的使用。该代码使用 Lua 表 lb_map 将客户端 ID 映射到负载平衡虚拟服务器名称 (lbname),然后使用 lb_connect () 获取 lbname 的 LB 上下文。最后使用发送 API 发送到 LB 上下文。

    local lb_map = {
       ["client1*"] = "lb_1",
       ["client2*"] = "lb_2",
       ["client3*"] = "lb_3",
       ["client4*"] = "lb_4"
    }

    -- map the clientID to the corresponding LB vserver and connect to it
    for client_pattern, lbname in pairs(lb_map) do
       local match_idx = string.find(clientID, client_pattern)
       if (match_idx == 1) then
      lb_ctxt = ctxt:lb_connect(lbname)
      if (lb_ctxt == nil) then
         error("Failed to connect to LB vserver: " .. lbname)
      end
      break
       end
    end
    if (lb_ctxt == nil) then
    -- If lb context is NULL, the user can raise an error or send data to default LB
       error("Failed to map LB vserver for client: " .. clientID)
    end
-- send the data so far to lb
ns.send(lb_ctxt, "DATA", {data = data}
<!--NeedCopy-->

SSL

支持使用扩展的协议的 SSL 的方式与支持本地协议 SSL 的方式类似。使用相同的解析代码创建自定义协议,您可以通过 TCP 或 SSL 创建协议实例,然后用于配置虚拟服务器。同样,您可以通过 TCP 或 SSL 添加用户服务。

有关更多信息,请参阅 为 MQTT 配置 SSL 卸载和使用端到端加密为 MQTT 配置 SSL 卸载。

服务器连接复用

有时,客户端一次发送一个请求,只有在从服务器收到第一个请求的响应后才发送下一个请求。在这种情况下,服务器连接可以重复用于其他客户端连接,也可以在将响应发送到客户端后用于同一连接上的下一条消息。要允许其他客户端连接重用服务器连接,您必须在服务器端上下文中使用 ctxt: reuse_server_connection () API。

注意:此 API 在 NetScaler 12.1 版本 49.xx 及更高版本中可用。

修改流量

要修改请求或响应中的数据,必须使用使用高级策略 PI 表达式的本机重写功能。由于您不能在扩展中使用 PI 表达式,因此您可以使用以下 API 修改 TCP 流数据。

data:replace(offset, length, new_string)
data:insert(offset, new_string)
data:delete(offset, length)
data:gsub(pattern, replace [,n]))

以下代码段显示了 replace () API 的使用。

-- Get the offset of the pattern, we want to replace
   local old_pattern = “pattern to repalace”
local old_pattern_length = old_pattern:len()
   local pat_off, pat_end = data:find(old_pattern)
   -- pattern is not present
if (not pat_off) then
    goto send_data
   end
  -- If the data we want to modify is not completely present, then
  -- wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
     goto done
  end
data:replace(pat_off, old_pattern_length, “new pattern”)
::send_data::
ns.send(ctxt.output, “EOM”, {data = data})
::done::

以下代码段显示了插入 () API 的使用。

data:insert(5, “pattern to insert”)

下面的代码片段显示了插入()API 的使用,当我们想在某个模式之后或之前插入:

-- Get the offset of the pattern, after or before which we want to insert
   local pattern = “pattern after/before which we need to insert”
local pattern_length = pattern:len()
   local pat_off, pat_end = data:find(pattern)
-- pattern is not present
   if (not pat_off) then
    goto send_data
   end
  -- If the pattern after which we want to insert is not
  -- completely present, then wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
     goto done
  end
-- Insert after the pattern
data:insert(pat_end + 1, “pattern to insert”)
   -- Insert before the pattern
data:insert(pat_off, “pattern to insert”)
::send_data::
    ns.send(ctxt.output, “EOM”, {data = data})
::done::

以下代码段显示了删除 () API 的使用。

-- Get the offset of the pattern, we want to delete
   local delete_pattern = “pattern to delete”
local delete_pattern_length = delete_pattern:len()
   local pat_off, pat_end = data:find(old_pattern)
   -- pattern is not present
if (not pat_off) then
          goto send_data
   end
  -- If the data we want to delete is not completely present,
  -- then wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
    goto done
  end
data:delete(pat_off, delete_pattern_length)
::send_data::
ns.send(ctxt.output, “EOM”, {data = data})
::done::

以下代码段显示了 gsub () API 的使用。

    -- Replace all the instances of the pattern with the new string
data:gsub(“old pattern”, “new string”)
-- Replace only 2 instances of “old pattern”
data:gsub(“old pattern”, “new string”, 2)
-- Insert new_string before all instances of “http”
data:gsub(“input data”, “(http)”, “new_string%1”)
-- Insert new_string after all instances of “http”
data:gsub(“input data”, “(http)”, “%1new_string”)
-- Insert new_string before only 2 instances of “http”
data:gsub(“input data”, “(http)”, “new_string%1”, 2)

注意:此 API 在 NetScaler 12.1 版本 50.xx 及更高版本中可用。

向客户端或服务器发起流量

您可以使用 ns.send () API 将源自扩展代码的数据发送到客户端和后端服务器。要从客户端上下文直接向客户端发送或接收响应,必须使用 ctxt.client 作为目标。要直接从服务器上下文向后端服务器发送或接收响应,必须使用 ctxt.server 作为目标。负载中的数据可以是 TCP 流数据或 Lua 字符串。

要停止连接上的流量处理,可以在客户端或服务器上下文中使用 ctxt: close () API。此 API 关闭客户端连接或任何链接到它的服务器连接。

当您调用 ctxt: close () API 时,扩展代码向客户端和服务器连接发送 TCP FIN 数据包,如果在此连接上从客户端或服务器收到更多数据,则设备会重置连接。

以下代码段显示了 ctxt.client 和 ctxt: close () API 的使用。

    -- If the input packet is not MQTT CONNECT type, then
-- send some error response to the client.
function client.on_data(ctxt, payload)
    local data = payload.data
    local offset = 1
    local msg_type = 0
    local error_response = “Missing MQTT Connect packet.”
    byte = data:byte(offset)
msg_type = bit32.rshift(byte, 4)
if (msg_type ~= 1) then
-- Send the error response
   ns.send(ctxt.client, “DATA”, {data = error_response})
-- Since error response has been sent, so now close the connection
    ctxt:close()
end

以下代码段显示了用户可以在正常流量流中注入数据的示例。

-- After sending request, send some log message to the server.
function client.on_data(ctxt, payload)
local data = payload.data
local log_message = “client id : “..data:sub(3, 7)..” user name : “ data:sub(9, 15)
-- Send the request we get from the client to backend server
ns.send(ctxt.output, “DATA”, {data = data})
After sending the request, also send the log message
ns.send(ctxt.output, “DATA”, {data = log_message”})
end

以下代码段显示了 ctxt.to_server API 的使用。

-- If the HTTP response status message is “Not Found”,
-- then send another request to the server.
function server.on_data(ctxt, payload)
    local data = payload.data
    local request “GET /default.html HTTP/1.1\r\n\r\n”ss
    local start, end = data:find(“Not Found”)
    if (start) then
    -- Send the another request to server
        ns.send(ctxt.server, “DATA”, {data = request})
end

注意:此 API 在 NetScaler 12.1 版本 50.xx 及更高版本中可用。

建立连接时的数据处理

在某些用例中,您可能想在建立连接(收到最终的 ACK 时)发送一些数据。例如,在代理协议中,您可能希望在建立连接时将客户端的源和目标 IP 地址及端口发送到后端服务器。在这种情况下,您可以使用 client.init () 回调处理程序来发送建立连接的数据。

下面的代码片段显示了对 client.init () 回调的使用:

-- Send a request to the next processing context
-- on the connection establishment.
function client.init(ctxt)
   local request “PROXY TCP4” + ctxt.client.ip.src.to_s + “ “ + ctxt.client.ip.dst.to_s + “ “ + ctxt.client.tcp.srcport + “ “ +     ctxt.client.tcp.dstport
-- Send the another request to server
   ns.send(ctxt.output, “DATA”, {data = request})
  end

注意:此 API 在 NetScaler 13.0 版本 xx.xx 及更高版本中可用。