2023-07-20 18:01:10 +08:00
|
|
|
|
## TCP服务器
|
|
|
|
|
|
|
|
|
|
目录
|
|
|
|
|
|
|
|
|
|
- 配置文件
|
|
|
|
|
- 一个基本的消息收发例子
|
|
|
|
|
- 注册路由
|
|
|
|
|
- 拦截器
|
|
|
|
|
- 服务认证
|
|
|
|
|
- 更多
|
|
|
|
|
|
2024-03-07 20:08:56 +08:00
|
|
|
|
> HotGo基于GoFrame的TCP服务器组件,提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能,如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等,大大简化和规范了服务器开发流程。
|
2023-07-20 18:01:10 +08:00
|
|
|
|
|
|
|
|
|
### 配置文件
|
|
|
|
|
- 配置文件:server/manifest/config/config.yaml
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
tcp:
|
|
|
|
|
# 服务器
|
|
|
|
|
server:
|
|
|
|
|
address: ":8099"
|
|
|
|
|
# 客户端
|
|
|
|
|
client:
|
|
|
|
|
# 定时任务
|
|
|
|
|
cron:
|
|
|
|
|
group: "cron" # 分组名称
|
|
|
|
|
name: "cron1" # 客户端名称
|
|
|
|
|
address: "127.0.0.1:8099" # 服务器地址
|
|
|
|
|
appId: "1002" # 应用名称
|
|
|
|
|
secretKey: "hotgo" # 密钥
|
|
|
|
|
# 系统授权
|
|
|
|
|
auth:
|
|
|
|
|
group: "auth" # 分组名称
|
|
|
|
|
name: "auth1" # 客户端名称
|
|
|
|
|
address: "127.0.0.1:8099" # 服务器地址
|
|
|
|
|
appId: "mengshuai" # 应用名称
|
|
|
|
|
secretKey: "123456" # 密钥
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
- 可以看到,除了服务器配置外,还有两个客户端配置`cron` 和`auth`
|
|
|
|
|
- `cron`是HotGo内置的定时任务服务,和http服务通过RPC通讯以实现和后台交互,使其可以独立、集群部署。
|
|
|
|
|
- `auth`可以为第三方平台提供授权服务。如果你需要他,可以将它部署在第三方程序中,在重要的位置进行授权验证。
|
|
|
|
|
|
|
|
|
|
### 一个基本的消息收发测试用例
|
|
|
|
|
|
|
|
|
|
- 文件路径:server/internal/library/network/tcp/tcp_example_test.go
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
package tcp_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/gogf/gf/v2/os/gctx"
|
|
|
|
|
"github.com/gogf/gf/v2/test/gtest"
|
|
|
|
|
"hotgo/internal/library/network/tcp"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var T *testing.T // 声明一个全局的 *testing.T 变量
|
|
|
|
|
|
|
|
|
|
type TestMsgReq struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TestMsgRes struct {
|
|
|
|
|
tcp.ServerRes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TestRPCMsgReq struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TestRPCMsgRes struct {
|
|
|
|
|
tcp.ServerRes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func onTestMsg(ctx context.Context, req *TestMsgReq) {
|
|
|
|
|
fmt.Printf("服务器收到消息 ==> onTestMsg:%+v\n", req)
|
|
|
|
|
conn := tcp.ConnFromCtx(ctx)
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNE(conn, nil)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
res := new(TestMsgRes)
|
|
|
|
|
res.Message = fmt.Sprintf("你的名字:%v", req.Name)
|
|
|
|
|
conn.Send(ctx, res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func onResponseTestMsg(ctx context.Context, req *TestMsgRes) {
|
|
|
|
|
fmt.Printf("客户端收到响应消息 ==> TestMsgRes:%+v\n", req)
|
|
|
|
|
err := req.GetError()
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func onTestRPCMsg(ctx context.Context, req *TestRPCMsgReq) (res *TestRPCMsgRes, err error) {
|
|
|
|
|
fmt.Printf("服务器收到消息 ==> onTestRPCMsg:%+v\n", req)
|
|
|
|
|
res = new(TestRPCMsgRes)
|
|
|
|
|
res.Message = fmt.Sprintf("你的名字:%v", req.Name)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func startTCPServer() {
|
|
|
|
|
serv := tcp.NewServer(&tcp.ServerConfig{
|
|
|
|
|
Name: "hotgo",
|
|
|
|
|
Addr: ":8002",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 注册路由
|
|
|
|
|
serv.RegisterRouter(
|
|
|
|
|
onTestMsg,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 注册RPC路由
|
|
|
|
|
serv.RegisterRPCRouter(
|
|
|
|
|
onTestRPCMsg,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 服务监听
|
|
|
|
|
err := serv.Listen()
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 一个基本的消息收发
|
|
|
|
|
func TestSendMsg(t *testing.T) {
|
|
|
|
|
T = t
|
|
|
|
|
go startTCPServer()
|
|
|
|
|
|
|
|
|
|
ctx := gctx.New()
|
|
|
|
|
client := tcp.NewClient(&tcp.ClientConfig{
|
|
|
|
|
Addr: "127.0.0.1:8002",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 注册路由
|
|
|
|
|
client.RegisterRouter(
|
|
|
|
|
onResponseTestMsg,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
err := client.Start()
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
})
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// 确保服务都启动完成
|
|
|
|
|
time.Sleep(time.Second * 1)
|
|
|
|
|
|
|
|
|
|
// 拿到客户端的连接
|
|
|
|
|
conn := client.Conn()
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNE(conn, nil)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 向服务器发送tcp消息,不会阻塞程序执行
|
|
|
|
|
err := conn.Send(ctx, &TestMsgReq{Name: "Tom"})
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 向服务器发送rpc消息,会等待服务器响应结果,直到拿到结果或响应超时才会继续
|
|
|
|
|
var res TestRPCMsgRes
|
|
|
|
|
if err = conn.RequestScan(ctx, &TestRPCMsgReq{Name: "Tony"}, &res); err != nil {
|
|
|
|
|
gtest.C(T, func(t *gtest.T) {
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("客户端收到RPC消息响应 ==> TestRPCMsgRes:%+v\n", res)
|
|
|
|
|
time.Sleep(time.Second * 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 注册路由
|
|
|
|
|
|
|
|
|
|
- 从上面的例子可以看到,不管是普通TCP消息和RPC消息的请求/响应结构体都采用类似GF框架的规范路由的结构,请求`XxxRes`/响应`XxxRes`的格式,是不是很亲切?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 拦截器
|
|
|
|
|
|
|
|
|
|
- 不管是服务端还是客户端,在初始化时都可以注册多个拦截器来满足更多场景的服务开发,下面是一个使用例子:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
|
"hotgo/internal/library/network/tcp"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
serv = tcp.NewServer(&tcp.ServerConfig{
|
|
|
|
|
Name: "hotgo",
|
|
|
|
|
Addr: ":8002",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 注册拦截器
|
|
|
|
|
// 执行顺序是从前到后,即Interceptor -> Interceptor2 -> Interceptor3。如果中间有任意一个抛出错误,则会中断后续处理
|
|
|
|
|
serv.RegisterInterceptor(Interceptor, Interceptor2, Interceptor3)
|
|
|
|
|
|
|
|
|
|
// 服务监听
|
|
|
|
|
if err := serv.Listen(); err != nil {
|
|
|
|
|
if !serv.IsClose() {
|
|
|
|
|
g.Log().Warningf(ctx, "TCPServer Listen err:%v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Interceptor(ctx context.Context, msg *tcp.Message) (err error) {
|
|
|
|
|
// 可以在拦截器中通过上下文拿到连接
|
|
|
|
|
conn := tcp.ConnFromCtx(ctx)
|
|
|
|
|
|
|
|
|
|
// 拿到原始请求消息
|
|
|
|
|
g.Dump(msg)
|
|
|
|
|
|
|
|
|
|
// 如果想要中断后续处理只需返回一个错误即可,但注意两种情况
|
|
|
|
|
// tcp消息:如果你还想对该消息进行回复应在拦截器中进行处理,例如:conn.Send(ctx, 回复消息内容)
|
|
|
|
|
// rpc消息:返回一个错误后系统会将错误自动回复到rpc响应中,无需单独处理
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Interceptor2(ctx context.Context, msg *tcp.Message) (err error) {
|
|
|
|
|
// ...
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Interceptor3(ctx context.Context, msg *tcp.Message) (err error) {
|
|
|
|
|
// ...
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 服务认证
|
|
|
|
|
|
|
|
|
|
- 一般情况下,建议客户端连接到服务器时都通过`授权许可证`的方式进行登录认证,当初始化客户端配置认证数据时,连接成功后会自动进行登录认证。
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// 创建客户端配置
|
|
|
|
|
clientConfig := &tcp.ClientConfig{
|
|
|
|
|
Addr: "127.0.0.1:8002",
|
|
|
|
|
AutoReconnect: true,
|
|
|
|
|
// 认证数据
|
|
|
|
|
// 认证数据可以在后台-系统监控-在线服务-许可证列表中添加,同一个授权支持多个服务使用,但多个服务不能使用相同的名称进行连接
|
|
|
|
|
Auth: &tcp.AuthMeta{
|
|
|
|
|
Name: "服务名称",
|
|
|
|
|
Group: "服务分组",
|
|
|
|
|
AppId: "APPID",
|
|
|
|
|
SecretKey: "SecretKey",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化客户端
|
|
|
|
|
client = tcp.NewClient(clientConfig)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 更多
|
|
|
|
|
|
|
|
|
|
TCP服务器源码路径:server/internal/library/network/tcp
|
|
|
|
|
|
|
|
|
|
更多文档请参考:https://goframe.org/pages/viewpage.action?pageId=1114625
|