总的来说,经典的Web框架会将每个HTTP请求回复抽象为两部分: Request, Response。
其中Request包括Scheme、Method、HTTP Version、Url、Headers、Body等;Response包括Status、Headers、Body等等。
像Cookie、User-Agent、Host、Query/Post参数等则更加细节,是对Url、Headers、Body的进一步处理。
但有一个问题,这些框架的逻辑,每一次请求更加像是单向的数据流。Client每一次将请求发送(至少是除了Body以外的部分)给Server端完毕后,Server端再处理给出回应。
像特殊情况,上传超大文件这种,一般都是要自己实现的,主要是针对Request的Body进行一些解析实现。
不过虽然开始处理时没有收到完整的数据,但是Body的大小其实在Content-length里是已知的。Server也是在保存文件成功或者失败以后再给Client答复。
Request的Body大小一般来说是已知的,写在Content-length里,当然也可以未知,例如Transfer-Encoding: Chunked通过指示每一份内容来做分割和控制,不过极其少见,估计也没多少场景中会用到这个。
Response的Body大小一般来说也是已知的,写在Content-length里,当然也可以未知,通过Transfer-Encoding: Chunked一段一段的写。
(当然http2里面这个被禁止使用了,详见MDN)
是的,理论上你可以通过Chunked+ Chunked来实现一个双向数据流,至少本地可以跑通。但是会有缺陷,就像TCP的粘包一样,中间层可能会缓存Chunked0、Chunked1、…ChunkedN甚至等到结尾再传给你。
毕竟逻辑上的普遍情况是Server端收到完整Request后,再返回Response,之后当次HTTP请求算是跑完了一整个流程。
我们考虑以下场景:
- Client发送 cChunked0,cChunked1
 - Server接收到 cChunked0,cChunked1,处理后返回 sChunked0、sChunked1
 - Client接收到 sChunked0、sChunked1,处理后再发送 cChunked2,cChunked结束边界
 - Server接收到 cChunked2,cChunked结束边界, 处理后返回sChunked结束边界
 - HTTP请求结束
 
实际上,可能在上述第二步和第三步就会卡住,无法实现。
所以,讲这么多最简单的还是Websocket。 通过Client通过HTTP GET附带Upgrade请求, Server 回复 101消息表示请求升级。接下来则完全不必按照Websocket来了,因为中间层不会浪费资源去解析或截留缓存接下来的内容。
前言
编程编程,不外乎数据的存储、传输、处理利用以及呈现。翻译一下,就是本地缓存/持久化存储(例如数据库)、网络通信、大数据/机器学习/深度学习、人机交互(GUI、CLI)等等。
每当学一门语言或工具框架,我总喜欢从通信这个角度,通过实现自定义的双向http流(Websocket·伪)客户端以及服务端,来熟悉了解一些较为基础底层的东西。
因为不成熟,基本上是从最基本的东西开始实现,然后根据需要慢慢添加扩展;没有站在一个较高的层面上去规划设计,导致耦合较深。通俗点讲,这个东西被设计用来做这个,以后就只能做这个了,再多一点的话拓展实现就会比较麻烦,再多两点就要命了。
而在玩具已经实现以后,就没有动力去花功夫去重新再实现一遍了。
于是我想,有没有可能在现有的框架下直接自定义DIY呢?
想到了,那就去做!
服务端实现
尝试直接操作Request.Body
下面是一个常见的HTTP Handler,我第一想法是操作像大文件上传那样操作Request.Body。
但是很可惜,在HTTP请求方法为GET时,读取Request的Body会返回EOF异常。
func HandleStream(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("xx", "this is a echo stream")
    w.Header().Add("Sec-WebSocket-Accept", randomString1)
    w.Header().Add("Connection", "Upgrade")
    w.Header().Add("Upgrade", "websocket")
    w.WriteHeader(101)
    // io.Copy(w, r.Body)
    buffer := make([]byte, 1024)
    for {
        len, err := r.Body.Read(buffer)
        if len > 0 {
            w.Write(buffer[:len])
        }
        if err != nil {
            break
        }
    }
}
劫持ResponseWriter
实现Websocket·伪的最好方式是参考Websocket·真gorilla/websocket
我们可以看一个服务端的简单例子
var upgrader = websocket.Upgrader{} // use default options
func echo(w http.ResponseWriter, r *http.Request) {
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer c.Close()
    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        err = c.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}
h, ok := w.(http.Hijacker)
if !ok {
    return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
}
var brw *bufio.ReadWriter
netConn, brw, err := h.Hijack()
if err != nil {
    return u.returnError(w, r, http.StatusInternalServerError, err.Error())
}
if brw.Reader.Buffered() > 0 {
    netConn.Close()
    return nil, errors.New("websocket: client sent data before handshake is complete")
}
// 这里netConn就可以放心使用了
我们可以将http.ResponseWriter 强转为 http.Hijacker并从中获取net.Conn,这是一个可以Read、Write、Close的接口,接下来你懂的。。。
在Gin框架中实现
http.Handler很容易转成gin的Handler
// 假设实现了func Handler(w http.ResponseWriter, r *http.Request)
func HandleTunnel(c *gin.Context) {
    Handler(c.Writer, c.Request)
    // c.Abort()
}
客户端实现
客户端的难点在于路由如何拦截处理到HTTPS代理的CONNECT方法
在http server中实现
http.HandleFunc("/404", demo.Handle404)
http.HandleFunc(pattern, handler)
查看源码,发现CONNECT方法的请求url path为空字符串,无论怎么添加pattern也不匹配,会由默认的NotFound Handler进行处理
一个解决方案是自定义一个ServerMux作为root Handler
mux := router.InitRouters()
http.ListenAndServe(":8080", mux)
func InitRouters() http.Handler {
    // 假设实现了func HandleConnect(w http.ResponseWriter, r *http.Request)
    var handler = http.HandlerFunc(HandleConnect)
    mux := NewMux(&handler, http.DefaultServeMux)
    // 其它
    mux.HandleFunc("/404", demo.Handle404)
    return mux
}
type Mux struct {
    ServeMux                 *http.ServeMux
    ConnectMethodHandlerFunc *http.HandlerFunc
}
func NewMux(ConnectMethodHandlerFunc *http.HandlerFunc, ServeMux *http.ServeMux) *Mux {
    return &Mux{
        ServeMux,
        ConnectMethodHandlerFunc,
    }
}
func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method == "CONNECT" && mux.ConnectMethodHandlerFunc != nil {
        (*mux.ConnectMethodHandlerFunc)(w, r)
    } else {
        mux.ServeMux.ServeHTTP(w, r)
    }
}
func (mux *Mux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
    mux.ServeMux.HandleFunc(pattern, handler)
}
func (mux *Mux) Handle(pattern string, handler http.Handler) {
    mux.ServeMux.Handle(pattern, handler)
}
在Gin框架中实现
// 假设实现了func HandleConnect(w http.ResponseWriter, r *http.Request)
// 初始化路由的时候 gin.NoRoute(HandleNoroute, ...)
func HandleNoroute(c *gin.Context) {
    if c.Request.Method == "CONNECT" {
        HandleConnect(c.Writer, c.Request)
        // c.Abort()
    }
}
关于HTTP代理
基础http server在自定义的root handler对Host进行判断
Gin框架在较上层添加中间件对Host进行判断拦截即可
其它
需要注意的是,以上和Websocket一样,仅适用于HTTP 1.1。  
最好再加上一个判断,当遇到HTTP2请求时直接抛出异常。