golang: 技巧---同一个端口监听 http & grpc & websocket 三种不同协议 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
guonaihong
V2EX    程序员

golang: 技巧---同一个端口监听 http & grpc & websocket 三种不同协议

  •  
  •   guonaihong
    guonaihong 2019-10-17 12:31:31 +08:00 9116 次点击
    这是一个创建于 2235 天前的主题,其中的信息可能已经有所发展或是发生改变。

    用 3 个端口也可以实现类似效果,此篇献给追求完美的你。。。

    http & websocket

    websocket 用的 http 协议握手,可以通过不同路由区分出 http 还是 websocket。

    package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "io" ) var upgrader = websocket.Upgrader{} func main() { r := gin.Default() // websocket echo r.Any("/websocket", func(c *gin.Context) { r := c.Request w := c.Writer conn, err := upgrader.Upgrade(w, r, nil) if err != nil { fmt.Printf("err = %s\n", err) return } defer func() { // 发送 websocket 结束包 conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // 真正关闭 conn conn.Close() }() // 读取一个包 mt, d, err := conn.ReadMessage() if err != nil { fmt.Printf("read fail = %v\n", err) return } fmt.Printf("data:%s\n", d) // 写入一个包 err = conn.WriteMessage(mt, d) if err != nil { fmt.Printf("write fail = %v\n", err) return } }) // http echo r.GET("/http", func(c *gin.Context) { io.Copy(c.Writer, c.Request.Body) }) r.Run() } 

    http & grpc

    // TODO 晚上

    第 1 条附言    2019-10-17 13:49:24 +08:00

    同一端口支持http & websocket & grpc

    package main var upgrader = websocket.Upgrader{} func main() { l, err := net.Listen("tcp", ":23456") if err != nil { log.Fatal(err) } m := cmux.New(l) httpl := m.Match(cmux.HTTP1Fast()) grpcl := m.Match(cmux.Any()) go serveGRPC(grpcl) go serveHTTPAndWs(httpl) if err := m.Serve(); !strings.Contains(err.Error(), "use of closed network connection") { panic(err) } } type grpcServer struct{} func (s *grpcServer) SayHello(ctx context.Context, in *grpchello.HelloRequest) ( *grpchello.HelloReply, error) { fmt.Printf("request:%v\n", in) return &grpchello.HelloReply{Message: "Hello " + in.Name + " from cmux"}, nil } func serveGRPC(l net.Listener) { grpcs := grpc.NewServer() grpchello.RegisterGreeterServer(grpcs, &grpcServer{}) if err := grpcs.Serve(l); err != cmux.ErrListenerClosed { panic(err) } } func serveHTTPAndWs(l net.Listener) { r := gin.Default() // websocket echo r.Any("/websocket", func(c *gin.Context) { r := c.Request w := c.Writer conn, err := upgrader.Upgrade(w, r, nil) if err != nil { fmt.Printf("err = %s\n", err) return } defer func() { // 发送websocket结束包 conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // 真正关闭conn conn.Close() }() // 读取一个包 mt, d, err := conn.ReadMessage() if err != nil { fmt.Printf("read fail = %v\n", err) return } fmt.Printf("data:%s\n", d) // 写入一个包 err = conn.WriteMessage(mt, d) if err != nil { fmt.Printf("write fail = %v\n", err) return } }) // http echo r.GET("/http", func(c *gin.Context) { io.Copy(c.Writer, c.Request.Body) }) s := &http.Server{ Handler: r, } if err := s.Serve(l); err != cmux.ErrListenerClosed { panic(err) } } 

    总结

    • 适用了cmux实现效果,需要注意上面的m.Match顺序不要修改

    github

    https://github.com/guonaihong/gout

    第 2 条附言    2019-10-17 13:49:48 +08:00

    补下依赖

    import ( "fmt" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/soheilhy/cmux" "log" "net/http" "strings" "google.golang.org/grpc" "context" "net" grpchello "google.golang.org/grpc/examples/helloworld/helloworld" "io" ) 
    32 条回复    2023-05-18 15:51:48 +08:00
    scukmh
        1
    scukmh  
       2019-10-17 12:45:07 +08:00
    emmmm , 这不是在 Go 高级编程里面有说嘛?
    sunny352787
        2
    sunny352787  
       2019-10-17 12:47:32 +08:00
    我?我还以为什么黑科技呢...你这数据基本操作吧?
    xmge
        3
    xmge  
       2019-10-17 12:48:37 +08:00
    额。。。。。。这个不本来就是这样吗
    guonaihong
        4
    guonaihong  
    OP
       2019-10-17 12:52:21 +08:00
    @scukmh 有 http & grpc 的?
    guonaihong
        5
    guonaihong  
    OP
       2019-10-17 12:53:09 +08:00
    @sunny352787 http & grpc 是你要的黑科技。
    guonaihong
        6
    guonaihong  
    OP
       2019-10-17 12:53:45 +08:00
    @xmge 还没写完。。。
    reus
        7
    reus  
       2019-10-17 13:27:58 +08:00
    https://godoc.org/net/http#Hijacker

    有啥黑科技的,hijack 之后就是个 net.Conn 了,干什么都随你了。
    guonaihong
        8
    guonaihong  
    OP
       2019-10-17 13:29:03 +08:00
    @reus hijacker 不适用 http2。
    reus
        9
    reus  
       2019-10-17 13:40:30 +08:00
    @guonaihong 实现一个 net.Listener,Accept 返回 hijack 的 net.Conn,然后将这个 listener 传给 http.Server.Serve 承载 grpc。
    dongxiaozhuo
        10
    dongxiaozhuo  
       2019-10-17 13:48:17 +08:00 via iPhone
    一直不太理解,为什么会有把 gRPC 和 HTTP 刚才一个端口下的诉求…
    scukmh
        11
    scukmh  
       2019-10-17 13:50:07 +08:00   1
    ```go
    func main() {
    ...

    http.ListenAndServeTLS(port, "server.crt", "server.key",
    http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.ProtoMajor != 2 {
    mux.ServeHTTP(w, r)
    return
    }
    if strings.Contains(
    r.Header.Get("Content-Type"), "application/grpc",
    ) {
    grpcServer.ServeHTTP(w, r) // gRPC Server
    return
    }

    mux.ServeHTTP(w, r)
    return
    }),
    )
    }
    ```
    想看看您能搞出什么花来
    guonaihong
        12
    guonaihong  
    OP
       2019-10-17 13:50:30 +08:00
    @scukmh @sunny352787 @xmge @reus 好了。
    scukmh
        13
    scukmh  
       2019-10-17 13:50:52 +08:00
    v2ex 连 markdown 都不支持了嘛?
    guonaihong
        14
    guonaihong  
    OP
       2019-10-17 13:52:38 +08:00
    @scukmh 谢了,我试下。
    reus
        15
    reus  
       2019-10-17 13:52:49 +08:00
    @dongxiaozhuo 减少一个配置也是好的
    dongxiaozhuo
        17
    dongxiaohuo  
       2019-10-17 14:17:34 +08:00
    @reus 端口数量并不是一个稀缺资源,增加一个配置并不会大量增加程序的复杂性。但是如果 gRPC 和 HTTP 本身在程序有不同的定位,仅仅是为了节省一个端口 /配置项,将两个不应该融合的东西融合到一起,看起来会得不偿失;但是如果 gRPC 和 HTTP 在程序中的定位是一样的,为什么不直接使用 gRPC 和 HTTP 中一个,而要将两个融合到一起?

    如果可以提供一个具体的落地场景讨论,那再好不过了。
    qwerthhusn
        18
    qwerthhusn  
       2019-10-17 14:38:23 +08:00
    是三种不同协议,但是 grpc 和 ws 都是先建立在 http 上的
    Suvigotimor
        19
    Suvigotimor  
       2019-10-17 15:26:38 +08:00
    巧了,我们还真有 grpc 和 http 在同一端口上做区分的需求.....RUA
    sunny352787
        20
    sunny352787  
       2019-10-17 15:46:32 +08:00
    @guonaihong 这是 golang 的技巧?这是 cmux 的使用...我以为你好歹会补一个 cmux 的实现原理
    suriv520
        21
    suriv520  
       2019-10-17 15:54:12 +08:00   1
    谢谢楼主分享。
    个人专的领域不同,楼上同学可以切磋实现细节探讨更多可能,干嘛冷嘲热讽。
    guonaihong
        22
    guonaihong  
    OP
       2019-10-17 21:00:03 +08:00
    @sunny352787 以后再分享吧,最近打算玩下 rust,挺花时间的。
    sunny352787
        23
    sunny352787  
       2019-10-17 21:17:37 +08:00
    @suriv520 只是看标题党难受而已,我以为会描述一下原理或者最起码自己手撸代码让大家看看是怎么做的,这直接调用 cmux 库,那你直接说发现一个库可以怎么怎么样不就行了?
    guonaihong
        24
    guonaihong  
    OP
       2019-10-17 21:34:25 +08:00
    @sunny352787 我想分享的是在工作中可以使用的套路。而不是玩具代码做法。如果一个方式自己都半生不熟。误导别人就不好了。
    sunny352787
        25
    sunny352787  
       2019-10-17 22:17:54 +08:00 via Android
    @guonaihong 分享没有任何问题,但标题这样写容易让人误会这几个东西之间的关系,毕竟小白还是挺多的,咱们写的多了自然知道 websocket 和 http 是什么,底层都是 tcp,但基础不扎实的就容易迷糊了。

    而且这边简单点讲一下通过 tcp 路由的方式区分普通 tcp 流量和 http 流量就好了,上来先写了个 http 路由区分普通 http 流量和 websocket 这肯定被人喷啊,你看前几条回复的不都是觉得这分享的内容不靠谱?中间你还加了个和本文没啥关系的 GitHub 库难免让人觉得你是为了推广加星才来这么个标题党忽悠小白。这可不是正常技术分享应该的做的,反倒是那帮自媒体推广的套路。
    guonaihong
        26
    guonaihong  
    OP
       2019-10-17 22:56:02 +08:00
    @sunny352787 哈哈。。sunny 兄让我不知道说什么好。难道一篇让人开箱即用的技巧。非要扯得高深点才好。非要告诉别人 http 除了是基于 tcp 的。http2 加入 tcp 多路复用,优化 http1.1 pipeline 的问题。http3 将要 使用 udp,解决 tcp 协议栈在内核开销大的问题。这种一堆细节,除了抬高自己,对读者没有任何好处。我喜欢站在大众读者角度,讲些开箱即用的东东。。。尽理追求复杂的事情说简单,简单的东西直接使用。。。 如果你觉得不舒服,我下一篇尽量用更平谈的标题。我无意在这种小事上继续讨论,这种非技术的讨论实在没意思。你下个回答,我也不回答了(特此说明)。
    sunny352787
        27
    sunny352787  
       2019-10-17 23:31:00 +08:00 via Android
    @guonaihong 技术分享也是讲方式方法的,也很期待大家能把自己的东西给大家讲明白。没想扯得多高深,但故弄玄虚就没劲了。不回复无所谓啊,也很期待你接下来的分享。

    忙完这段我也发点东西出来,欢迎指正。
    sip2u
        28
    sip2u  
       2019-10-18 10:32:46 +08:00
    感谢 lz 分享
    TheGonG
        29
    TheGonG  
       2019-10-18 17:35:22 +08:00
    感谢分享,之前有类似的需求,只不过我直接用 nginx 来做这一层转发,
    guonaihong
        30
    guonaihong  
    OP
       2019-10-19 19:04:22 +08:00
    @Daath 厉害厉害,可否分享下 nginx 的做法。。。
    TheGonG
        31
    TheGonG  
       2019-10-20 13:28:15 +08:00
    * 思路是差不多的,都是基于 url 的 path 来重定向上游服务
    * 不过好像 nginx 还没支持在 http1.x 上识别 http2.0 的样子。我们就还是分了两个端口。这里跟你像把所有协议都 all in 想法不太一样。
    * 大概这么配置的

    ```
    ssl_certificate /opt/ssl/nginx-selfsigned.crt;
    ssl_certificate_key /opt/ssl/nginx-selfsigned.key;

    server {
    listen 80 ssl;

    location /http/ {
    proxy_pass http://upstream-address:8001/;
    ...

    }

    location /websocket/ {
    proxy_pass http://upstream-address:8002/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    ...

    }
    }

    server {
    listen 81 ssl http2;

    location /testd.HelloWorldSrv {
    grpc_pass grpc://upstream-address:8003;
    ....
    }

    location /otherd.HelloWorldSrv {
    grpc_pass grpc://upstream-address:8004;
    ....
    }
    }


    ```
    ikaros
        32
    ikaros  
       2023-05-18 15:51:48 +08:00
    @dongxiaozhuo cloudflare 要求代理的 grpc 必须走 443 端口,我的服务同时 host 了 https 服务, 现在就有需求了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2462 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 04:30 PVG 12:30 LAX 20:30 JFK 23:30
    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