Thrift是Facebook的一个开源项目,主要是一个跨语言的服务开发框架 提供完整的解决方案 优点很多也就不说了, 但是有个缺点必须要求客户端调用采用thrift框架 于是开始使用基本socket 来传输数据 一看到涉及底层,有的同学就表示
现在分析其中一个问题:
问题1 如何读出socket所有数据 (这里阻塞方式 )
方法1 读取数据到固定大小冲区(读取一次)
var buffer []byte = make([]byte, 1024)
outBytes, err := con.Read(buffer)
缺点:对方发送数据过大 造成解析失败
日志:
2017/12/21 15:12:33 read pack failed runtime error: slice bounds out of range
没有接受到完整的包
什么是半包 接受方没有接受到完整的包,只接受了一部分。 由于发送方看到内容太大切分数据包进行发送,
涉及问题:如何确定接收数据的大小
方法2:读取数据到动态缓冲区(读取多次)
缺点:涉及问题: TCP粘包
概念:TCP TCP 协议本身把这些数据块仅仅看成一连串无结构的字节流 就是没有界限的一串数据.就像河里的流水,绵延不断,没有分界 针对结构化数据如何确定边界呢
思考下面几个场景
1: 类似 http的请求就不用考虑粘包的问题,因为服务端收到报文后, 就将缓冲区数据接收, 然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。
2:如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
3: UDP
UDP是有边界的,应用层要整包地收,一次只能收一包,每次接收的要么是一个独立的完整的数据包,要么什么也接收不到。
TCP是无边界的,是字节流,需要应用层自己判断包边界,一次不一定能收几包,也不一定是完整的包
粘包情况
每个包数据长度大小不一 ,每次接受和发送 数据大小不一致 可能导致 包含数据,也可能多,也可能少于实际数据
第一种情况,接收端正常收到两个数据包 都是完整的包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。
第二种情况(多),接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。
这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
第三种情况(少),这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,
这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
方法3 粘包处理原理:读取数据到缓冲区,然后根据协议来解析
包头+数据的格式 确定边界
解包步骤: 1 建立一个缓冲去 2 读取固定数据到缓冲区 3 如果符合一个完整的pack,移走 4 如果不符合 ,继续读取 说明:数据不能错位
方法4 粘包处理原理:通过请求头中数据包大小,将客户端N次发送的数据缓冲到一个数据包中
从数据流中读取数据的时候,只要根据包头和数据长度就能取到需要的数据。这个其实就是平时说的协议(protocol)
里面处理这个包的方式之一如下:
1: 一直阻塞读取第一个第二个字节,获取版本号(如果错误就做错误处理);
2: 然后读取第三、四个字节,获取数据的大小;
3: 然后根据第二步中的数据大小,后面下面的数据;
4: 重复上面的过程;
NSQ就是采取这种方式。
参考例子
1 https://feixiao.github.io/2016/05/08/bufio/
2 http://blog.csdn.net/scythe666/article/details/51996268
思考:
这只是其中一个问题 阻塞下read,
非阻塞,同步呢 异步
正确读写方式是什么
不同语言实现区别:
read函数说明
对于阻塞socket,read/write返回-1代表网络出错了。
但对于非阻塞socket,read/write返回-1不一定网络真的出错了。
可能是Resource temporarily unavailable。
这时你应该再试,直到Resource available
综上,对于non-blocking的socket,正确的读写操作为:
读:忽略掉errno = EAGAIN的错误,下次继续读
写:忽略掉errno = EAGAIN的错误,下次继续写
对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。
所以,在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN 写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN。