xuezhq 发表于 2025-9-20 20:08:09

【源代码共享】安卓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: )+").创建匹配器 (收到数据)
                  如果 (匹配器.查找下一个 ())
                  {
                        请求密钥 = 匹配器.取子匹配组 (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数据协议
                变量 第一字节 <类型 = 文本型>
                变量 第二字节 <类型 = 文本型>
                第一字节 = 整数到二进制文本 (位与 (当前所接收数据 , 0xff))
                第二字节 = 整数到二进制文本 (位与 (当前所接收数据 , 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 位长度
                        {
                            掩码数据 [取循环索引 ()] = 位与 (掩码 [取循环索引 ()], 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 位长度
                        {
                            掩码数据 [取循环索引 ()] = 位与 (掩码 [取循环索引 ()], 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)
    }
}


页: [1]
查看完整版本: 【源代码共享】安卓WebSocket服务端