看项目的tcp通信模块时跟同事的偶然讨论,意外拉出来好几样东西,体验非常棒,记录下来~( ̄0  ̄)y
我们的tcp网络直接用的conn,之前在看别人项目里见到有用bufio.Reader优化的
|
io.readatleast 不是也是使用一个分配好的缓冲区吗。
|
不是的,用的传进去的那个
|
这里的r.Read是原始的conn.Read
|
bufio.NewReader(conn)可以用bufio包上一层
|
代替conn传进io.readatleast
|
能看到源吗,这多包了一层,这个读和次数就减少了吗,什么原理。
|
稍等,我翻下那篇文章
|
https://zhuanlan.zhihu.com/p/21369473
func ReadFull(r Reader, buf []byte) (n int, err error) { return ReadAtLeast(r, buf, len(buf)) }
|
他用的io.ReadFull
|
跟我们实际是一样的
|
作者解释的说,bufio帮我们内置一个buf,io发生时数据先写进那个buf里了,我们去取时,先取buf里的,要是不够才会调底层io.Read,以此减少io调用次数
|
我明白这个意思, 换句话说,就应该像C++的收包过程一样,一性有多少字节,全部收下来,再去解析数据,用逻辑去分包。而不是用读操作去分包。
|
嗯,是这意思,有io就缓存,逻辑去取缓存的数据
是的,我一开始是打算这样做的,当时还有内存池的一些东西也没有想完善,就用了这种最简 的做法。
是的,现在一个包的内容,还要分三次读。
代码用法也很简单,用bufio包装conn,换下就OK了
理论上是多个包的内容,尽可能一次读。
嗯
下午改着跑跑试下
C++的处理粘包也有点麻烦,这个bufio这么好用吗,
我有时间也看看。
看例子,业务层的代码差别不大
还有一个优化也准备后面做,就是还要加一个内存池,不让GC回收。
这个叫达达的很厉害,一直做go服务器的,出了蛮多干货
你看一看,顺便了解三个问题,1, bufio内部是怎么分配的内存,2, 分配多大的内存,3,这个内存是分配一次,还是多次。
OK
想了一下,bufio也不行,肯定也不高效。
什么原因呢?
最高效的做法,还是学C++的处理服务器包的方式。
c++也是recvBuf不断接收io数据,逻辑操作的总是recvBuf
如果bufio每次都新建buf,那用都不能用,现在假设bufio接受用的一个固定大小的buf.
不会每次都新建,不然就数据就被丢了,肯定是一条连接一个buf的
我们每read一次,这个buf的前一部分字节被我们读出来的,他内部是不是要进行数据的移动。
看看底层怎么控制buf的增长、缩短~
这个要看buf的设计了
数组试的,肯定会移动
circle式的就不用
是的,我说的就是这个移动的次数会比我们自己实现移动次数多。
还有游标式的,只有内存不够才会移动
这个我估计我们自己用一个固定的缓冲区来做,比这个会高效一点。
嗯,是有顾虑,看下bufio内存的缓冲咋写的,要是不好,可以参照逻辑写个类似的
我们用一个固定的缓冲区来接收,解析出一个包,移动一次。
包就定为4k。
不用变动,我们目前所有TCP包,都很小.
好的
但其实这样的优化不是很大, @周梦飞 你研究过C++服务器,知道C++服务器里每一个连接都有一个接收缓冲区, 其GO底层就己经这个做了。
这个优化我们需要做,不如C++那边的优化那么大。C++如果没有缓冲区就直接读的网络上的数据,像这样一个包读三次,服务器就完了。
在GO语言里,我们一个包在我们这层逻辑上读了三次,在GO底层,其实一次性就读完了所有的数据。
不一定吧,如果数据还没到,io.Read会阻塞当前线程等数据,数据到了重新唤起
数据没有到,不应该阻塞吗。
GO在底层有一个读网络数据的逻辑,有大概8kb的缓存/
异步的网络架构不用,这个另一个问题了,写法差别也挺大
这个8k是缓存了从系统tcp层取来的数据……那io.Read平常的消耗应该不会很重
如果conn本身的io.Read不是取系统层的数据,那确实没多大必要再包一层buf
嗯,你可以查一些资料确认一下,我也是之前不记得在哪看的。
不过8k缓存了TCP层的数据,但每次取也有锁,代价不大的情况下,包一层buf效率也有好处
待会看下bufio的实现……之前有瞅见过别人跟io.Read(还是Write?)解释调用路径的,找找看
IO调用的开销是什么呢?这得从Go的runtime实现分析起,假设我们这里用到的是一个TCP连接,从`TCPConn.Read()`为入口,我们可以定位到`fd_unix.go`这个文件中的`netFD.Read()`方法。
这个方法中有一个循环调用`syscall.Read()`和`pd.WaitRead()`的过程,这个过程有两个主要开销。
首先是`syscall.Read()`的开销,这个系统调用会在应用程序缓冲区和系统的Socket缓冲区之间复制数据。
其次是`pd.WaitRead()`,因为Go的核心是CSP模型,要让一个线程上可以跑多个Goroutine,其中的关键就是让需要等待IO的Goroutine让出执行线程,当IO事件到达的时候再重新唤醒Goroutine,这样来回切换是有一定开销的。
而我们的这个分包协议的包头很小,有极大的概率是包头和一部分包体甚至是整个包已经在Socket缓冲区等待我们读取,这种情况就很适合使用`bufio.Reader`来优化性能。
就是这段,他跟过系统调用路径,说明消耗点
go在底层,使用的也是iocp, iocp接收数据,怎么会应用层调read的时候,才去复制数据。
如果是用的iocp,那这个syscall.Read很可能只是投递了一次RecvIO,要等完成端口的回调,这个过程中就有系统socket的拷贝了……只是猜测
嗯,不管怎么实现,我们上面需要做的优化,肯定都要做。
看了下bufio.Read的实现,内部用的游标式buf,不会频繁移动内存
读完后游标会归零,复写之前的数据区,缓冲默认是4096
这个buf貌似不会自动增长,满了后调panic("bufio: tried to fill full buffer")
有个接口NewReaderSize(2)可以指定buf的长度
如果不是circle的,就不可能不移动数据吧
游标式的,如果设计成写满就挂,那也不用移动……c++里的游标到尾巴了,如果前面用掉的区域够,就移动内存块到buf前面,如果还不够就会resize了
这个还是不太好,最好的还是一个固定内存,一次性读完数据(小于4K), 解析所有完整的包,就把剩余的数据(不完整的包),往最前面移动的一次
这个内存不存在resize的问题, IO也会减少。
模仿c++的做法,也可以,readRoutine是个单独的线程,阻塞了没啥
发现bufio实际已经是这样的了,整个文件里调底层Read的只有两个地方
一个是外界传入的[]byte超过4096时候,直接调底层Read,就是你讲的一次读完了
另一个是内部缓存已被读完,调了fill(),里面会用剩余的缓冲去读“b.rd.Read(b.buf[b.w:])”也是足够大的
效果上就是固定大内存去一次性尽可能读全部数据
加上bufio了,再改了下doWrite的写法,用select省下每次都要的判断
试了间隔200毫秒发100byte数据,能正常收到,没报错