V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
guonaihong
V2EX  ›  程序员

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

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

    用 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
    dongxiaozhuo  
       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 分享
    Daath
        29
    Daath  
       2019-10-18 17:35:22 +08:00
    感谢分享,之前有类似的需求,只不过我直接用 nginx 来做这一层转发,
    guonaihong
        30
    guonaihong  
    OP
       2019-10-19 19:04:22 +08:00
    @Daath 厉害厉害,可否分享下 nginx 的做法。。。
    Daath
        31
    Daath  
       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   ·   实用小工具   ·   2485 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 15:32 · PVG 23:32 · LAX 07:32 · JFK 10:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.