用 3 个端口也可以实现类似效果,此篇献给追求完美的你。。。
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()
}
// TODO 晚上
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)
}
}
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"
)
1
scukmh 2019-10-17 12:45:07 +08:00
emmmm , 这不是在 Go 高级编程里面有说嘛?
|
2
sunny352787 2019-10-17 12:47:32 +08:00
我?我还以为什么黑科技呢...你这数据基本操作吧?
|
3
xmge 2019-10-17 12:48:37 +08:00
额。。。。。。这个不本来就是这样吗
|
4
guonaihong OP @scukmh 有 http & grpc 的?
|
5
guonaihong OP @sunny352787 http & grpc 是你要的黑科技。
|
6
guonaihong OP @xmge 还没写完。。。
|
7
reus 2019-10-17 13:27:58 +08:00
|
8
guonaihong OP @reus hijacker 不适用 http2。
|
9
reus 2019-10-17 13:40:30 +08:00
@guonaihong 实现一个 net.Listener,Accept 返回 hijack 的 net.Conn,然后将这个 listener 传给 http.Server.Serve 承载 grpc。
|
10
dongxiaozhuo 2019-10-17 13:48:17 +08:00 via iPhone
一直不太理解,为什么会有把 gRPC 和 HTTP 刚才一个端口下的诉求…
|
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 }), ) } ``` 想看看您能搞出什么花来 |
12
guonaihong OP |
13
scukmh 2019-10-17 13:50:52 +08:00
v2ex 连 markdown 都不支持了嘛?
|
14
guonaihong OP @scukmh 谢了,我试下。
|
15
reus 2019-10-17 13:52:49 +08:00
@dongxiaozhuo 减少一个配置也是好的
|
16
scukmh 2019-10-17 13:58:13 +08:00
|
17
dongxiaozhuo 2019-10-17 14:17:34 +08:00
@reus 端口数量并不是一个稀缺资源,增加一个配置并不会大量增加程序的复杂性。但是如果 gRPC 和 HTTP 本身在程序有不同的定位,仅仅是为了节省一个端口 /配置项,将两个不应该融合的东西融合到一起,看起来会得不偿失;但是如果 gRPC 和 HTTP 在程序中的定位是一样的,为什么不直接使用 gRPC 和 HTTP 中一个,而要将两个融合到一起?
如果可以提供一个具体的落地场景讨论,那再好不过了。 |
18
qwerthhusn 2019-10-17 14:38:23 +08:00
是三种不同协议,但是 grpc 和 ws 都是先建立在 http 上的
|
19
Suvigotimor 2019-10-17 15:26:38 +08:00
巧了,我们还真有 grpc 和 http 在同一端口上做区分的需求.....RUA
|
20
sunny352787 2019-10-17 15:46:32 +08:00
@guonaihong 这是 golang 的技巧?这是 cmux 的使用...我以为你好歹会补一个 cmux 的实现原理
|
21
suriv520 2019-10-17 15:54:12 +08:00 1
谢谢楼主分享。
个人专的领域不同,楼上同学可以切磋实现细节探讨更多可能,干嘛冷嘲热讽。 |
22
guonaihong OP @sunny352787 以后再分享吧,最近打算玩下 rust,挺花时间的。
|
23
sunny352787 2019-10-17 21:17:37 +08:00
@suriv520 只是看标题党难受而已,我以为会描述一下原理或者最起码自己手撸代码让大家看看是怎么做的,这直接调用 cmux 库,那你直接说发现一个库可以怎么怎么样不就行了?
|
24
guonaihong OP @sunny352787 我想分享的是在工作中可以使用的套路。而不是玩具代码做法。如果一个方式自己都半生不熟。误导别人就不好了。
|
25
sunny352787 2019-10-17 22:17:54 +08:00 via Android
@guonaihong 分享没有任何问题,但标题这样写容易让人误会这几个东西之间的关系,毕竟小白还是挺多的,咱们写的多了自然知道 websocket 和 http 是什么,底层都是 tcp,但基础不扎实的就容易迷糊了。
而且这边简单点讲一下通过 tcp 路由的方式区分普通 tcp 流量和 http 流量就好了,上来先写了个 http 路由区分普通 http 流量和 websocket 这肯定被人喷啊,你看前几条回复的不都是觉得这分享的内容不靠谱?中间你还加了个和本文没啥关系的 GitHub 库难免让人觉得你是为了推广加星才来这么个标题党忽悠小白。这可不是正常技术分享应该的做的,反倒是那帮自媒体推广的套路。 |
26
guonaihong OP @sunny352787 哈哈。。sunny 兄让我不知道说什么好。难道一篇让人开箱即用的技巧。非要扯得高深点才好。非要告诉别人 http 除了是基于 tcp 的。http2 加入 tcp 多路复用,优化 http1.1 pipeline 的问题。http3 将要 使用 udp,解决 tcp 协议栈在内核开销大的问题。这种一堆细节,除了抬高自己,对读者没有任何好处。我喜欢站在大众读者角度,讲些开箱即用的东东。。。尽理追求复杂的事情说简单,简单的东西直接使用。。。 如果你觉得不舒服,我下一篇尽量用更平谈的标题。我无意在这种小事上继续讨论,这种非技术的讨论实在没意思。你下个回答,我也不回答了(特此说明)。
|
27
sunny352787 2019-10-17 23:31:00 +08:00 via Android
@guonaihong 技术分享也是讲方式方法的,也很期待大家能把自己的东西给大家讲明白。没想扯得多高深,但故弄玄虚就没劲了。不回复无所谓啊,也很期待你接下来的分享。
忙完这段我也发点东西出来,欢迎指正。 |
28
sip2u 2019-10-18 10:32:46 +08:00
感谢 lz 分享
|
29
Daath 2019-10-18 17:35:22 +08:00
感谢分享,之前有类似的需求,只不过我直接用 nginx 来做这一层转发,
|
30
guonaihong OP @Daath 厉害厉害,可否分享下 nginx 的做法。。。
|
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; .... } } ``` |
32
ikaros 2023-05-18 15:51:48 +08:00
@dongxiaozhuo cloudflare 要求代理的 grpc 必须走 443 端口,我的服务同时 host 了 https 服务, 现在就有需求了
|