总的来说,经典的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请求时直接抛出异常。