网络层

This commit is contained in:
2025-06-28 17:38:22 +08:00
parent 54dc7ba173
commit 605197345b
20 changed files with 482 additions and 376 deletions

View File

@@ -1,175 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏方块容器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, html {
height: 100%;
overflow: hidden;
background-color: #f0f0f0; /* 页面背景色 */
}
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 15px;
width: min(100vw, 100vh); /* 取视口宽高的较小值 */
height: min(100vw, 100vh); /* 确保容器是正方形 */
padding: 20px;
background-color: white; /* 容器背景为白色 */
margin: 0 auto; /* 水平居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 完全居中 */
}
.grid-item {
background-color: white; /* 大方块背景为白色 */
border: 2px solid #e0e0e0; /* 柔和的边框 */
border-radius: 6px;
display: flex;
flex-wrap: wrap; /* 允许内部小方块换行 */
position: relative; /* 为内部元素定位做准备 */
overflow: hidden; /* 隐藏超出部分 */
/* 不再需要aspect-ratio因为grid布局会自动保持正方形 */
}
/* 内部小方块样式 */
.inner-square {
width: 25%; /* 4x4网格每个占25%宽度 */
height: 25%; /* 4x4网格每个占25%高度 */
border: 1px solid #f5f5f5; /* 浅色边框分隔 */
box-sizing: border-box;
}
/* 状态指示器 */
.status-indicator {
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ccc; /* 默认灰色(未连接) */
}
.status-connected {
background-color: #2ecc71; /* 连接成功时绿色 */
}
.status-error {
background-color: #e74c3c; /* 错误时红色 */
}
</style>
</head>
<body>
<div class="grid-container">
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.grid-container');
// 创建16个大方块容器
for (let i = 1; i <= 16; i++) {
const gridItem = document.createElement('div');
gridItem.className = 'grid-item';
gridItem.id = `grid-${i}`; // 为每个方块设置唯一ID
// 添加状态指示器
const statusIndicator = document.createElement('div');
statusIndicator.className = 'status-indicator';
gridItem.appendChild(statusIndicator);
// 在方块内部创建4x4小方块
for (let j = 0; j < 16; j++) {
const innerSquare = document.createElement('div');
innerSquare.className = 'inner-square';
gridItem.appendChild(innerSquare);
}
container.appendChild(gridItem);
// 为每个大方块创建独立的WebSocket连接
connectWebSocket(gridItem, i);
}
// WebSocket连接函数
function connectWebSocket(gridItem, index) {
// 替换为实际的WebSocket服务器地址
const wsUrl = `ws://localhost:8080/game/${index}`;
const ws = new WebSocket(wsUrl);
// 获取状态指示器
const statusIndicator = gridItem.querySelector('.status-indicator');
ws.onopen = () => {
console.log(`方块 ${index} 已连接到服务器`);
statusIndicator.classList.add('status-connected');
statusIndicator.classList.remove('status-error');
// 发送初始化消息(示例)
ws.send(JSON.stringify({
type: 'init',
gridId: index
}));
};
ws.onmessage = (event) => {
console.log(`方块 ${index} 收到消息:`, event.data);
// 在此处理服务器消息,更新小方块状态
// 示例:根据消息改变小方块颜色
const data = JSON.parse(event.data);
if (data.type === 'update') {
updateInnerSquares(gridItem, data);
}
};
ws.onerror = (error) => {
console.error(`方块 ${index} 连接错误:`, error);
statusIndicator.classList.remove('status-connected');
statusIndicator.classList.add('status-error');
};
ws.onclose = () => {
console.log(`方块 ${index} 连接关闭`);
statusIndicator.classList.remove('status-connected');
};
// 将WebSocket实例附加到DOM元素上
gridItem.ws = ws;
}
// 更新小方块状态的函数(示例)
function updateInnerSquares(gridItem, data) {
const innerSquares = gridItem.querySelectorAll('.inner-square');
innerSquares.forEach((square, idx) => {
// 根据服务器数据更新小方块样式
// 示例:随机改变背景色
if (data.squares && data.squares[idx]) {
square.style.backgroundColor = data.squares[idx].color;
}
});
}
// 窗口关闭时关闭所有WebSocket连接
window.addEventListener('beforeunload', () => {
document.querySelectorAll('.grid-item').forEach(item => {
if (item.ws && item.ws.readyState === WebSocket.OPEN) {
item.ws.close();
}
});
});
});
</script>
</div>
</body>
</html>

103
Client/web/game.js Normal file
View File

@@ -0,0 +1,103 @@
const { createApp } = Vue;
const App = {
template: `
<div class="app-container">
<div class="grid-container">
<div class="square" v-for="(square, index) in squares" :key="index">
<div class="connection-dot" :class="{ connected: square.connected }"></div>
</div>
</div>
</div>
`,
data() {
return {
squares: [
{ connected: false, ws: null },
{ connected: false, ws: null },
{ connected: false, ws: null },
{ connected: false, ws: null }
]
}
},
mounted() {
// 为每个正方形建立独立的WebSocket连接
this.squares.forEach((square, index) => {
this.connectWebSocket(square, index);
});
},
methods: {
connectWebSocket(square, index) {
// 替换为实际的WebSocket服务器地址
const wsUrl = `ws://localhost:8501/?token=${index + 1}`;
try {
square.ws = new WebSocket(wsUrl);
square.ws.onopen = () => {
square.connected = true;
console.log(`WebSocket ${index + 1} connected`);
// 连接建立后发送初始消息
try {
square.ws.send(JSON.stringify({
type: "init"
}));
console.log(`Initial message sent to WebSocket ${index + 1}`);
} catch (error) {
console.error(`Failed to send initial message to WebSocket ${index + 1}:`, error);
}
};
square.ws.onclose = (e) => {
square.connected = false;
console.log(`WebSocket ${index + 1} disconnected`);
console.log(e.code, e.reason, e.wasClean)
// 尝试重新连接
// setTimeout(() => this.connectWebSocket(square, index), 1000);
};
square.ws.onerror = (error) => {
console.error(`WebSocket ${index + 1} error:`, error);
};
square.ws.onmessage = (event) => {
console.log(`WebSocket ${index + 1} message:`, event.data);
// 处理接收到的消息
try {
const message = JSON.parse(event.data);
const arr = JSON.parse(message.data);
if (Array.isArray(arr) && arr.length === 2) {
const [x, y] = arr;
console.log(`Creating dot at (${x}, ${y}) for square ${index}`);
const squareElement = document.querySelectorAll('.square')[index];
if (!squareElement) {
console.error('Square element not found');
return;
}
// 创建圆点元素
const dot = document.createElement('div');
dot.className = 'game-dot';
dot.style.left = `${x}px`;
dot.style.top = `${y}px`;
dot.style.zIndex = '10';
// 添加到游戏场景
squareElement.appendChild(dot);
console.log('Dot added successfully');
}
} catch (error) {
console.error('Error processing message:', error);
}
};
} catch (error) {
console.error(`WebSocket ${index + 1} init error:`, error);
}
}
}
};
createApp(App).mount('#app');

17
Client/web/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏方块容器</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script src="game.js"></script>
</body>
</html>

80
Client/web/style.css Normal file
View File

@@ -0,0 +1,80 @@
/* 重置默认边距 */
body, html {
margin: 0;
padding: 0;
height: 100%;
background-color: #f5f5f5;
}
/* 应用容器填满整个视口并居中 */
.app-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
/* 重置body样式 */
body {
margin: 0;
padding: 0;
overflow: hidden;
}
/* 网格容器 - 固定2x2布局 */
.grid-container {
display: grid;
grid-template-columns: 400px 400px;
grid-template-rows: 400px 400px;
gap: 30px;
padding: 40px;
background-color: #e0e0e0;
min-height: 100vh;
width: 100vw;
box-sizing: border-box;
margin: 0 auto;
place-content: center;
}
/* 正方形样式 */
.square {
width: 400px;
height: 400px;
background-color: #ffffff;
background-image:
linear-gradient(to right, #ddd 1px, transparent 1px),
linear-gradient(to bottom, #ddd 1px, transparent 1px);
background-size: 20px 20px;
border: 5px solid #555;
border-radius: 15px;
position: relative;
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
/* 连接状态圆点 */
.connection-dot {
position: absolute;
top: 15px;
right: 15px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #ff4444; /* 默认断开状态-红色 */
transition: background-color 0.3s;
}
.connection-dot.connected {
background-color: #44ff44; /* 连接状态-绿色 */
}
/* 游戏动态圆点 */
.game-dot {
position: absolute;
width: 10px;
height: 10px;
background-color: red;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"gateway/config"
"gateway/grpc_server"
"gateway/handler/ws_handler"
"github.com/gin-gonic/gin"
"github.com/judwhite/go-svc"
"runtime/debug"
@@ -52,10 +51,16 @@ func (p *Program) Start() error {
}()
discover.Listen()
p.server = grpc_server.NewServer(config.Get().Grpc.Registry.TTL)
p.server.Init(config.Get().Grpc.Registry.Address, config.Get().Grpc.Registry.Port)
ws_handler.NewClient(123, nil)
p.server = grpc_server.NewServer(config.Get().Serve.Grpc.TTL)
p.server.Init(config.Get().Serve.Grpc.Address, config.Get().Serve.Grpc.Port)
go func() {
cfg := config.Get()
_ = p.wsServer.Run(
log.GetLogger().Named("gnet"),
fmt.Sprintf("tcp4://0.0.0.0:%v", cfg.Serve.Socket.Web.Port),
true, true, false, false, true, 8,
)
}()
return nil
}

View File

@@ -25,7 +25,7 @@ func (p *Program) initWsServer(cfg *config.Config) error {
//)
p.wsServer = websocket.NewWSServer(
&ws_gateway.GatewayWsServer{},
log.GetLogger(),
log.GetLogger().Named("ws_server"),
5*time.Second,
)

View File

@@ -8,12 +8,22 @@ log:
max_backups: 3
max_age: 7
grpc:
registry:
db:
etcd:
address: [ "10.0.40.9:2379" ]
serve:
grpc:
address: "10.0.40.199"
port: 8500
ttl: 20
db:
etcd:
address: [ "10.0.40.9:2379" ]
socket:
web:
address: "0.0.0.0"
port: 8501
raw:
address: "0.0.0.0"
port: 8502
http:
address: "0.0.0.0"
port: 8503

View File

@@ -1,10 +1,10 @@
package config
type Config struct {
App AppConfig `yaml:"app"`
Log LogConfig `yaml:"log"`
Grpc GrpcConfig `yaml:"grpc"`
DB DBConfig `yaml:"db"`
App *AppConfig `yaml:"app"`
Log *LogConfig `yaml:"log"`
DB *DBConfig `yaml:"db"`
Serve *ServeConfig `yaml:"serve"`
}
type AppConfig struct {
@@ -19,16 +19,25 @@ type LogConfig struct {
Level string `yaml:"level"`
}
type GrpcConfig struct {
Registry *struct {
Address string `yaml:"address"`
Port int `yaml:"port"`
TTL int64 `yaml:"ttl"`
} `yaml:"registry"`
}
type DBConfig struct {
Etcd *struct {
Address []string `yaml:"address"`
} `yaml:"etcd"`
}
type ServeConfig struct {
Grpc *struct {
AddressConfig
TTL int64 `yaml:"ttl"`
} `yaml:"grpc"`
Socket *struct {
Web *AddressConfig `yaml:"web"`
Raw *AddressConfig `yaml:"raw"`
} `yaml:"socket"`
Http *AddressConfig `yaml:"http"`
}
type AddressConfig struct {
Address string `yaml:"address"`
Port int `yaml:"port"`
}

View File

@@ -6,12 +6,9 @@ require (
common v0.0.0-00010101000000-000000000000
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/gobwas/ws v1.4.0
github.com/golang/protobuf v1.5.4
github.com/judwhite/go-svc v1.2.1
github.com/spf13/viper v1.20.1
go.mongodb.org/mongo-driver v1.17.4
golang.org/x/text v0.26.0
go.uber.org/zap v1.27.0
google.golang.org/grpc v1.71.1
google.golang.org/protobuf v1.36.6
)
@@ -20,11 +17,9 @@ require (
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/getsentry/sentry-go v0.34.0 // indirect
@@ -35,8 +30,10 @@ require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
@@ -48,7 +45,6 @@ require (
github.com/panjf2000/ants/v2 v2.11.3 // indirect
github.com/panjf2000/gnet/v2 v2.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/redis/go-redis/v9 v9.10.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
@@ -62,12 +58,12 @@ require (
go.etcd.io/etcd/client/pkg/v3 v3.6.1 // indirect
go.etcd.io/etcd/client/v3 v3.6.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

View File

@@ -1,9 +1,5 @@
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
@@ -11,8 +7,6 @@ github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
@@ -23,8 +17,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
@@ -112,8 +104,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
@@ -154,8 +144,6 @@ go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BS
go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc=
go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0=
go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=

View File

@@ -3,7 +3,9 @@ package ws_handler
import (
"common/log"
"common/net/socket"
"common/utils"
"context"
"fmt"
"go.uber.org/zap"
"runtime/debug"
"sync"
@@ -14,29 +16,90 @@ type Client struct {
sync.WaitGroup
conn socket.ISocketConn // Socket
mailChan chan Event // 邮箱队列
logger *zap.SugaredLogger
ctx context.Context
cancel context.CancelFunc
heartBeat time.Time
logger *zap.SugaredLogger // 日志
ctx context.Context // 上下文
cancel context.CancelFunc // 取消上下文
heartBeat time.Time // 最后一次心跳
UID int32
}
func NewClient(uid int32, conn socket.ISocketConn) *Client {
client := &Client{}
client.UID = uid
client.conn = conn
client.logger = log.GetLogger().Named("uid").With("uid", client.UID)
client.logger.Errorf("错误日志 %v", 1)
client.heartBeat = time.Now()
client.mailChan = make(chan Event, 1024)
client := &Client{
UID: uid,
conn: conn,
logger: log.GetLogger().Named(fmt.Sprintf("uid:%v", uid)),
heartBeat: time.Now(),
mailChan: make(chan Event, 1024),
}
client.ctx, client.cancel = context.WithCancel(context.Background())
client.Add(1)
go client.Loop()
return client
}
// CloseClient 关闭客户端
func (c *Client) Loop() {
defer func() {
if err := recover(); err != nil {
c.logger.Errorf("Client Loop err: %v", err)
debug.PrintStack()
}
}()
defer c.onClose()
t := time.NewTicker(20 * time.Second)
defer t.Stop()
for {
select {
case <-c.ctx.Done():
return
case evt, ok := <-c.mailChan:
if ok {
c.handle(evt)
}
case <-t.C:
_ = c.conn.Ping()
if time.Now().Sub(c.heartBeat) > 120*time.Second {
return
}
}
}
}
func (c *Client) OnEvent(event Event) {
defer func() {
if err := recover(); err != nil {
c.logger.Warnf(fmt.Sprintf("send event chan error: %v", err))
}
}()
select {
case c.mailChan <- event:
default:
c.logger.Warnf("Client mailChan full")
}
}
func (c *Client) handle(event Event) {
switch e := event.(type) {
case *ClientEvent:
m, err := parseMsg(e.Msg)
if err != nil {
c.logger.Errorf("handle event json.Unmarshal err: %v", err)
c.cancel()
}
c.logger.Infof("收到客户端消息:%+v", *m)
switch m.Type {
case "init":
_ = c.conn.Write(wapMsg(&msg{
Type: "init",
Data: fmt.Sprintf("[%v,%v]", utils.RandInt(1, 100), utils.RandInt(1, 100)),
}))
}
case *PongEvent:
c.heartBeat = time.Now()
}
}
// CloseClient 关闭客户端同步会等待onClose执行完成
func (c *Client) CloseClient() {
if c.cancel != nil {
c.cancel()
@@ -56,34 +119,3 @@ func (c *Client) onClose() {
UserMgr.Delete(c.UID)
c.Done()
}
func (c *Client) Loop() {
defer func() {
if err := recover(); err != nil {
c.logger.Errorf("Client Loop err: %v", err)
debug.PrintStack()
}
}()
defer c.onClose()
c.Add(1)
//心跳检测
hearBeatTicker := time.NewTicker(3000 * time.Millisecond)
for {
select {
case <-c.ctx.Done():
return
case _, _ = <-c.mailChan:
case <-hearBeatTicker.C:
// 心跳超时直接关掉连接
if c.checkHeartBeatTimeout() {
return
}
}
}
}
func (c *Client) checkHeartBeatTimeout() bool {
sub := time.Now().Sub(c.heartBeat)
return sub > 60*time.Second
}

View File

@@ -1,18 +1,13 @@
package ws_handler
import (
"google.golang.org/protobuf/proto"
)
type Event interface {
}
// ClientEvent 客户端发过来的Event
type ClientEvent struct {
Event
Msg proto.Message
Msg []byte
}
type RemoveConnectionEvent struct {
type PongEvent struct {
Event
}

View File

@@ -0,0 +1,21 @@
package ws_handler
import "encoding/json"
type msg struct {
Type string `json:"type"`
Data string `json:"data"`
}
func parseMsg(data []byte) (*msg, error) {
m := &msg{}
if err := json.Unmarshal(data, m); err != nil {
return nil, err
}
return m, nil
}
func wapMsg(m *msg) []byte {
data, _ := json.Marshal(m)
return data
}

View File

@@ -1,27 +1,59 @@
package ws_gateway
import (
"common/log"
"common/net/socket"
"fmt"
"gateway/handler/ws_handler"
"go.uber.org/zap"
"strconv"
"time"
)
type GatewayWsServer struct {
logger *zap.SugaredLogger
}
func (g *GatewayWsServer) OnOpen(_ socket.ISocketConn) ([]byte, socket.Action) {
func (g *GatewayWsServer) OnOpen(conn socket.ISocketConn) ([]byte, socket.Action) {
g.logger = log.GetLogger().Named(fmt.Sprintf("addr:%v", conn.RemoteAddr()))
return nil, socket.None
}
func (g *GatewayWsServer) OnHandShake(conn socket.ISocketConn) {
//query := conn.GetParam("query")
token, ok := conn.GetParam("token").(string)
if !ok || len(token) == 0 {
g.logger.Warnf("token is not string")
_ = conn.Close()
return
}
t, err := strconv.Atoi(token)
if err != nil {
_ = conn.Close()
}
client := ws_handler.NewClient(int32(t), conn)
ws_handler.UserMgr.Add(int32(t), client)
conn.SetParam("client", client)
}
func (g *GatewayWsServer) OnMessage(conn socket.ISocketConn, bytes []byte) socket.Action {
client, ok := conn.GetParam("client").(*ws_handler.Client)
if !ok || client.UID == 0 {
return socket.Close
}
client.OnEvent(&ws_handler.ClientEvent{Msg: bytes})
return socket.None
}
func (g *GatewayWsServer) OnClose(conn socket.ISocketConn, _ error) socket.Action {
return socket.None
func (g *GatewayWsServer) OnPong(conn socket.ISocketConn) {
client, ok := conn.GetParam("client").(*ws_handler.Client)
if !ok || client.UID == 0 {
return
}
client.OnEvent(&ws_handler.PongEvent{})
}
func (g *GatewayWsServer) OnClose(_ socket.ISocketConn, _ error) socket.Action {
return socket.Close
}
func (g *GatewayWsServer) OnTick() (time.Duration, socket.Action) {

View File

@@ -56,7 +56,7 @@ func Init(debug bool, maxSize, maxBackups, maxAge int, level string) {
logger = logger.WithOptions(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.WarnLevel),
zap.AddStacktrace(zapcore.ErrorLevel),
)
}
SetLogger(logger.Sugar())

View File

@@ -15,19 +15,33 @@ const (
Shutdown
)
type OpCode byte
const (
OpContinuation OpCode = 0x0
OpText OpCode = 0x1
OpBinary OpCode = 0x2
OpClose OpCode = 0x8
OpPing OpCode = 0x9
OpPong OpCode = 0xa
)
// ISocketServer 由应用层实现
type ISocketServer interface {
OnOpen(ISocketConn) ([]byte, Action) // 开启连接
OnHandShake(ISocketConn) // 开始握手
OnMessage(ISocketConn, []byte) Action // 收到消息
OnClose(ISocketConn, error) Action // 关闭连接
OnPong(ISocketConn)
OnClose(ISocketConn, error) Action // 关闭连接
OnTick() (time.Duration, Action)
}
// ISocketConn 由网络层实现
type ISocketConn interface {
GetParam(key string) string
SetParam(key string, values string)
GetParam(key string) interface{}
SetParam(key string, values interface{})
RemoteAddr() string
Ping() error // 需要隔一段时间调用一下建议20s
Write(data []byte) error
Close() error
}

View File

@@ -66,7 +66,8 @@ func (s *WSServer) OnOpen(c gnet.Conn) ([]byte, gnet.Action) {
curHeader: nil,
cachedBuf: bytes.Buffer{},
},
param: make(map[string]string),
param: make(map[string]interface{}),
remoteAddr: c.RemoteAddr().String(),
}
c.SetContext(ws)
s.unUpgradeConn.Store(c.RemoteAddr().String(), ws)
@@ -78,10 +79,10 @@ func (s *WSServer) OnOpen(c gnet.Conn) ([]byte, gnet.Action) {
// The parameter err is the last known connection error.
func (s *WSServer) OnClose(c gnet.Conn, err error) (action gnet.Action) {
s.unUpgradeConn.Delete(c.RemoteAddr().String())
tmp := c.Context()
ws, ok := tmp.(*WSConn)
ws, ok := c.Context().(*WSConn)
if ok {
s.logger.Warnf("connection close")
ws.isClose = true
ws.logger.Warnf("connection close: %v --------------------------------------------------", err)
return gnet.Action(s.i.OnClose(ws, err))
}
return
@@ -91,18 +92,14 @@ func (s *WSServer) OnClose(c gnet.Conn, err error) (action gnet.Action) {
func (s *WSServer) OnTraffic(c gnet.Conn) (action gnet.Action) {
tmp := c.Context()
if tmp == nil {
if s.logger != nil {
s.logger.Errorf("OnTraffic context nil: %v", c)
}
s.logger.Errorf("OnTraffic context nil: %v", c)
action = gnet.Close
return
}
ws, ok := tmp.(*WSConn)
if !ok {
if s.logger != nil {
s.logger.Errorf("OnTraffic convert ws error: %v", tmp)
}
ws.logger.Errorf("OnTraffic convert ws error: %v", tmp)
action = gnet.Close
return
}
@@ -121,9 +118,7 @@ func (s *WSServer) OnTraffic(c gnet.Conn) (action gnet.Action) {
if data != nil {
err := ws.Conn.AsyncWrite(data, nil)
if err != nil {
if ws.logger != nil {
ws.logger.Errorf("update ws write upgrade protocol error", err)
}
ws.logger.Errorf("update ws write upgrade protocol error", err)
action = gnet.Close
}
}
@@ -131,14 +126,22 @@ func (s *WSServer) OnTraffic(c gnet.Conn) (action gnet.Action) {
} else {
msg, err := ws.readWsMessages()
if err != nil {
if ws.logger != nil {
ws.logger.Errorf("read ws messages errors", err)
}
ws.logger.Errorf("read ws messages errors", err)
return gnet.Close
}
if msg != nil {
for _, m := range msg {
if socket.OpCode(m.OpCode) == socket.OpPong {
s.i.OnPong(ws)
continue
}
if socket.OpCode(m.OpCode) == socket.OpClose {
return gnet.Close
}
if socket.OpCode(m.OpCode) == socket.OpPing {
continue
}
a := s.i.OnMessage(ws, m.Payload)
if gnet.Action(a) != gnet.None {
action = gnet.Action(a)
@@ -182,10 +185,7 @@ func (s *WSServer) OnTick() (delay time.Duration, action gnet.Action) {
continue
}
if err := v.Close(); err != nil {
if s.logger != nil {
s.logger.Errorf("upgrade ws time out close socket error: %v", err)
continue
}
v.logger.Errorf("upgrade ws time out close socket error: %v", err)
}
}
d, a := s.i.OnTick()

View File

@@ -15,12 +15,13 @@ import (
// WSConn 实现ISocketConn接口
type WSConn struct {
gnet.Conn
buf bytes.Buffer
logger logging.Logger
isUpgrade bool
isClose bool
param map[string]string
openTime int64
buf bytes.Buffer
logger logging.Logger
isUpgrade bool
isClose bool
param map[string]interface{}
openTime int64
remoteAddr string
wsMessageBuf
}
@@ -169,19 +170,24 @@ func (w *WSConn) OnRequest(u []byte) error {
return nil
}
func (w *WSConn) GetParam(key string) string {
func (w *WSConn) GetParam(key string) interface{} {
return w.param[key]
}
func (w *WSConn) SetParam(key string, values string) {
func (w *WSConn) SetParam(key string, values interface{}) {
w.param[key] = values
}
func (w *WSConn) RemoteAddr() string {
return w.remoteAddr
}
func (w *WSConn) Write(data []byte) error {
if w.isClose {
return errors.New("connection has close")
}
return w.write(data, ws.OpBinary)
return w.write(data, ws.OpText)
}
func (w *WSConn) Ping() (err error) {
return w.write(make([]byte, 0), ws.OpPing)
}
func (w *WSConn) Close() (err error) {
@@ -192,74 +198,12 @@ func (w *WSConn) Close() (err error) {
}
func (w *WSConn) write(data []byte, opCode ws.OpCode) error {
if w.isClose {
return errors.New("connection has close")
}
buf := bytes.Buffer{}
err := wsutil.WriteServerMessage(&buf, opCode, data)
if err != nil {
if err := wsutil.WriteServerMessage(&buf, opCode, data); err != nil {
return err
}
return w.Conn.AsyncWrite(buf.Bytes(), nil)
}
//func (w *WSConn) Pong(data []byte) (err error) {
// buf := bytes.Buffer{}
// if data == nil {
// err = wsutil.WriteServerMessage(&buf, ws.OpPong, emptyPayload)
// } else {
// err = wsutil.WriteServerMessage(&buf, ws.OpPong, data)
// }
// if w.isClose {
// return errors.New("connection has close")
// }
// if err != nil {
// return
// }
// return w.Conn.AsyncWrite(buf.Bytes(), nil)
//}
//func (w *WSConn) Ping(data []byte) (err error) {
// buf := bytes.Buffer{}
// if data == nil {
// err = wsutil.WriteServerMessage(&buf, ws.OpPing, emptyPayload)
// } else {
// err = wsutil.WriteServerMessage(&buf, ws.OpPing, data)
// }
// if w.isClose {
// return errors.New("connection has close")
// }
// if err != nil {
// return err
// }
//
// return w.Conn.AsyncWrite(buf.Bytes(), nil)
//}
//func (w *WSConn) GetProperty(key string) (interface{}, error) {
// if w.context[key] == nil {
// return nil, errors.New("not found this key")
// }
// return w.context[key], nil
//}
//
//func (w *WSConn) SetProperty(key string, ctx interface{}) {
// w.context[key] = ctx
//}
//
//func (w *WSConn) RemoveProperty(key string) {
// delete(w.context, key)
//}
//
//func (w *WSConn) GetConnID() uint64 {
// return w.connID
//}
//
//func (w *WSConn) Clear() {
// w.context = make(map[string]interface{})
//}
//
//func (w *WSConn) GetUrlParam() map[string][]string {
// return w.urlParma
//}
//
//func (w *WSConn) SetUrlParam(key string, values []string) {
// w.urlParma[key] = values
//}

View File

@@ -0,0 +1,22 @@
syntax = "proto3";
option go_package = "common/proto/gen/cs";
import "common.proto";
enum MessageID {
MESSAGE_ID_INVALID = 0;
// 移动指令
MESSAGE_TYPE_PLAYER_MOVE = 3;
// 聊天消息
MESSAGE_TYPE_CHAT_MESSAGE = 4;
// 心跳包
MESSAGE_TYPE_HEARTBEAT = 5;
}
message Message {
MessageID id = 1;
bytes payload = 2;
}

View File

@@ -0,0 +1,13 @@
package utils
import (
"math/rand"
)
// RandInt 生成 [min, max] 范围内的随机整数
func RandInt(min, max int) int {
if min > max {
min, max = max, min
}
return rand.Intn(max-min+1) + min
}