Golang: 從 io-Reader 中讀數據
Go 的標準庫提供了多個從 io.Reader
中讀取數據的方法 (io、ioutil),本文通過從 net.Conn
中讀取數據爲例,演示各種方法已經應用場景。
使用 TCP 連接訪問某個網站,採用HTTP 1.0
的協議,讓 TCP 連接保持短連接,讀取完 response 之後連接會關閉,這樣就模擬了io.EOF
的錯誤:
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
io.Reader.Read
io.Reader
接口定義了Read(p []byte) (n int, err error)
方法,我們可以使用它從 Reader 中讀取一批數據。
- 一次最多讀取
len(p)
長度的數據 - 讀取遭遇到 error(io.EOF 或者其它錯誤), 會返回已讀取的數據的字節數和 error
- 即使讀取字節數 <len(p), 也不會更改 p 的大小 (顯然的,因爲正常它也沒辦法更改)
- 當輸入流結束時,調用它可能返回
err == EOF
或者err == nil
,並且n >=0
, 但是下一次調用肯定返回n=0
,err=io.EOF
常常使用這個方法從輸入流中批量讀取數據,直到輸入流讀取到頭,但是需要注意的時候,我們應該總是先處理讀取到的 n 個字節,然後再處理 error。
如果你要實現io.Reader
接口,在實現Read
方法時,不建議你返回0,nil
, 除非輸入的len(p)==0
, 因爲這容易造成困惑,一般用0,io.EOF
來表示輸入流已經讀取到頭。
下面是演示這個方法的例子:
package main
import (
"fmt"
"io"
"net"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
var sb strings.Builder
buf := make([]byte, 256)
for {
n, err := conn.Read(buf)
if err != nil {
if err != io.EOF {
fmt.Println("read error:", err)
}
break
}
sb.Write(buf[:n])
}
fmt.Println("response:", sb.String())
fmt.Println("total response size:", sb.Len())
}
io.Copy
func Copy(dst Writer, src Reader) (written int64, err error)
可以從 src 複製數據到 dst(Go 實現的方法一般把目的地址參數放在源參數的前面),直到:
- EOF
- 其它 error
它返回讀取的字節數以及遇到的第一個錯誤
注意成功的讀取完輸入流後 err 並不是io.EOF
, 而是nil
。
它內部是調用CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
實現的,
CopyBuffer
會有可能使用一個指定的buf
來複制數據, 如果 buf 的長度爲 0, 它會 panic。
在兩種特例的情況下 buf 並不會被使用, 而是使用接口的方法進行復制:
- src 實現了
WriterTo
接口 - dst 實現了
ReadFrom
接口
package main
import (
"fmt"
"io"
"net"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
var sb strings.Builder
_, err = io.Copy(&sb, conn)
if err != nil {
fmt.Println("read error:", err)
}
fmt.Println("response:", sb.String())
fmt.Println("total response size:", sb.Len())
}
ioutil.ReadAll
ReadAll(r io.Reader) ([]byte, error)
提供了一個從輸入流中讀取全部數據的方法,它讀取輸入流直到出現錯誤(error)或者讀到頭(EOF)。
成功的讀取到頭不會返回 io.EOF, 而是返回數據和 nil。
我們經常用它來讀取可信的 http.Response.Body。
package main
import (
"fmt"
"io"
"io/ioutil"
"net"
)
func main() {
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
data, err := ioutil.ReadAll(conn)
if err != nil {
if err != io.EOF {
fmt.Println("read error:", err)
}
panic(err)
}
fmt.Println("response:", string(data))
fmt.Println("total response size:", len(data))
}
io.ReadFull
ReadFull(r Reader, buf []byte) (n int, err error)
從輸入流中讀取正好 len(buf) 字節的數據。
- 讀取到 0 字節,並且遇到 EOF, 返回 EOF
- 讀取到 0<n<len(buf) 字節,並且遇到 EOF, 返回 ErrUnexpectedEOF
- 讀取到 n==len(buf), 即使遇到 error 也返回 err=nil
- 返回 err != nil 則肯定 n<len(buf)
package main
import (
"fmt"
"io"
"net"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
var sb strings.Builder
buf := make([]byte, 256)
for {
n, err := io.ReadFull(conn, buf)
if err != nil {
if err != io.EOF && err != io.ErrUnexpectedEOF {
fmt.Println("read error:", err)
}
break
}
sb.Write(buf[:n])
}
fmt.Println("response:", sb.String())
fmt.Println("total response size:", sb.Len())
}
io.ReadAtLeast
ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
從輸入流中讀取至少 min 個字節到 buf 中,直到把 buf 讀取滿或者遇到 error,包括 EOF。
- 如果 n < min, 肯定返回錯誤
- 沒有數據可讀返回 n=0 和 EOF
- n < min, 並且遇到 EOF, 返回 n 和 ErrUnexpectedEOF
- 如果參數 min>len(buf), 返回 0,ErrShortBuffer
- 如果讀取了至少 min 個字節,即使遇到錯誤也會返回 err=nil
package main
import (
"fmt"
"io"
"net"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
var sb strings.Builder
buf := make([]byte, 256)
for {
n, err := io.ReadAtLeast(conn, buf, 256)
if err != nil {
if err != io.EOF && err != io.ErrUnexpectedEOF {
fmt.Println("read error:", err)
}
break
}
sb.Write(buf[:n])
}
fmt.Println("response:", sb.String())
fmt.Println("total response size:", sb.Len())
}
io.LimitReader
LimitReader(r Reader, n int64) Reader
返回一個內容長度受限的 Reader,當從這個 Reader 中讀取了 n 個字節一會就會遇到 EOF。
設想一下如果沒有這個保護措施,別讓告訴你發送一個圖片,結果發送給你一個 3G 的葫蘆娃的視頻,有可能會使你的內存飆升。
它就是提供了一個保護的功能,其它和普通的 Reader 沒有兩樣。
package main
import (
"fmt"
"io"
"net"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "rpcx.site:80")
if err != nil {
fmt.Println("dial error:", err)
return
}
defer conn.Close()
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
var sb strings.Builder
buf := make([]byte, 256)
rr := io.LimitReader(conn, 102400)
for {
n, err := io.ReadAtLeast(rr, buf, 256)
if err != nil {
if err != io.EOF && err != io.ErrUnexpectedEOF {
fmt.Println("read error:", err)
}
break
}
sb.Write(buf[:n])
}
fmt.Println("response:", sb.String())
fmt.Println("total response size:", sb.Len())
}
參考資料
- https://github.com/golang/go/issues/20477
- https://stackoverflow.com/questions/24339660/read-whole-data-with-golang-net-conn-read
- https://stackoverflow.com/questions/29221872/golang-read-bytes-from-net-tcpconn
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://colobu.com/2019/02/18/read-data-from-net-Conn/