feat: 在线双人五子棋(Socket.IO + Vue3 + Canvas)

- 服务端权威状态机:落子校验、四方向五连判胜、悔棋请求、认输、离线判负、交换黑白再来一局
- 断线重连:resumeToken 凭证 + localStorage,刷新/锁屏自动回到原座位
- 移动端 Canvas 棋盘:DPR 适配、触摸容差吸附、最后一手标记、胜利连子高亮
- Docker 多阶段构建,非 root + 只读根文件系统
- k8s 清单:单副本 Recreate(内存态房态)、Ingress WebSocket 超时与粘性会话注释
This commit is contained in:
root
2026-09-10 18:17:10 +08:00
commit 59bfc98afa
22 changed files with 5995 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
# 本机镜像导入集群的两种方式(无镜像仓库场景):
#
# 方式 A:集群就是执行 docker build 的这台机器(单节点或每台节点都构建)
# docker save wuziqi:0.1.0 -o wuziqi-0.1.0.tar
# ctr -n k8s.io images import wuziqi-0.1.0.tar
#
# 方式 B:集群在别的机器上
# docker save wuziqi:0.1.0 -o wuziqi-0.1.0.tar
# scp wuziqi-0.1.0.tar <node>:~/
# 在【每台可能被调度到的节点】上执行:ctr -n k8s.io images import ~/wuziqi-0.1.0.tar
#
# 导入后配合 imagePullPolicy: IfNotPresent 即可直接启动,无需任何 registry。
apiVersion: apps/v1
kind: Deployment
metadata:
name: wuziqi
namespace: wuziqi
labels:
app: wuziqi
spec:
# 【必须保持为 1】
# 房间与棋局状态存放在进程内存(RoomManager),多副本会各自持有一份状态:
# 两名玩家被 Ingress 分流到不同 Pod 时,第二个玩家会看到"房间不存在"。
# 若确实需要多副本,必须引入 Redis + Socket.IO Redis Adapter,并把房间状态外置。
replicas: 1
strategy:
# Recreate:先终止旧 Pod 再创建新 Pod。
# 默认的 RollingUpdate 会让新旧两个 Pod 短暂并存,此时新 Pod 内存里没有旧 Pod 的房间,
# 进行中的对局会"房间丢失"。
type: Recreate
selector:
matchLabels:
app: wuziqi
template:
metadata:
labels:
app: wuziqi
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# 内存态服务,收到 SIGTERM 后推完最后一次状态即可退出
terminationGracePeriodSeconds: 15
containers:
- name: wuziqi
image: wuziqi:0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
env:
- name: PORT
value: "3000"
- name: NODE_ENV
value: production
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
# 进程不需要写文件系统,静态资源与构建产物都是只读的
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
+55
View File
@@ -0,0 +1,55 @@
# 部署前需要改两处:
# 1. spec.ingressClassName —— 用 `kubectl get ingressclass` 确认集群里实际的 class 名
# 2. spec.rules[].host 与 spec.tls[].hosts —— 换成你自己的域名
#
# 证书二选一:
# A) cert-manager 自动签发:在 annotations 里加
# cert-manager.io/cluster-issuer: <你的 ClusterIssuer 名>
# B) 手动导入已有证书:
# kubectl -n wuziqi create secret tls wuziqi-tls \
# --cert=fullchain.pem --key=privkey.pem
# 没有域名时删掉整段 spec.tls,并去掉 annotations 里的 force-ssl-redirect(若有),
# 改为纯 HTTP 访问;Socket.IO 在 HTTP 下也能工作,但手机网络上不如 wss 稳定。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wuziqi
namespace: wuziqi
labels:
app: wuziqi
annotations:
# —— Socket.IO 长连接的关键三项 ——
# 默认 proxy-read-timeout 只有 60s,长连接会被 Ingress 直接掐断,
# 表现为手机每隔一分钟掉线一次。心跳间隔 25s,这里放宽到 1 小时。
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# 关闭缓冲,避免事件被攒着不发导致落子延迟
nginx.ingress.kubernetes.io/proxy-buffering: "off"
# 显式声明 WebSocket Upgrade 的目标 Service
nginx.ingress.kubernetes.io/websocket-services: "wuziqi"
# —— 会话亲和性 ——
# 客户端 transports 为 [websocket, polling]WebSocket 不通时会回退到长轮询。
# 长轮询的握手与后续请求必须落到同一 Pod,否则会话丢失。
# replicas=1 时这几项是空操作,但一旦调大副本数就是必需的。
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "wuziqi-affinity"
nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"
nginx.ingress.kubernetes.io/session-cookie-samesite: "Lax"
spec:
ingressClassName: nginx
tls:
- hosts:
- gomoku.example.com
secretName: wuziqi-tls
rules:
- host: gomoku.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wuziqi
port:
number: 80
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: wuziqi
labels:
app: wuziqi
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: wuziqi
namespace: wuziqi
labels:
app: wuziqi
spec:
type: ClusterIP
selector:
app: wuziqi
ports:
- name: http
port: 80
targetPort: http