這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近在用 Go 做一個小型的 gateway 服務。PHP 請求 Go 的 tcp server,然後 Go 根據命令參數開啟多個 goroutine 去調度 php-fpm 執行不同的指令碼並組合結果返回。 想來只是利用 goroutine 的便利並發執行邏輯,如此簡單直接。
不過在測試的時候 PHP 發送 socket 的 json 資料發生了明確的截斷,後來發現是我煞筆的被 bufio.Reader 坑了,真是無言以對。
問題重現
因為是長串連,PHP 每次發一段 json ,都會加上分行符號\n分割。我就很理所當然的用起了*bufio.Reader.ReadLine()。就像下面的代碼:
func handleConn(conn net.Conn) { reader := bufio.NewReader(conn) for { // 讀取一行資料,交給幕後處理 line,_,err := reader.ReadLine() if len(line) > 0{ fmt.Printf("ReadData|%d \n",len(line)) executeBytes(line) } if err != nil{ break } } conn.Close()}
可是當 PHPer 測試發送大json資料的時候,發現了明確的截斷:
ReadData|4096|{"ename.com":........,"yunduo.com":{"check":[1// 又一次ReadData|4096|{"ename.com":........,"yunduo.com":{"getWhois"
想讓 json 還沒讀完就被截斷。我記得預設的 bufio.Reader 的大小是 4096,所以 bufio.Reader 的行為是讀滿了buffer就return出來啊。。我勒個去。我總不能寫個很大的size吧。
reader := bufio.NewReaderSize(conn,409600)
PHP 發送的測試資料最大可能得到100k左右,平均只有<10k。我服務端每次都開一個100k+的bufio.Reader太浪費啦。最後變成,開小了截斷,開大了浪費,我煞筆了。
解決方式
既然bufio.Reader.ReadLine() 玩不下去了,我就只能回到最原生的用法,例如改造後的代碼:
func handleConn(conn net.Conn) { buf := make([]byte, 4096) var jsonBuf bytes.Buffer for { n, err := conn.Read(buf) if n > 0 { if buf[n-1] == 10 { // 10就是\n的ASCII jsonBuf.Write(buf[:n-1]) // 去掉最後的分行符號 executeBytes(jsonBuf.Bytes()) jsonBuf.Reset() // 重設後用於下一次解析 } else { jsonBuf.Write(buf[:n]) } } if err != nil { break } } conn.Close()}
這樣就是每次讀出4096的位元組,讀到\n就截斷,和之前放入buffer的資料取出來一起交給後續運算。這樣不用在意傳來的資料大小,只要\n分隔字元正確就沒有問題。
經過測試也沒有發現什麼業務問題,pprof也沒有看出明顯的效能瓶頸。那就這麼用著,我鬆一口氣交差。
又煞筆啦
bufio.Reader.ReadLine()應該沒有那麼傻吧。如果buffer滿了就return,我怎麼知道只是滿了還是讀到\n的返回,萬一內容剛好buffer長度呢!下班回家翻源碼,我又煞筆了:
// ReadLine is a low-level line-reading primitive. Most callers should use// ReadBytes('\n') or ReadString('\n') instead or use a Scanner.//// ReadLine tries to return a single line, not including the end-of-line bytes.// If the line was too long for the buffer then isPrefix is set and the// beginning of the line is returned. The rest of the line will be returned// from future calls. isPrefix will be false when returning the last fragment// of the line. The returned buffer is only valid until the next call to// ReadLine. ReadLine either returns a non-nil line or it returns an error,// never both.//// ......func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) { line, err = b.ReadSlice('\n') if err == ErrBufferFull { // Handle the case where "\r\n" straddles the buffer. if len(line) > 0 && line[len(line)-1] == '\r' { // Put the '\r' back on buf and drop it from line. // Let the next call to ReadLine check for "\r\n". if b.r == 0 { // should be unreachable panic("bufio: tried to rewind past start of buffer") } b.r-- line = line[:len(line)-1] } return line, true, nil } ......}
我擦。如果讀出的滿了buffer,當不是\n結尾,isPrefix是true。我從來沒注意過中間這個傳回值的意思。真是被自己坑了。其實代碼可以這麼寫:
func handleConn(conn net.Conn) { reader := bufio.NewReader(conn) var jsonBuf bytes.Buffer for { // 讀取一行資料,交給幕後處理 line,isPrefix,err := reader.ReadLine() if len(line) > 0{ jsonBuf.Write(line) if !isPrefix{ executeBytes(jsonBuf.Bytes()) jsonBuf.Reset() } } if err != nil{ break } } conn.Close()}
RTFD
結論:Read The Fucking Documentation