用 Golang 写了个通用路由器,除了能路由 HTTP 协议外,还能路由 Websocket/Tcp/Udp 等协议,欢迎体验 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
idrunk
V2EX    分享创造

用 Golang 写了个通用路由器,除了能路由 HTTP 协议外,还能路由 Websocket/Tcp/Udp 等协议,欢迎体验

  •  
  •   idrunk
    idrunk 338 天前 2717 次点击
    这是一个创建于 338 天前的主题,其中的信息可能已经有所发展或是发生改变。

    求职时发现 Golang 岗位挺多,本身对它也挺感兴趣,于是花了一个月时间,把我的 DCE 用 Golang 升级重构了遍,来学习练手。于是,DCE-GO诞生了。(仍然求职中,go/rust/全栈,远程/深圳,春节不休,邮箱:aGlAaWRydW5rLm5ldA==,欢迎联系)


    DCE-GO 是一个功能强大的通用路由库,不仅支持 HTTP 协议,还能路由 CLI 、WebSocket 、TCP/UDP 等非标准协议。它采用模块化设计,按功能划分为以下核心模块:

    1. 路由器模块
      作为 DCE 的核心模块,定义了 API 、上下文及路由器库,同时提供了转换器、可路由协议等接口,确保灵活性和扩展性。

    2. 可路由协议模块
      封装了多种常见协议的可路由实现,包括 HTTP 、CLI 、WebSocket 、TCP 、UDP 、QUIC 等,满足多样化场景需求。

    3. 转换器模块
      内置 JSON 和模板转换器,支持串行数据的序列化与反序列化,以及传输对象与实体对象的双向转换。

    4. 会话管理器模块
      定义了基础会话、用户会话、连接会话及自重生会话接口,并提供了 Redis 和共享内存的实现类库,方便开发者快速集成。

    5. 工具模块
      提供了一系列实用工具,简化开发流程。

    DCE-GO 的所有功能特性均配有详细用例,位于 _examples 目录下。其路由性能与 Gin 相当,具体性能测试报告可查看 ab 测试结果,其中端口 2046 为 DCE 的测试结果。

    DCE-GO 源自 DCE-RUST,而两者均基于 DCE-PHP 的核心路由模块升级而来。DCE-PHP 是一个完整的网络编程框架,现已停止更新,其核心功能已迁移至 DCE-RUST 和 DCE-GO 。目前,DCE-GO 的功能版本较新,未来 DCE-RUST 将与之同步。

    DCE 致力于打造一个高效、开放、安全的通用路由库,欢迎社区贡献,共同推动其发展。


    TODO

    • 优化 JS 版 WebSocket 可路由协议客户端,完善各协议的 Golang 客户端实现。
    • 升级控制器前后置事件接口,支持与程序接口绑定。
    • 完善数字路径支持。
    • 调整弹性数字函数为结构方法式。
    • 研究可路由协议中支持自定义业务属性的可能性。
    • 升级 DCE-RUST 功能版本。
    • 校验优化 AI 生成的文档,逐步完善。

    使用示例

    以下是一个简单的 TCP 请求响应示例,利用dce/router实现,基本涵盖了DCE所有核心特性的使用方法。通过此示例,开发者可以快速上手并理解DCE的强大功能与灵活性。

    测试步骤: 1: 新建go.mod

    module example go 1.23.3 require ( github.com/idrunk/dce-go v0.1.1 ) 

    2: 在同目录新建main.go并粘贴尾部的代码

    3: 打开命令行终端并cd到同目录,执行go mod tidy自动引入依赖模块

    4: 在命令行终端同目录下执行go run . tcp start启动 TCP 服务器

    5: 新建一个终端cd到同目录,执行go run main.go sign,输入用户名密码登录(可随便输,按回车完成,会自动注册)

    6: 第5步成功将响应一个“SESSION-ID”,复制该 ID ,替换到此命令go run main.go signer $SESSION_ID回车执行,获取脱敏的登录者信息

    package main import ( "bufio" "encoding/json" "fmt" "log/slog" "net" "os" "slices" "strings" "github.com/idrunk/dce-go/converter" "github.com/idrunk/dce-go/proto" "github.com/idrunk/dce-go/proto/flex" "github.com/idrunk/dce-go/router" "github.com/idrunk/dce-go/session" "github.com/idrunk/dce-go/util" ) func main() { // 启动一个 TCP 服务器:go run main.go tcp start // 你还可以指定绑定 IP 与端口,如:go run main.go tcp start 0.0.0.0:2048 // 配置一条 CliRouter 路由规则,绑定处理方法,在该方法中启动一个 TCP 服务器 proto.CliRouter.Push("tcp/start/{address?}", func(c *proto.Cli) { bindServer() addr := c.ParamOr("address", ":2048") listener, err := net.Listen("tcp", addr) if err != nil { panic(err.Error()) } defer listener.Close() fmt.Printf("tcp server start at %s\n", addr) for { conn, err := listener.Accept() if err != nil { slog.Warn(fmt.Sprintf("accept error: %s", err)) continue } go func(conn net.Conn) { defer conn.Close() // Connection sessions are used to store the connection information for sending message across hosts to clients in a distributed environment. // 新建一个影子会话,影子会话仅用于长连接,用于将连接信息记录到 Session 中,以便跨主机向目标客户端发消息 shadow, err := session.NewShmSession[Member](nil, session.DefaultTtlMinutes) if err != nil { slog.Warn(fmt.Sprintf("new session error: %s", err)) return } shadow.Connect(conn.LocalAddr().String(), conn.RemoteAddr().String()) // 连接断开时从会话中删除连接相关信息 defer shadow.Disconnect() for { // 读取解包 TCP 输入流,路由定位 API ,并调用控制函数处理,按需响应处理结果 if !flex.TcpRouter.Route(conn, map[string]any{"$shadowSession": shadow}) { break } } }(conn) } }) bindClient() // 取命令行参数,路由定位 API ,并调用控制函数处理,按需响应(输出)处理结果 proto.CliRoute(1) } func bindServer() { // 设置 TcpRouter 控制处理前置事件(当前无法与特定`Api`绑定,需自行判断,下个大版本将支持绑定指定`Api`) flex.TcpRouter.SetEventHandler(func(c *flex.Tcp) error { // 从可路由协议对象上下文取影子会话,必定能取到所以直接推定为会话指针 shadow, _ := c.Rp.CtxData("$shadowSession") rs := shadow.(*session.ShmSession[Member]) // 用影子会话克隆一个请求会话(影子会话可能过旧,此法方法将更新之,并将影子会话临时记录的连接信息同步到请求会话) cloned, err := rs.CloneForRequest(c.Rp.Sid()) if err != nil { // 克隆失败返回错误(返回错误将阻止控制函数与`AfterController`调用) return err } se := cloned.(*session.ShmSession[Member]) if roles := util.MapSeqFrom[any, uint16](c.Api.ExtrasBy("roles")).Map(func(i any) uint16 { return uint16(i.(int)) }).Collect(); len(roles) > 0 { // Roles configured means need to login // 若配置了`roles`意味着需要登录 if member, ok := se.User(); !ok { // 无法从会话取到用户信息意味着未登录,返回相应错误 return util.Openly(401, "need to login") } else if !slices.Contains(roles, member.Role) { // 授权`roles`中不包含当前会员角色意味着无权,返回相应错误 return util.Openly(403, "no permission") } else if newer, err := session.NewAutoRenew(se).TryRenew(); err != nil { // 自更新会话遇到错误则返回(自更新并非会话过期更新续命,而是在会话有效期内,短时间高频率的更新会话 ID ,以增强会话安全性) return err } else if newer { // Logged session need to auto renew to enhance security // 若自更新成功,则设置响应心的会话 ID c.Rp.SetRespSid(se.Id()) } } // 将会话指针设置到可路由协议对象上下文,以便在控制器函数等中直接获取,而无需重新创建 c.Rp.SetSession(se) return nil }, nil) // 配置一条 TcpRouter 路由规则,绑定服务端登录 API 。 // (`Push`方法绑定的都是自动响应式 API ,若无需自动响应,请用`PushApi`方法并指定`Api.Responsive`为`false`) flex.TcpRouter.Push("sign", func(c *flex.Tcp) { // 新建一个 JSON 转换器,该转换器仅将请求数据序列转换为对象,不作传输对象与实体对象转换,不作响应转换 // (在上一个版本( DCE-RUST )中,序列与 DTO 对象转换逻辑,是集成在路由流程中自动处理的,这在无需转换的情况下, // 也需做处理判断,并且需在整个路由流携带 DTO 泛型类型,不太合理,所以在新版提取到单独的转换器模块了,可随时按需调用) jc := converter.JsonConverterSame[*flex.TcpProtocol, Member, router.DoNotConvert](c) // 获取请求数据并转换为 Member 对象 signInfo, ok := jc.Parse() if !ok { return } // 若入参不全,则设置响应失败状态并直接返回(转换器的全部响应方法,包括此处的`jc.Fail`, // 只记录响应数据,必定返回 true ,方便直接用`return`退出无返回参数的控制器) if (len(signInfo.Name) == 0 || len(signInfo.Password) == 0) && jc.Fail("name or password is empty", 0) { return } // 以名称从列表取会员信息,若未取到,则自动注册一个(示例代码,为方便用了非线程安全的 map ,请勿在意) member, ok := members[signInfo.Name] if !ok { // Notfound then auto register memberId++ member = signInfo member.Id = memberId member.Role = 1 members[member.Name] = member } if member.Password != signInfo.Password && jc.Fail("password error", 0) { return } // Must be have a session obj after `BeforeController` event, so we no need to check nil // 在`BeforeController`中必定成功创建并绑定了 Session 对象,否则不会调用处理函数,所以此处无需判断可直接推定为 Session 对象指针 se := c.Rp.Session().(*session.ShmSession[Member]) // 将会员信息记录到 Session 中实现登录 if err := se.Login(member, 0); err != nil && jc.Fail(err.Error(), 0) { return } // Must be have a new session id after `UserSession.Login()` // 登录后必定产生一个新的 SID ,设置响应,以便可路由协议自动将其附加到响应头 c.Rp.SetRespSid(se.Id()) // 设置成功响应(登录成功会将会员信息记录到 Session 中,用户通过该 SID 即可取到会员信息) jc.Success(nil) }) // Bind an api wih Path: signer, roles: [1] // 配置一条 TcpRouter 路由规则,绑定取登录者信息 API //(用`Path`函数新建一个`Api`对象,并授权`roles`为`[1]`。若无需自动响应,请追加调用`Unresponsive`方法) flex.TcpRouter.PushApi(router.Path("signer").Append("roles", 1), func(c *flex.Tcp) { // 新建一个 JSON 转换器,该转换器不作请求转换,仅作响应转换 jc := converter.JsonConverterNoParse[*flex.TcpProtocol, Member, Signer](c) sess := c.Rp.Session().(*session.ShmSession[Member]) // Member info can be obtained here, so there is no need to check // 由于`Api`配置了授权角色,且在`BeforeController`中会进行鉴权,未登录或无权的用户都无法进到此,所以必定能取到用户而无需判断 member, _ := sess.User() // Response the member, it can be convert to Signer struct automatically // 设置响应数据。转换器将自动转换 Member 对象为脱敏的 Signer 对象,并自动序列化为 JSON jc.Response(member) }) } func bindClient() { // 从命令行通过 Tcp 客户端登录:go run main.go sign // 配置一条 CliRouter 路由规则,支持`sign`路径路由(你还可以通过 port=$PORT 来指定服务器端口,如`go run main.go sign port=3000`) proto.CliRouter.Push("sign", func(c *proto.Cli) { reader := bufio.NewReader(os.Stdin) signInfo := Member{} fmt.Print("Enter username: ") username, _ := reader.ReadString('\n') signInfo.Name = strings.TrimSpace(username) fmt.Print("Enter password: ") password, _ := reader.ReadString('\n') signInfo.Password = strings.TrimSpace(password) reqBody, err := json.Marshal(signInfo) // Json 化表单并发送登录请求 if err != nil && c.SetError(err) { return } else if resp := request(c, "sign", reqBody, ""); resp != nil { // 若服务端响应了 SID ,则记录到 Cli 可路由上下文,以便响应时输出 c.Rp.SetRespSid(resp.Sid) // 响应登录成功(响应对于 Cli 即为打印到控制台) c.WriteString("Signed in successfully") } }) // 从命令行通过 Tcp 客户端取登录者信息:go run main.go signer $SESSION_ID // (`{sid?}`是一个可选路径变量,但其实是必填的,配为可选是为了方便在控制器中输出具体提示) proto.CliRouter.Push("signer/{sid?}", func(c *proto.Cli) { sid := c.Param("sid") if len(sid) == 0 { panic("Session ID is required") } if resp := request(c, "signer", nil, sid); resp == nil { c.SetError(util.Closed0("Request failed")) } else if resp.Code == 0 { var signer Signer if err := json.Unmarshal(resp.Body, &signer); err != nil && c.SetError(err) { return } else { // Just response the signer info if the session is logged in c.WriteString(fmt.Sprintf("Signer: %v", signer)) } } else { // 若登录失败,则设置错误,Cli 可路由协议会自动响应打印 c.SetError(util.Openly(int(resp.Code), resp.Message)) } }) } // It's a simple example, need to mapping request id and the response callback if the server is async // 这只是一个简单的客户端请求示例,实际使用应创建一个请求 ID 与响应回调的映射,以应对异步编程时可能的错序响应问题 // (在`_examples`下有 js 版请求响应式的 flex-websocket 客户端的封装可供参考,后续 DCE 会提供 GO 版封装) func request(c *proto.Cli, path string, reqBody []byte, sid string) *flex.Package { // ID 传入`-1`将生成自增 ID pkg := flex.NewPackage(path, reqBody, sid, -1) conn, _ := net.Dial("tcp", "127.0.0.1:"+c.Rp.ArgOr("port", "2048")) defer conn.Close() if _, err := conn.Write(pkg.Serialize()); err != nil && c.SetError(err) { return nil } // 从输出流提取字节序并解码为弹性可路由包 resp, err := flex.PackageDeserialize(bufio.NewReader(conn)) if err != nil && c.SetError(err) { return nil } return resp } var memberId uint64 = 0 var members map[string]Member = make(map[string]Member) type Member struct { Id uint64 Role uint16 Name string `json:"name"` Password string `json:"password"` } // 实现`UidGetter`接口,以便在`UserSession`中获取`uid`以作相应绑定 func (m Member) Uid() uint64 { return m.Id } type Signer struct { Name string `json:"name"` } // Member entity converted to transfer object desensitization // 实现`From[S, T any]`接口以便在`JsonConverter`中自动转换`Member`为`Signer`脱敏 func (m Signer) From(member Member) (Signer, error) { m.Name = member.Name return m, nil } 
    6 条回复    2025-01-22 13:47:59 +08:00
    superchijinpeng
        1
    superchijinpeng  
       338 天前
    支持 lb 吗
    zjsxwc
        2
    zjsxwc  
       338 天前
    谁能解释下,这个库的使用目的是什么,使用场景是什么,我土鳖了
    脱敏的登录者 是啥用途?
    YanSeven
        3
    YanSeven  
       338 天前
    mark 一下,不晓得使用场景
    idrunk
        4
    idrunk  
    OP
       337 天前
    @superchijinpeng 不支持,目前只是个路由库,可通过 nginx 等实现。有想过实现一个类库级别的网关,实现 loadbalance 等,短期应该不会做。
    idrunk
        5
    idrunk  
    OP
       337 天前
    @zjsxwc 路由器,http 路由器知道吗,比如将`GET /home`路由到`func home()`,我这个是除了 HTTP 外也能路由其他全部协议,目前内置支持了一些常用的,没内置的可自行实现接口来支持 DCE-GO 来路由。
    “脱敏”是转换器应用的业务场景之一,要将用户信息输出到前台要脱敏吧,就可以用转换器转成一个不带敏感信息的传输对象( Entity to DTO )
    idrunk
        6
    idrunk  
    OP
       337 天前
    @SGL 当你想基于 Websocket 编程时可能需要,http 路由器有很多,但 websocket 等基本没有,用 DCE 就可以像 HTTP 编程一样编写 Websocket 接口,可以用同一套鉴权类库来鉴权等。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1532 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 16:27 PVG 00:27 LAX 08:27 JFK 11:27
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86