go websocket 问题(Hijacker)
在写websocket包的时候发现一个比较有趣问题!go 使用 TLS验证的时候发现 websocket 使用不了。深入了解发现其中奥秘:go 在执行 TLS 验证时候默认是使用 http2 协议进行的!但是 websocket 是无法支持 http2 协议(暂时),导致这个问题所在的原因!
解决方法:
- 不开启 http/2 协议
- 使用 Hijacker
第一种:不开始 http/2 协议
使用空 map 来使用 http1.x协议
server := http.Server {
Addr : ":8080",
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // 这
}
log.Fatal(server.ListenAndServeTLS("./coms.crt", "./coms.key"))
因为默认支持h2,所有我们把降到http1.x。
第二种:使用 Hijack
// w 是 http.ResponseWriter
hj,ok := w.(http.Hijacker)
if !ok {
return
}
conn , _ , err := hj.Hijack()
if err != nil {
return
}
使用 http.Hijacker 对其进行劫持 net.Conn , 让程序员自己控制使用!其实这个时候已经脱离 http 协议规范!
//hijacker请求
http.HandleFunc("/hijacker", func(w http.ResponseWriter, r *http.Request) {
hj, _ := w.(http.Hijacker)
conn, buf, err := hj.Hijack()
if err != nil {
return
}
defer conn.Close()
buf.WriteString("coms is good boy\r\n")
buf.Flush()
})
//正常的http请求
http.HandleFunc("/http", func(writer http.ResponseWriter, request *http.Request) {
io.WriteString(w,"coms is good boy")
})
$ curl "http://127.0.0.1:9001/hijacker" -i
coms is good boy
$ curl "http://127.0.0.1:9001/http" -i
HTTP/1.1 200 OK
Date: Mon, 30 September 2020 01:11:52 GMT
Content-Length: 16
Content-Type: text/plain; charset=utf-8
coms is good boy
发现使用 Hijacker 会脱离 http 协议范畴,可以解决h2与websocket的相关问题!
使用Hijacker实现 websocket 实现连接测试
协议: 状态码:101 , Upgrade , Connection, Sec-WebSocket-Accept
package main
import (
"crypto/sha1"
"encoding/base64"
"fmt"
"net/http"
)
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func computeAcceptKey(challengeKey string) string {
h := sha1.New()
h.Write([]byte(challengeKey))
h.Write(keyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func echo(w http.ResponseWriter,r *http.Request) {
hj,ok := w.(http.Hijacker)
if !ok {
return
}
conn , _ , err := hj.Hijack()
if err != nil {
return
}
challengeKey := r.Header.Get("Sec-Websocket-Key")
var p []byte
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...)
p = append(p, "\r\n"...)
if _, err = conn.Write(p); err != nil {
fmt.Println(err)
conn.Close()
}
}
func main() {
http.HandleFunc("/",echo)
http.ListenAndServe(":8082",nil)
}
实现最简单 websocket 连接!
websocket连接已经建立,有些 sec 是默认添加上去!
对 Hijack 源码分析
- 如何脱离协议范畴
- 如何劫持连接
根据 http.Listen 进行向下追踪
在 func (srv *Server) Serve(l net.Listener) error 发现原由
func (srv *Server) Serve(l net.Listener) error {
...
c.setState(c.rwc, StateNew)
go c.serve(connCtx)
}
对 c.setState 进入分析
func (c *conn) setState(nc net.Conn, state ConnState) {
srv := c.server
switch state {
case StateNew:
srv.trackConn(c, true)
case StateHijacked, StateClosed:
srv.trackConn(c, false)
}
if state > 0xff || state < 0 {
panic("internal error")
}
packedState := uint64(time.Now().Unix()<<8) | uint64(state)
atomic.StoreUint64(&c.curState.atomic, packedState)
if hook := srv.ConnState; hook != nil {
hook(nc, state)
}
}
发现当 case 是 Hijack【StateHijacked, StateClosed】状态时候执行 trackConn
func (s *Server) trackConn(c *conn, add bool) {
s.mu.Lock()
defer s.mu.Unlock()
if s.activeConn == nil {
s.activeConn = make(map[*conn]struct{})
}
if add {
s.activeConn[c] = struct{}{}
} else {
delete(s.activeConn, c)
}
}
add 为 false ,对其 delete(s.activeConn,c) 。Hijacker是满足相关条件
所以在 go c.serve(connCtx) 里面不在给 Hijacker 进行操作!导致 http header 无法设置!最后交给程序员自己操作!
使用场景
RPC使用
go中自带的rpc可以直接复用http server处理请求的那一套流程去创建连接,连接创建完毕后再使用Hijack方法拿到连接。
func (server *server) serveset(w http.responsewriter, req *http.request) {
if req.method != "connect" {
w.header().set("content-type", "text/plain; charset=utf-8")
w.writeheader(http.statusmethodnotallowed)
io.writestring(w, "405 must connect\n")
return
}
conn, _, err := w.(http.hijacker).hijack()
if err != nil {
log.print("rpc hijacking ", req.remoteaddr, ": ", err.error())
return
}
io.writestring(conn, "http/1.0 "+connected+"\r\n")
server.serveconn(conn)
}
还有在上述的 websocket 实用
注: github.com/gorilla/websocket
这包就接入 HIjacker ,直接使用现成包就香~~~~~