分享
  1. 首页
  2. 文章

我煞笔的被 bufio.Reader 小坑

傅小黑 · · 12073 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

最近在用 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


有疑问加站长微信联系(非本文作者)

本文来自:傅小黑的自留地

感谢作者:傅小黑

查看原文:我煞笔的被 bufio.Reader 小坑

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
12073 次点击 ∙ 2 赞
5 回复 | 直到 2018年02月09日 19:12:33
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏