|
本帖最后由 xuezhq 于 2025-9-20 20:23 编辑
安卓没有发现合适的 WebSocket服务端 ,特使用HP_TCP服务写了一个WebSocket服务端;
使用方法:
1、建立变量并引用类【WebSocket服务器】
2、变量.启动 (“服务IP地址”, 端口号)
3、启用“WebSocket服务器_数据进入”事件 来接受数据
4、使用 变量.发送数据 (连接ID, 1, "要发送的文本数据内容")
已经通过现有的WebSocket在线工具测试,PING/PONG帧暂时未做,大部分都是发的文本数据来保活,未完成或其他需要完善的功能也可以帮忙完善一下,但完善后还请务必回馈一下!
- <火山程序 类型 = "通常" 版本 = 1 />
- 类 WebSocket服务器 <折叠>
- {
- 变量 服务器 <类型 = HP_TCP服务器>
- 变量 ""
- 变量 "// 客户端ID" <类型 = 整数数组类>
- 变量 "// 客户端保活" <类型 = 整数数组类 注释 = "客户端的最后通讯时间">
- 变量 "// 客户端握手" <类型 = 整数数组类 注释 = "0未完成握手,1已完成握手">
- 变量 "// 锁_客户端" <参考 类型 = 线程写锁类 注释 = "防止非 WS 协议连接">
- 常量 关闭码_正常关闭 <公开 类型 = 文本型 值 = "1000" 注释 = "正常关闭(连接完成预期目的)">
- 常量 关闭码_端点离开 <公开 类型 = 文本型 值 = "1001" 注释 = "端点离开(如客户端关闭页面、服务器下线)">
- 常量 关闭码_协议错误 <公开 类型 = 文本型 值 = "1002" 注释 = "协议错误(对方发送了不符合协议的帧)">
- 常量 关闭码_不支持的数据类型 <公开 类型 = 文本型 值 = "1003" 注释 = "不支持的数据类型(如服务器不接受二进制帧)">
- 常量 关闭码_异常关闭 <公开 类型 = 文本型 值 = "1006" 注释 = "异常关闭(未发送关闭帧直接断开,无关闭码)">
- 常量 关闭码_服务器内部错误导致关闭 <公开 类型 = 文本型 值 = "1011" 注释 = "服务器内部错误导致关闭">
- 方法 启动 <公开 类型 = 逻辑型 折叠>
- 参数 服务器地址 <类型 = 文本型 @默认值 = "0.0.0.0">
- 参数 服务器端口 <类型 = 整数 @默认值 = 8000>
- {
- 如果 (服务器.状态 == HP状态.已经启动 || 服务器.状态 == HP状态.正在启动)
- {
- 服务器.停止 ()
- 睡眠当前线程 (500)
- }
- 服务器.端口 = 服务器端口
- 调试输出 ("等候队列大小 " + 到文本 (服务器.等候队列大小))
- 调试输出 ("最大投递数 " + 到文本 (服务器.最大投递数))
- 调试输出 ("通信缓冲区尺寸 " + 到文本 (服务器.通信缓冲区尺寸))
- 调试输出 ("内存缓存池尺寸 " + 到文本 (服务器.内存缓存池尺寸))
- 调试输出 ("连接缓存池尺寸 " + 到文本 (服务器.连接缓存池尺寸))
- 服务器.通信缓冲区尺寸 = 1024 * 1024 * 50 // 100M
- 调试输出 ("通信缓冲区尺寸 " + 到文本 (服务器.通信缓冲区尺寸))
- 返回 (服务器.启动 (服务器地址))
- }
- 方法 HP_TCP服务器_数据进入 <接收事件 类型 = 整数 注释 = " 当服务器接收到客户端数据时,将触发本事件."
- 注释 = " 请注意,HP服务器/Pull服务器/Pack服务器收到数据后,都将会通过本事件通知用户," 注释 = "但不同的服务器将会导致本事件参数不同,请您按照以下方式进行数据接收."
- 注释 = "" 注释 = " 1.HP服务器: HP服务器为PUSH通信模型,接收到数据后,将会立即通过本事件通知用"
- 注释 = "户,并且设置本事件的"当前接收数据长度"和"当前所接收数据"参数." 注释 = ""
- 注释 = " 2.PULL服务器: Pull服务器接收到数据后,将会立即通过本事件通知用户,但是只会"
- 注释 = "设置本事件的"当前接收数据长度"参数,"当前所接收数据"将为空对象,您可以进行数"
- 注释 = "据长度累计,当所接收到数据长度为完整的包长度后,使用方法"抓取数据"或"窥探数据"" 注释 = "从内存中将数据提取,直接组成一个完整的数据包." 注释 = ""
- 注释 = " 3.Pack服务器: Pack服务器接收到数据后,并不会立即通过本事件通知用户,只有当数" 注释 = "据接收完整之后,才会触发本事件,省去您自行拆包组包的步骤."
- 返回值注释 = "本事件返回值无具体意义,请返回默认值0." 折叠 折叠2>
- 参数 来源对象 <类型 = HP_TCP服务器 注释 = "提供事件产生的具体来源对象">
- 参数 标记值 <类型 = 整数 注释 = "用户调用"挂接事件"命令时所提供的"标记值"参数值,非此方式挂接事件则本参数值固定为0.">
- 参数 当前数据来源连接ID <类型 = 整数 注释 = "当前连接ID">
- 参数 当前接收数据长度 <类型 = 整数 注释 = "当前数据长度">
- 参数 当前所接收数据 <类型 = "字节 []" 注释 = "当前所接收数据." 注释 = "请注意: 如果当前服务器为PULL服务器,本参数将为空对象,请不要使用本参数.">
- {
- 如果 (来源对象 == 服务器)
- {
- // 调试输出 ("数据进入=====================")
- 如果 (取数组成员数 (当前所接收数据) < 4)
- {
- // 防止空数据
- 来源对象.断开连接 (当前数据来源连接ID)
- 返回 (0)
- }
- 如果 (字节数组到文本 (字节数组操作.取数组左边 (当前所接收数据, 4)) == "GET ") // WS 握手协议
- {
- 变量 收到数据 <类型 = 文本型>
- 收到数据 = 字节数组到文本 (当前所接收数据)
- // 检查是否为请求建立协议
- 如果 (文本包含 (收到数据, "Sec-WebSocket-Key"))
- {
- 变量 匹配器 <参考 类型 = 正则匹配器类>
- 变量 请求密钥 <类型 = 文本型>
- 变量 回复密钥 <类型 = 文本型>
- 变量 回复内容 <类型 = 文本型>
- 匹配器 = 正则表达式类.编译 ("(?<=Sec-WebSocket-Key: )[A-Za-z0-9+\\/=]+").创建匹配器 (收到数据)
- 如果 (匹配器.查找下一个 ())
- {
- 请求密钥 = 匹配器.取子匹配组 (0)
- // 组织回复密钥
- 如果 (请求密钥 != "")
- {
- 回复密钥 = Base64类.编码至文本 (加解密类.取数据SHA1_字节数组 (文本到字节数组 (请求密钥 + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")), Base64编码标记.不换行)
- 调试输出 ("回复密钥:" + 回复密钥)
- // 组织回复内容
- 回复内容 = "HTTP/1.1 101 Switching Protocols" + "\r\n"
- 回复内容 = 回复内容 + "Upgrade: websocket" + "\r\n"
- 回复内容 = 回复内容 + "Connection: Upgrade" + "\r\n"
- 回复内容 = 回复内容 + "Sec-WebSocket-Accept: " + 回复密钥 + "\r\n"
- 回复内容 = 回复内容 + "Server: BM Server" + "\r\n"
- 回复内容 = 回复内容 + "Access-Control-Allow-Headers: content-type" + "\r\n" + "\r\n"
- 来源对象.发送数据 (当前数据来源连接ID, 文本到字节数组 (回复内容))
- 客户进入 (当前数据来源连接ID)
- }
- 否则
- {
- 来源对象.断开连接 (当前数据来源连接ID)
- 返回 (0)
- }
- }
- 否则
- {
- 来源对象.断开连接 (当前数据来源连接ID)
- 返回 (0)
- }
- }
- }
- <折叠> 否则
- {
- // 调试输出 (加解密类.字节数组到十六进制文本 (当前所接收数据))
- // 尝试解析WS数据协议
- 变量 第一字节 <类型 = 文本型>
- 变量 第二字节 <类型 = 文本型>
- 第一字节 = 整数到二进制文本 (位与 (当前所接收数据 [0], 0xff))
- 第二字节 = 整数到二进制文本 (位与 (当前所接收数据 [1], 0xff))
- // 调试输出 ("第一字节 " + 第一字节)
- // 调试输出 ("第二字节 " + 第二字节)
- 如果 (取文本左边 (第一字节, 1) == "1") // FIN 结束位,=1 即为最后一帧 ;目前缓存设置够大,无需分片,如有分片需求,需要完善分片组包代码
- {
- 变量 操作码 <类型 = 整数>
- 变量 掩码 <类型 = "字节 []" 值 = 空对象 注释 = "内容长度 后面 4 个字节为掩码;MASK=1时有效">
- 操作码 = 进制_到整数 (取文本右边 (第一字节, 4))
- 如果 (取文本左边 (第二字节, 1) != "1")
- {
- // 不接受 没有掩码加密 的数据
- 返回 (0)
- }
- 调试输出 ("FIN 操作码 " + 到文本 (操作码))
- <折叠> 如果 (操作码 == 1 || 操作码 == 2) // 1 表示帧内容是纯文本 2 表示帧内容是二进制数据
- {
- // 掩码标志位"MASK":表示帧内容是否使用异或操作(xor)做简单的加密.目前的 WebSocket 标准规定,客户端发送数据必须使用掩码,而服务器发送则必须不使用掩码.
- 变量 内容长度 <类型 = 整数>
- 变量 内容开始位 <类型 = 整数>
- 内容长度 = 进制_到整数 (取文本右边 (第二字节, 7))
- // 调试输出 ("内容长度 " + 到文本 (内容长度))
- 如果 (内容长度 < 126) // <=125直接表示 内容长度,无扩展
- {
- 掩码 = 字节数组操作.取数组中间 (当前所接收数据, 2, 4)
- 内容开始位 = 6
- }
- 否则 (内容长度 == 126) // 扩展 2 个字节为 内容长度
- {
- 内容长度 = 进制_到整数 (加解密类.字节数组到十六进制文本 (字节数组操作.取数组中间 (当前所接收数据, 2, 2)), 16)
- 掩码 = 字节数组操作.取数组中间 (当前所接收数据, 4, 4)
- 内容开始位 = 8
- }
- 否则 (内容长度 == 127) // 扩展 8 个字节为 内容长度
- {
- 内容长度 = 进制_到整数 (加解密类.字节数组到十六进制文本 (字节数组操作.取数组中间 (当前所接收数据, 2, 2)), 16)
- 掩码 = 字节数组操作.取数组中间 (当前所接收数据, 10, 4)
- 内容开始位 = 14
- }
- // 调试输出 ("内容长度 " + 到文本 (内容长度))
- // 调试输出 ("内容长度 " + 到文本 (取数组成员数 (当前所接收数据) - 内容开始位))
- // 调试输出 ("内容开始位 " + 到文本 (内容开始位))
- // 首先校验数据长度是否正确
- 如果 (内容长度 != 取数组成员数 (当前所接收数据) - 内容开始位)
- {
- 返回 (0)
- }
- // 校验掩码是否正确
- 如果 (取数组成员数 (掩码) != 4)
- {
- 返回 (0)
- }
- // 异或 出数据内容
- 变量 数据内容 <类型 = "字节 []">
- 变量 掩码位 <类型 = 整数 注释 = "使用哪儿个掩码进行 异或 运算,0-3 循环使用">
- 变量 掩码数据 <类型 = "整数 [4]">
- 计次循环 (4) // 掩码是固定的 4 位长度
- {
- 掩码数据 [取循环索引 ()] = 位与 (掩码 [取循环索引 ()], 0xff)
- }
- 数据内容 = 字节数组操作.创建 (内容长度)
- 计次循环 (内容长度)
- {
- 如果 (掩码位 > 3)
- {
- 掩码位 = 0
- }
- 数据内容 [取循环索引 ()] = (字节)位异或 (位与 (当前所接收数据 [内容开始位 + 取循环索引 ()], 0xff), 掩码数据 [掩码位])
- 掩码位 = 掩码位 + 1
- }
- // 调试输出 ("收到内容 " + 字节数组到文本 (数据内容))
- 如果 (操作码 == 1)
- {
- 数据进入 (当前数据来源连接ID, 内容长度, 操作码, 字节数组到文本 (数据内容))
- }
- 否则 (操作码 == 2)
- {
- 数据进入 (当前数据来源连接ID, 内容长度, 操作码, 加解密类.字节数组到十六进制文本 (数据内容))
- }
- }
- 否则 (操作码 == 8) // 8 是关闭连接
- {
- // 掩码标志位"MASK":表示帧内容是否使用异或操作(xor)做简单的加密.目前的 WebSocket 标准规定,客户端发送数据必须使用掩码,而服务器发送则必须不使用掩码.
- 变量 内容长度 <类型 = 整数>
- 变量 内容开始位 <类型 = 整数>
- 内容长度 = 进制_到整数 (取文本右边 (第二字节, 7))
- // 调试输出 ("内容长度 " + 到文本 (内容长度))
- 如果 (内容长度 == 0) // 0 字节(无状态码和原因)
- {
- 来源对象.断开连接 (当前数据来源连接ID)
- 返回 (0)
- }
- 否则 (内容长度 < 126) // <=125直接表示 内容长度,无扩展
- {
- 掩码 = 字节数组操作.取数组中间 (当前所接收数据, 2, 4)
- 内容开始位 = 6
- }
- 否则 (内容长度 == 126) // 扩展 2 个字节为 内容长度
- {
- 内容长度 = 进制_到整数 (加解密类.字节数组到十六进制文本 (字节数组操作.取数组中间 (当前所接收数据, 2, 2)), 16)
- 掩码 = 字节数组操作.取数组中间 (当前所接收数据, 4, 4)
- 内容开始位 = 8
- }
- 否则 (内容长度 == 127) // 扩展 8 个字节为 内容长度
- {
- 内容长度 = 进制_到整数 (加解密类.字节数组到十六进制文本 (字节数组操作.取数组中间 (当前所接收数据, 2, 2)), 16)
- 掩码 = 字节数组操作.取数组中间 (当前所接收数据, 10, 4)
- 内容开始位 = 14
- }
- // 调试输出 ("内容长度 " + 到文本 (内容长度))
- // 调试输出 ("内容长度 " + 到文本 (取数组成员数 (当前所接收数据) - 内容开始位))
- // 调试输出 ("内容开始位 " + 到文本 (内容开始位))
- // 首先校验数据长度是否正确
- 如果 (内容长度 != 取数组成员数 (当前所接收数据) - 内容开始位)
- {
- 返回 (0)
- }
- // 校验掩码是否正确
- 如果 (取数组成员数 (掩码) != 4)
- {
- 返回 (0)
- }
- // 异或 出数据内容
- 变量 数据内容 <类型 = "字节 []">
- 变量 掩码位 <类型 = 整数 注释 = "使用哪儿个掩码进行 异或 运算,0-3 循环使用">
- 变量 掩码数据 <类型 = "整数 [4]">
- 计次循环 (4) // 掩码是固定的 4 位长度
- {
- 掩码数据 [取循环索引 ()] = 位与 (掩码 [取循环索引 ()], 0xff)
- }
- 数据内容 = 字节数组操作.创建 (内容长度)
- 计次循环 (内容长度)
- {
- 如果 (掩码位 > 3)
- {
- 掩码位 = 0
- }
- 数据内容 [取循环索引 ()] = (字节)位异或 (位与 (当前所接收数据 [内容开始位 + 取循环索引 ()], 0xff), 掩码数据 [掩码位])
- 掩码位 = 掩码位 + 1
- }
- // 取出 关闭帧 的在和结构(状态码 + 原因字符串)
- 变量 状态码 <类型 = 整数 注释 = "1000:正常关闭(正常终止连接)." 注释 = "1001:终端离开(如浏览器关闭页面)." 注释 = "1008:消息违反协议(如格式错误)."
- 注释 = "1011:服务器内部错误.">
- 状态码 = 进制_到整数 (加解密类.字节数组到十六进制文本 (字节数组操作.取数组左边 (数据内容, 2)), 16)
- 如果 (状态码 == 1001)
- {
- 来源对象.断开连接 (当前数据来源连接ID)
- }
- 否则
- {
- // 原 帧 回复
- // 第一字节 = 整数到十六进制文本 (进制_到整数 (第一字节)) + 文本到大写(整数到十六进制文本 (进制_到整数 ("0" + 取文本右边 (第二字节, 7)))) + 加解密类.字节数组到十六进制文本 (数据内容)
- 第一字节 = "88" + 文本到大写 (整数到十六进制文本 (进制_到整数 ("0" + 取文本右边 (第二字节, 7)))) + 加解密类.字节数组到十六进制文本 (数据内容) // 首字节 88 是 断开帧 的固定头
- 数据内容 = 加解密类.十六进制文本到字节数组 (第一字节)
- 来源对象.发送数据 (当前数据来源连接ID, 数据内容)
- }
- }
- <折叠> 否则 (操作码 == 9) // 9 是连接保活的 PING
- {
- }
- <折叠> 否则 (操作码 == 10) // 10 是连接保活的 PONG
- {
- }
- }
- 否则
- {
- 来源对象.断开连接 (当前数据来源连接ID)
- }
- }
- }
- 返回 (0)
- }
- 方法 发送数据 <公开 类型 = 逻辑型 折叠>
- 参数 连接ID <类型 = 整数 注释 = "当前连接ID">
- 参数 数据类型 <类型 = 整数 注释 = "1 表示帧内容是纯文本(二进制数据流建议转为Base64后发送,接收端解码)." 注释 = "8 是关闭连接."
- 注释 = "9 是连接保活的 PING.暂不支持" 注释 = "10 是连接保活的 PONG.暂不支持">
- 参数 数据内容 <类型 = 文本型 注释 = "关闭连接 时使用常量中的关闭码,如:WebSocket服务器.关闭码_正常关闭" @默认值 = "">
- {
- 如果 (数据类型 == 1) // 发送文本
- {
- 如果 (数据内容 == "")
- {
- 返回 (假)
- }
- 变量 发送内容 <类型 = "字节 []">
- 变量 内容长度 <类型 = 整数>
- 变量 内容数据 <类型 = 文本型 注释 = "待发送的十六进制文本">
- 发送内容 = 文本到字节数组 (数据内容)
- 内容长度 = 取数组成员数 (发送内容)
- 如果 (内容长度 < 126) // 0-125:直接用 7 位表示
- {
- 返回 (服务器.发送数据 (连接ID, 加解密类.十六进制文本到字节数组 ("81" + 文本到大写 (整数到十六进制文本 (内容长度)) + 加解密类.字节数组到十六进制文本 (发送内容)))) // 首字节 81 是 文本帧 的固定头
- }
- 否则 (内容长度 <= 65535) // 126-65535:7 位设为 126,后跟 16 位长度
- {
- 内容数据 = 文本到大写 (整数到十六进制文本 (内容长度))
- 判断循环 (取文本长度 (内容数据) < 4)
- {
- 内容数据 = "0" + 内容数据 // 16 位长度为 2 个字节= 0xFF *2,需要4位(0xFFFF)
- }
- 返回 (服务器.发送数据 (连接ID, 加解密类.十六进制文本到字节数组 ("81" + 文本到大写 (整数到十六进制文本 (126)) + 内容数据 + 加解密类.字节数组到十六进制文本 (发送内容)))) // 首字节 81 是 文本帧 的固定头
- }
- 否则 // 大于 65535:7 位设为 127,后跟 64 位长度
- {
- 内容数据 = 文本到大写 (整数到十六进制文本 (内容长度))
- 判断循环 (取文本长度 (内容数据) < 16)
- {
- 内容数据 = "0" + 内容数据 // 64 位长度为 8 个字节= 0xFF *8,需要16位(0xFFFF)
- }
- 返回 (服务器.发送数据 (连接ID, 加解密类.十六进制文本到字节数组 ("81" + 文本到大写 (整数到十六进制文本 (126)) + 内容数据 + 加解密类.字节数组到十六进制文本 (发送内容)))) // 首字节 81 是 文本帧 的固定头
- }
- }
- 否则 (数据类型 == 8) // 主动关闭
- {
- 服务器.发送数据 (连接ID, 加解密类.十六进制文本到字节数组 ("881C03E841637469766520636C6F73757265206F66207468652075736572"))
- 返回 (服务器.断开连接 (连接ID))
- }
- 返回 (假)
- }
- 方法 数据进入 <公开 定义事件 类型 = 整数 注释 = " 当服务器接收到客户端数据时,将触发本事件." 返回值注释 = "本事件返回值无具体意义,请返回默认值0." 折叠>
- 参数 连接ID <类型 = 整数 注释 = "当前连接ID">
- 参数 数据长度 <类型 = 整数 注释 = "当前数据长度">
- 参数 数据类型 <类型 = 整数 注释 = "当前数据类型:1 表示帧内容是纯文本,2 表示帧内容是二进制数据(十六进制文本)">
- 参数 数据内容 <类型 = 文本型 注释 = "当前所接收数据." 注释 = "请注意: 如果当前服务器为PULL服务器,本参数将为空对象,请不要使用本参数.">
- 方法 客户进入 <公开 定义事件 类型 = 整数 注释 = " 当服务器接收到客户端握手请求并握手成功后,将触发本事件." 返回值注释 = "本事件返回值无具体意义,请返回默认值0." 折叠>
- 参数 连接ID <类型 = 整数 注释 = "当前连接ID">
- 方法 客户离开 <公开 定义事件 类型 = 整数 注释 = " 当服务器与客户端断开后,将触发本事件." 返回值注释 = "本事件返回值无具体意义,请返回默认值0." 折叠>
- 参数 连接ID <类型 = 整数 注释 = "当前连接ID">
- 方法 进制_到整数 <公开 静态 类型 = 整数 注释 = "本方法可将 X进制文本转换成十进制整数" 返回值注释 = "返回整数" 折叠 @嵌入式方法 = "">
- 参数 参_文本 <类型 = 文本型 注释 = "提供十六进制文本">
- 参数 进制数 <类型 = 整数 注释 = "提供参数 进制数 2-36" @默认值 = 2>
- {
- @ Integer.parseInt(@<参_文本>, @<进制数>)
- }
- 方法 HP_TCP服务器_客户离开 <接收事件 类型 = 整数 注释 = " 当客户端断开服务器后,将触发本事件." 注释 = " 请注意:当本事件被触发后,服务器将会从连接ID队列中删除该连接ID,"
- 注释 = "之后您将不可继续操作此连接ID." 返回值注释 = "本事件返回值无具体意义,请返回默认值0." 折叠>
- 参数 来源对象 <类型 = HP_TCP服务器 注释 = "提供事件产生的具体来源对象">
- 参数 标记值 <类型 = 整数 注释 = "用户调用"挂接事件"命令时所提供的"标记值"参数值,非此方式挂接事件则本参数值固定为0.">
- 参数 当前离开客户ID <类型 = 整数 注释 = "当前离开服务器的连接ID">
- 参数 客户断开原因 <类型 = 整数 注释 = "当前客户断开服务器原因">
- 参数 客户端断开错误码 <类型 = 整数 注释 = "如果断开原因非">
- {
- 如果 (来源对象 == 服务器)
- {
- 客户离开 (当前离开客户ID)
- }
- 返回 (0)
- }
- }
复制代码
|
|