作者:教授
微信提供了多种轻量的应用生态,如公众号、小程序、小游戏等。用户可以通过微信开发者平台接入微信的生态系统,满足自己的业务需求。但是依靠微信小程序、小游戏的服务形式,只是基于C端提供服务。要想接入微信生态运行起来,还离不开S端,那么S端可以考虑私有部署、云部署等。
这里重点是C端与S端的通信跟以往的基于HTTP/HTTPS、TCP/UDP协议形式有所区别,微信小游戏的接入依赖WebScoket协议进行通信。这样的话实际上S端只要能提供WebScoket协议访问即可。
但是我们的业务有可能是很早期开发的,原始的S端架构仅支持TCP/UDP协议通信,如果需要接入微信小游戏实现多端混服,就可能需要大量的代码重构工作,重构后的代码还不确定是否能稳定地运行。这让S端的同学挠破脑袋~~
那么,有没有一种可以不重构S端代码而又能直接接入微信小游戏的中间 “代理服务”呢?
答案是 “有的”。
为了解决小游戏后端服务重构问题,提高生产效率,我们设计了一套基于 WebSocket无缝代理TCP/UDP协议的转流框架。它不仅支持WebSocket–>TCP/UDP协议代理,还能利用代理性质,并规划好地域服务,它能作为边缘节点部署到用户就近的地域,这样就实现了用户就近服务,还能提高前端应用的安全访问。WebSocket代理本质上是一个无状态的服务,可以利用云原生部署,也可以基于传统部署,且能实现横向扩展。
由于网关是无状态服务,它仅负责协议转换和转发,所以它能支持不同应用同时使用,类似于 Nginx代理转发。
上图是一个全局应用的网关架构,它实现了就近访问、安全防护、故障自动转移以及全局流量调度等功能。而最基本的前置条件是必须依赖“大内网”。
“为什么要依赖内网?”
由于玩家分布广泛,涉及各省市及地区,接入的运营商也大小各异,玩家所处网络环境错综复杂,跨网跨地区外网传输时可能出现延时及抖动,影响游戏体验。而基于大内网的网关节点能够避开公网抖动,通过内部专线连接源站服务器。这不仅实现了“加速”,还保证了源站的安全性,源站服务器不需要直接暴露到公网,攻击无法直达服务器,保证后端的安全性从而提升高防保护能力。
wsproxy是一个将websocket与tcp协议互转的代理工具,通过网关代理之后,可以直接用原来的tcp服务器,然后客户端用websocket与网关进行连接通信。
PS:若开启proxy protocol协议,需要后端服务器支持获取PP协议报文。可阅读 https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt
go version go1.20 linux/amd64
Intel(R) E5-2699 v3 2.30GHz /4核/8G
GitHub地址:https://github.com/ywjt/wsproxy
测试机:E5-2699 v3 2.30GHz、4核、8G
连接数:1W 连接
转发包:22W pps
PS:瓶颈在网卡软中断,使用更大网络吞吐的硬件,能增加更高的性能。
#安装go
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version
#克隆代码
yum install git -y
git clone https://github.com/ywjt/wsproxy.git
cd wsproxy
export PATH=$PATH:/usr/local/go/bin:`pwd`
export GOPATH=`pwd`
go env -w GO111MODULE=auto
go get -u golang.org/x/sys/unix
#进入主目录
cd wsproxy
go build .
./wsproxy -version
#把编译好的二进制文件复制到bin目录
mv wsproxy ../bin/
cd ../bin/
cat >Dockerfile <<EOF
FROM busybox
WORKDIR /
COPY wsproxy /
EXPOSE 1443
LABEL org.opencontainers.image.authors="YWJT"
LABEL org.opencontainers.image.version="2.3.1-beta"
ENTRYPOINT ["./wsproxy"]CMD ["-h"]
EOF
docker build -t wsproxy:2.3.1 .
docker run --name wsproxy -d -p 1443:1443 wsproxy:2.3.1 -secret test1234
docker ps -a|grep wsproxy
./wsproxy -addr 0.0.0.0:1443 -secret test1234
./wsproxy -help
参数 | 默认值 | 说明 |
-addr | 0.0.0.0:1443 | 服务地址 |
-aes_only | —— | Token强制加密模式 |
-ssl_only | —— | WSS服务模式 |
-buffer | 1024 | 读写缓冲区大小 |
-max_conns | 65536 | 最大连接池 |
-secret | —— | Token加密密钥 |
-timeout | 3 | 代理超时时间 |
##加密方式
ws://your-domain:1443/?token=U2FsdGVkX1+G76LHp6mvNpyMSqR1WoGGTcSLIyD+/7A=
ws://your-domain:1443/ws?token=U2FsdGVkX1+G76LHp6mvNpyMSqR1WoGGTcSLIyD+/7A=
##开启TLS
wss://your-domain:443/?token=U2FsdGVkX1+G76LHp6mvNpyMSqR1WoGGTcSLIyD+/7A=
wss://your-domain:443/ws?token=U2FsdGVkX1+G76LHp6mvNpyMSqR1WoGGTcSLIyD+/7A=
##非加密方式
wss://your-domain:443/?token=127.0.0.1:80
wss://your-domain:443/ws?token=127.0.0.1:80
可以在线测试连接是否正常 http://wstool.js.org 。
备注:建议正式应用时使用强制加密方式。本项目基于自有业务设计,不一定适用于外部业务,请做好详细评估再使用。
客户端发送到网关的目标服务器地址使用AES256加密,并进行base64编码。当全局网关应用时,应保证其后端服务器所在的网段唯一,因此生成的密钥也是唯一的。
示例:
U2FsdGVkX19KIJ9OQJKT/yHGMrS+5SsBAAjetomptQ0=
进行加密目的是为了隐藏后端服务IP地址和端口,保证较高的安全性。而网关是可以作为全局代理,所有业务均可无缝使用。
使用AES算法加密文本格式的后端地址,生成base64编码的密文。可以在线生成:http://tool.oschina.net/encrypt
也可以使用openssl命令生成,如:
echo -n "127.0.0.1:8088" | openssl enc -e -aes-256-cbc -a -salt -k "test1234"
举例,当后端地址为127.0.0.1:8088并且Secret为 test1234 时,密文结果应类似:
U2FsdGVkX1+G76LHp6mvNpyMSqR1WoGGTcSLIyD+/7A=
备注:上述方式都会使用随机Salt,也是建议的方式。它每次加密得出的密文结果并不一样,实现了混淆,提高了密钥的安全性,但并不会影响解密。
按代理协议请求:
网关能复用代理端口进行不同后端协议的转换。
请求URL | 后端协议 | 说明 |
/?token= | TCP | H5客户端 –> 网关WS/WSS –> 后端TCP (必须是tcp协议) |
/udp?token= | UDP | H5客户端 –> 网关WS/WSS –> 后端UDP (未测试) |
/ws?token= | WS | H5客户端 –> 网关WS/WSS –> 后端WS (必须是ws协议) |
*注意必须匹配好对应的后端协议,否则代理不成功。
GitHub地址:https://github.com/novnc/websockify
简介:websockify 是 noVNC 项目的一部分。websockify 是将 WebSocket 流量转换为正常的套接字流量。Websockify 接受 WebSockets 握手,解析它,然后开始在客户端和目标之间双向转发流量。
优点:与本文 wsproxy方案一样实现了WebSocket 与正常套接字流量互转。
缺点:不支持token加密,每个代理服务器需要先添加转发配置,才能实现转发,集群管理不方便。
由于微信小程序要求支持SSL,并且域名数限制最多使用200个。正常开服来说,每个游戏服至少占用一个域名,开到200个服后将需要进行域名回收再用。使用Nginx的location /~ 重定向,可实现统一入口代理。Nginx代理也是最常用的微信小程序代理方式,可使用openresty方案,实现统一API网关。
#微信WSS统一入口代理
#--------------------------------------------------
server {
listen 443 ssl;
server_name your-domain;
...
location /s1001 {
proxy_pass http://{backserver}:{port};
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
...
}
客户端只需要请求 wss://your-domain/s1001,即可将流量转发到对应的 {backserver}:{port} 后端服务上。
优点:可实现统一的API网关,适用于小程序应用。
缺点:不支持TCP/UDP协议转换,不能用于原生tcp/udp协议的后端代理,需要预先添加转发配置。
由于在实践应用中,遇到了某些项目对接完网关后,客户端无法与后端正常收发数据包。这种情况多数是采用WebScoket代理TCP的方式,那么可能是接收到的数据包出现了粘包的情况,导致数据无法解析。
TCP是个“流”协议,所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要人为地去给这些协议划分边界。所谓粘包就是连续给对端发送两个或者两个以上的数据包,对端在一次收取中收到的数据包数量可能大于 1 个,当大于 1 个时,可能是几个(包括一个)包加上某个包的部分,或者干脆就是几个完整的包在一起。当然,也可能收到的数据只是一个包的部分,这种情况一般也叫半包。
网关在转发数据时,设定了一个缓冲区(-buffer),上游线程先将接收到的数据写满缓冲区,下游线程再将整个缓冲区数据一次性封装发送到对端。缓冲区里的数据就可能存在多个包的片段,除非约定好每次收发的包体大小,否则很难避免出现以上几种情况的粘包。
解决粘包的情况简单的做法是给每个TCP数据包添加包头,包头中应该至少包含数据包的长度,这样接收端在接收到数据后,就可以通过读取包头的长度字段,知道每一个数据包的实际长度。
包头 + 包体格式
struct msg_header
{
int32_t bodySize;
int32_t cmd;
};
这就是一个典型的包头格式,bodySize 指定了这个包的包体是多大。由于包头大小是固定的(这里是 size(int32_t) + sizeof(int32_t) = 8 字节),对端先收取包头大小字节数目(当然,如果不够还是先缓存起来,直到收够为止),然后解析包头,根据包头中指定的包体大小来收取包体,等包体收够了,就组装成一个完整的包来处理。在有些实现中,包头中的 bodySize可能被另外一个叫 packageSize 的字段代替,这个字段的含义是整个包的大小,这个时候,我们只要用 packageSize 减去包头大小(这里是 sizeof(msg_header))就能算出包体的大小,原理同上。
参考文献:
https://cppguide.cn/pages/525d9b/
https://cppguide.cn/pages/899207/
https://cppguide.cn/pages/d886eb/
https://mp.weixin.qq.com/s/TP8mlfUm00JaPHNbsj5Osw