递归火山软件开发平台
标题:
【源代码共享】安卓WebSocket服务端
[打印本页]
作者:
xuezhq
时间:
6 天前
标题:
【源代码共享】安卓WebSocket服务端
本帖最后由 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)
}
}
复制代码
欢迎光临 递归火山软件开发平台 (https://bbs.voldp.com/)
Powered by Discuz! X3.4