ws
CI / changes (push) Successful in 1m29s
CI / ad (push) Successful in 0s
CI / admin (push) Successful in 0s
CI / bff (push) Successful in 29s
CI / chore (push) Successful in 0s
CI / equipment (push) Successful in 0s
CI / express (push) Successful in 0s
CI / factory (push) Successful in 0s
CI / message (push) Successful in 1s
CI / order (push) Successful in 0s
CI / payment (push) Successful in 0s
CI / product (push) Successful in 1s
CI / record (push) Successful in 0s
CI / sale (push) Successful in 20s
CI / support (push) Successful in 1s
CI / task (push) Successful in 0s
CI / user (push) Successful in 0s
CI / wecom (push) Successful in 1s
CI / changes (push) Successful in 1m29s
CI / ad (push) Successful in 0s
CI / admin (push) Successful in 0s
CI / bff (push) Successful in 29s
CI / chore (push) Successful in 0s
CI / equipment (push) Successful in 0s
CI / express (push) Successful in 0s
CI / factory (push) Successful in 0s
CI / message (push) Successful in 1s
CI / order (push) Successful in 0s
CI / payment (push) Successful in 0s
CI / product (push) Successful in 1s
CI / record (push) Successful in 0s
CI / sale (push) Successful in 20s
CI / support (push) Successful in 1s
CI / task (push) Successful in 0s
CI / user (push) Successful in 0s
CI / wecom (push) Successful in 1s
This commit is contained in:
+43
-3
@@ -8,11 +8,17 @@ import (
|
||||
"lone-services/bff/internal/dialer"
|
||||
"lone-services/bff/internal/request"
|
||||
"lone-services/bff/internal/response"
|
||||
"lone-services/bff/internal/wsClient"
|
||||
"lone-services/bff/internal/wsproxy"
|
||||
"lone-services/pkg/discovery"
|
||||
"lone-services/pkg/utils"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/gateway"
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/bff.yaml", "the config file")
|
||||
@@ -54,16 +60,50 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Gateway 仅处理普通接口 /api /admin,端口 c.Port=10000
|
||||
gw := gateway.MustNewServer(c.GatewayConf,
|
||||
gateway.WithDialer(dialer.Nacos),
|
||||
|
||||
gateway.WithMiddleware(request.FormUploadMiddleware),
|
||||
// 权鉴+穿透
|
||||
gateway.WithMiddleware(request.UserReadMiddleware),
|
||||
gateway.WithMiddleware(response.Wrap),
|
||||
)
|
||||
defer gw.Stop()
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
rpcMgr := wsproxy.NewRpcClientManager(c.WsPercutaneousRpc,
|
||||
zrpc.WithUnaryClientInterceptor(request.UserClientInterceptor),
|
||||
zrpc.WithStreamClientInterceptor(request.UserStreamInterceptor),
|
||||
)
|
||||
|
||||
wsRest := rest.MustNewServer(rest.RestConf{
|
||||
Host: c.Host,
|
||||
Port: c.WsPort,
|
||||
})
|
||||
defer wsRest.Stop()
|
||||
|
||||
// 注册ws路由到独立rest服务
|
||||
wsRest.AddRoutes([]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/ws/percutaneous/login",
|
||||
Handler: wsClient.PercutaneousLogin(rpcMgr),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/ws/percutaneous/used",
|
||||
Handler: wsClient.PercutaneousUsed(rpcMgr),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/ws/percutaneous/add/user",
|
||||
Handler: wsClient.PercutaneousAddUser(rpcMgr),
|
||||
},
|
||||
})
|
||||
|
||||
go func() {
|
||||
logx.Info("WS rest server start on :", c.WsPort)
|
||||
wsRest.Start()
|
||||
}()
|
||||
|
||||
fmt.Printf("Gateway server at %s:%d\n", c.Host, c.Port)
|
||||
gw.Start()
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/zeromicro/go-zero/gateway"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
gateway.GatewayConf
|
||||
Nacos NacosConf
|
||||
Nacos NacosConf
|
||||
WsPercutaneousRpc zrpc.RpcClientConf `json:",optional"`
|
||||
WsPort int
|
||||
}
|
||||
|
||||
type NacosConf struct {
|
||||
|
||||
@@ -205,3 +205,40 @@ func UserClientInterceptor(ctx context.Context, method string, req, reply any, c
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
// UserStreamInterceptor gRPC Stream客户端拦截器,用于ws桥接的双向流透传metadata
|
||||
func UserStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
user := GetUserInfo(ctx)
|
||||
md := metadata.New(map[string]string{})
|
||||
|
||||
if user.Valid {
|
||||
md.Set("x-user-id", user.RawUID)
|
||||
md.Set("x-user-name", url.QueryEscape(user.Name))
|
||||
}
|
||||
|
||||
setMDIfNotEmpty(md, "x-raw-body", user.RawBody)
|
||||
setMDIfNotEmpty(md, "x-refresh", user.Refresh)
|
||||
setMDIfNotEmpty(md, "x-client-ip", user.ClientIP)
|
||||
if user.UserAgent != utils.StringEmpty {
|
||||
md.Set("x-user-agent", url.QueryEscape(user.UserAgent))
|
||||
}
|
||||
setMDIfNotEmpty(md, "x-user-type", user.Type)
|
||||
setMDIfNotEmpty(md, "x-client-code", user.ClientCode)
|
||||
setMDIfNotEmpty(md, "x-sale-id", user.SaleId)
|
||||
if user.SaleName != utils.StringEmpty {
|
||||
md.Set("x-sale-name", url.QueryEscape(user.SaleName))
|
||||
}
|
||||
setMDIfNotEmpty(md, "x-sale-mobile", user.SaleMobile)
|
||||
setMDIfNotEmpty(md, "x-sale-province", user.SaleProvince)
|
||||
setMDIfNotEmpty(md, "x-store-id", user.StoreId)
|
||||
if user.StoreName != utils.StringEmpty {
|
||||
md.Set("x-store-name", url.QueryEscape(user.StoreName))
|
||||
}
|
||||
setMDIfNotEmpty(md, "x-group-id", user.GroupId)
|
||||
if user.GroupName != utils.StringEmpty {
|
||||
md.Set("x-group-name", url.QueryEscape(user.GroupName))
|
||||
}
|
||||
|
||||
newCtx := metadata.NewOutgoingContext(ctx, md)
|
||||
return streamer(newCtx, desc, cc, method, opts...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package wsClient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"lone-services/bff/internal/request"
|
||||
"lone-services/bff/internal/wsproxy"
|
||||
percutaneous "lone-services/rpc/percutaneous/pb"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// ws handler
|
||||
func PercutaneousLogin(rpcMgr *wsproxy.RpcClientManager) http.HandlerFunc {
|
||||
wsHandler := request.UserReadMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
cli, ready := rpcMgr.GetClient()
|
||||
if !ready {
|
||||
logx.Error("ws request: rpc client not ready, return 503")
|
||||
http.Error(w, "rpc service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
proxyHandler := wsproxy.NewWsProxyHandler(
|
||||
func(ctx context.Context) (grpc.ClientStream, error) {
|
||||
streamCli := percutaneous.NewPercutaneousClient(cli.Conn())
|
||||
return streamCli.WsLoginStream(ctx)
|
||||
},
|
||||
func() protoreflect.ProtoMessage { return &percutaneous.WsStreamRequest{} },
|
||||
)
|
||||
proxyHandler(w, r)
|
||||
})
|
||||
|
||||
return wsHandler
|
||||
}
|
||||
|
||||
func PercutaneousAddUser(rpcMgr *wsproxy.RpcClientManager) http.HandlerFunc {
|
||||
wsHandler := request.UserReadMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
cli, ready := rpcMgr.GetClient()
|
||||
if !ready {
|
||||
logx.Error("ws request: rpc client not ready, return 503")
|
||||
http.Error(w, "rpc service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
proxyHandler := wsproxy.NewWsProxyHandler(
|
||||
func(ctx context.Context) (grpc.ClientStream, error) {
|
||||
streamCli := percutaneous.NewPercutaneousClient(cli.Conn())
|
||||
return streamCli.WsAddUserStream(ctx)
|
||||
},
|
||||
func() protoreflect.ProtoMessage { return &percutaneous.WsStreamRequest{} },
|
||||
)
|
||||
proxyHandler(w, r)
|
||||
})
|
||||
|
||||
return wsHandler
|
||||
}
|
||||
|
||||
func PercutaneousUsed(rpcMgr *wsproxy.RpcClientManager) http.HandlerFunc {
|
||||
wsHandler := request.UserReadMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
cli, ready := rpcMgr.GetClient()
|
||||
if !ready {
|
||||
logx.Error("ws request: rpc client not ready, return 503")
|
||||
http.Error(w, "rpc service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
proxyHandler := wsproxy.NewWsProxyHandler(
|
||||
func(ctx context.Context) (grpc.ClientStream, error) {
|
||||
streamCli := percutaneous.NewPercutaneousClient(cli.Conn())
|
||||
return streamCli.WsUsedStream(ctx)
|
||||
},
|
||||
func() protoreflect.ProtoMessage { return &percutaneous.WsStreamRequest{} },
|
||||
)
|
||||
proxyHandler(w, r)
|
||||
})
|
||||
|
||||
return wsHandler
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package wsproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
type StreamCreator func(ctx context.Context) (grpc.ClientStream, error)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func NewWsProxyHandler(createStream StreamCreator, newFrame func() protoreflect.ProtoMessage) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
stream, err := createStream(r.Context())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer stream.CloseSend()
|
||||
|
||||
// 上行 ws -> grpc stream
|
||||
go func() {
|
||||
for {
|
||||
_, rawBytes, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg := newFrame()
|
||||
fd := msg.ProtoReflect().Descriptor().Fields().ByName("raw_body")
|
||||
msg.ProtoReflect().Set(fd, protoreflect.ValueOfBytes(rawBytes))
|
||||
_ = stream.SendMsg(msg)
|
||||
}
|
||||
}()
|
||||
|
||||
// 下行 grpc stream -> ws
|
||||
for {
|
||||
msg := newFrame()
|
||||
err := stream.RecvMsg(msg)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fd := msg.ProtoReflect().Descriptor().Fields().ByName("raw_body")
|
||||
val := msg.ProtoReflect().Get(fd)
|
||||
_ = conn.WriteMessage(websocket.TextMessage, val.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全局rpc客户端管理器,线程安全
|
||||
type RpcClientManager struct {
|
||||
mu sync.RWMutex
|
||||
client zrpc.Client
|
||||
ready bool
|
||||
cfg zrpc.RpcClientConf
|
||||
streamOpts []zrpc.ClientOption
|
||||
}
|
||||
|
||||
func NewRpcClientManager(cfg zrpc.RpcClientConf, streamOpts ...zrpc.ClientOption) *RpcClientManager {
|
||||
m := &RpcClientManager{
|
||||
cfg: cfg,
|
||||
streamOpts: streamOpts,
|
||||
}
|
||||
go m.loopReconnect()
|
||||
return m
|
||||
}
|
||||
|
||||
// 尝试建立一次连接
|
||||
func (m *RpcClientManager) tryConnect() error {
|
||||
cli, err := zrpc.NewClient(m.cfg, m.streamOpts...)
|
||||
if err != nil {
|
||||
logx.Errorf("tryConnect: create client failed, err=%v", err)
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.client = cli
|
||||
m.ready = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// 后台循环重试
|
||||
func (m *RpcClientManager) loopReconnect() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
m.mu.RLock()
|
||||
ready := m.ready
|
||||
m.mu.RUnlock()
|
||||
if ready {
|
||||
logx.Debug("rpc client already ready, skip reconnect")
|
||||
continue
|
||||
}
|
||||
if err := m.tryConnect(); err != nil {
|
||||
logx.Errorf("loopReconnect reconnect failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取client,判断是否就绪
|
||||
func (m *RpcClientManager) GetClient() (zrpc.Client, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.client, m.ready
|
||||
}
|
||||
Reference in New Issue
Block a user