golang net/http包 http請求的位元組碼讀取與解析。
c.readRequest(ctx)讀請求
先配置Header最長讀取時間、req最長讀取時間、req最大讀取長度預設6M。
RFC7230禁止\r\n引數,Url中只允許包含英文字母(a-zA-Z)、數字(0-9)、-_.~4個特殊字元以及所有保留字元。但go net/http包放寬了這個要求。
讀出Request
先構建newTextprotoReader,由於緩衝區是物件複用的,用完後要defer put。共完以以下解析任務:
- 協議第一行
- URL
- Header
- Body
TextprotoReader資料結構,將位元組碼Reader轉成文字Reader。
type Reader struct R*bufio.Reader dot *dotReader buf []byte // a re-usable buffer for readContinuedLineSlice } type dotReader struct { r*Reader state int } //struct之間相互引用
第一步,從第一行解析出method uri prototype。
第二步解析URL。url.URL資料結構:
type URL struct { Schemestring Opaquestring User*Userinfo Hoststring Pathstring RawPathstring ForceQuery bool RawQuerystring Fragmentstring }
解析Scheme,協議字首(小寫)。有查詢引數?,則配置url.ForceQuery url.RawQuery。有認證資訊///...//,則解析url.User url.Host。最後配置url.Path和url.RawPath,如果Path==RawPath,則RawPath=""。
第三步解析MIMEHeader。
第四步readTransfer。重新配置如下引數:RequestMethod ProtoMajor ProtoMinor Header Trailer ContentLength Close。對於Body,如果encodings支援chunked,讀取流用chunkedReader包裹。預設情況用LimitedReader,無body賦空的struct{}。
Request檢查並補全
以下情況返回非空err,示得到正確的請求:
- 請求太大,超過了readLimitSize。
- 不支援的0.x和2+的HTTP協議版本。
- 請求Header中沒有Host、超過1個或不合法。合法字符集為httpguts.validHostByte。
- Header的Key或Value不合法。Key的要求必須一httpguts.isTokenTable字符集,Value的要求不可以是控制字符集。
最後配置req.ctx req.RemoteAddr req.TLS body.doEarlyClose = true。
構建Response幷包裝chunkWriter
構建Response:
w = &response{ conn:c, cancelCtx:cancelCtx, req:req, reqBody:req.Body, handlerHeader: make(Header), contentLength: -1, closeNotifyCh: make(chan bool, 1), wants10KeepAlive: req.wantsHttp10KeepAlive(), wantsClose:req.wantsClose(), }
其中closeNotifyCh必須在構建時初始化,沒有content所以先置contentLength為-1。
配置w.cw並被w.w包裹。w.cw緩衝預設大小2M。
請求未正確獲取的情況
獲取Request可能出現如下錯誤:
- 請求超過readLimitSize,返回431錯誤,並重新整理conn的讀寫緩衝。
- basRequest錯誤,返回400錯誤。包括網路原因導致的讀取錯誤。
w.finishRequest()
先上響應資料結構:
type response struct { conn*conn req*Request reqBodyio.ReadCloser cancelCtxcontext.CancelFunc wroteHeaderbool wroteContinuebool wants10KeepAlive bool wantsClosebool w*bufio.Writer cw chunkWriter handlerHeader Header calledHeaderbool writtenint64 contentLength int64 statusint closeAfterReply bool requestBodyLimitHit bool trailers []string handlerDone atomicBool dateBuf[len(TimeFormat)]byte clenBuf[10]byte statusBuf [3]byte closeNotifyChchan bool didCloseNotify int32 }
response欄位可以分類為:大物件、緩衝、KV對或bool型的狀態引數。
大物件有:
- conn為TCP連線指標
- req為對應的請求指標
- reqBody為req.Body
- cancelCtx為連線上下文context
狀態欄位:
- requestBodyLimitHit,讀req.Body使用了maxBytesReader保護機制,當請求資訊過大達到上限,就會在這個引數上反映。
-
4個Header引數:
- wroteHeader響應Header寫入完成
- wroteContinue響應加入了100 Continue
- wants10KeepAlive為Connection "keep-alive"連線重用引數
- wantsClose為Connection "close"連線重用引數
- 3個數據: dateBuf clenBuf statusBuf對應Date Content-Lengthstatus cod
- 3個Body相關引數:contentLength written status三個欄位用來校驗響應內容是否寫入完整,如果未寫完則不對此連線進行復用。定位到上一篇文章serve()方法中有提到。
- trailers:指寫完響應之後,還需要向TCP連線寫入的資料
- handlerDone 表示handler是否存在
- handlerHeader calledHeader儲存Header
緩衝區欄位
chunkWriter資料結構:
type chunkWriter struct { res *response header Header wroteHeader bool chunking bool }
chunkWriter包裹了Response,功能之一是完成Header設定,包括Content-TypeContent-Length chunk-header。bufio.Writer是chunkWriter是緩衝包裹。
邏輯流
共完成三層緩衝寫入:resp.w->resp.cw->conn.bufw
handler將響應寫入到response.w。
呼叫w.w.Flush()將w寫入到cw,注意到Flush()操作,如果未刷空快取並報錯,觸發拷貝操作。報錯不會退回已寫出的資料。
copy(b.buf[0:b.n-n], b.buf[n:b.n]
進而呼叫cw.Write(),根據cw.chunking引數。
- 使能,則全部資料寫入到TCP緩衝。
- 否則返回實際向TCP緩衝的寫入量。
putBufioWriter(w.w)清空resp.w緩衝,如果池化放回sync.pool。
根據chunkWriter的定義,w.cw.close()負責cw的結束工作:寫入換行符和resp.trailers資料。
最後重新整理TCP緩衝w.conn.bufw.Flush(),完成響應包傳送。並正確關閉request。