对TCP数据流的理解
TCP是一种流式协议。
在发送端,当我们调用 send 函数完成数据“发送”以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中,至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能假设每次 send 调用发送的数据,都会作为一个整体完整地被发送出去。
在实际传输中,如果发送端两次调用 send 函数,发送 "hello" 和 "world" 报文,那么可能实际发送中是这个样子:
-
将"hello" 和 "world" 在一个TCP分组中发送:
...xxxhelloworldxxx... -
将"hello" 和 "world" 在两个个TCP分组中发送,其中分组1发送了"hello" 的一部分,"hello"剩下的部分随第二个分组和"world"一起发送:
...xxxxxhel // 分组1 loworldxxxxxxxxxx... // 分组2
实际上,上述类似的情况组合有无数种,其核心问题在于,我们不知道"hello" 和 "world" 报文是如何进行TCP分组传输的。即"hello" 和 "world" 报文和TCP分组并没有映射关系("hello" 报文并不一定只对应一个TCP分组)
不过在接收端,当我们使用recv从接收端缓冲区读取数据时,发送端缓冲区的数据是以字节流的形式存在的,无论在发送端发出的TCP分组是如何构造的,接收端最终收到的字节流总是像下面这样:
xxxxxxxxxxxxxhelloworldxxxxxx
在接收端:
- "hello" 和 "world" 的顺序是保持不变的,即先调用send函数发送的字节,一定在后调用send函数发送的字节的前面,这由TCP严格保证。
- 如果在发送过程中,TCP分组丢失,其后续分组到达,那么TCP协议栈会先缓存后续分组,一直到前面丢失的分组到达,最终形成可以被应用程序读取的数据流。
网络字节序
在计算机中进行保存和传输使用的都是 0101 这样的二进制数据,字节流在网络上的传输也是通过二进制完成的。从字节到二进制是通过编码完成的,而从二进制到字节是通过解码完成的。
在我们传输数字时,比如 0x0201,对应的二进制为 0000001000000001,那么两个字节的数据到底是先传 0x01,还是相反?(如果发送的是字符类型的数据,就没有所谓的网络顺序了)
主机字节序: 目前计算机对如何存储这种数据没有一个统一的标准,不同的系统有不同的存储方式,分别为 大端字节序 和 小端字节序 。
大端字节序:将 0x02 高字节存放在起始地址,即高字节存放在低地址。
小端字节序:将 0x01 低字节存放在起始地址,即低字节存放在低地址。
但是在网络传输中,必须保证双方都用的是同一种标准来表达。所以就有了网络字节序的选择问题,目前互联网使用的网络字节序采用大端模式进行编址。
为了保证网络字节序的一致,有如下转换函数(由参数可知,对数值类型(除字符类型外) 需要进行网络字节序的转换):
uint16_t htons (uint16_t hostshort)
uint16_t ntohs (uint16_t netshort)
uint32_t htonl (uint32_t hostlong)
uint32_t ntohl (uint32_t netlong)
n表示网络,h表示主机,s表示short,l表示long。
这些函数可以帮助我们在主机字节序和网络字节序的格式间灵活转换。使用这些函数时,我们并不需要关心主机到底是什么样的字节顺序,只要使用函数给定值进行网络字节序和主机字节序的转换就可以了。
读取并解析报文
在应用层,我们发送的报文是通过字节流的形式传输的,而字节流本身是没有边界的,那么接收端的应用程序应该如何解读所收到的字节流?
解决方法就是:发送端和接收端按照统一的报文格式进行数据传输和解析。其中,报文格式定义了字节的组织形式。当知道了报文格式后,接收端可以针对性地进行报文读取和解析工作。
报文格式有两种定义方法:
- 发送端把要发送的报文长度预先通过报文告知给接收端(显式编码报文长度)。
- 通过一些特殊字符进行边界划分。
显示编码报文长度
即我们先定义报文的结构,然后按照该结构进行发送。(接收端必须也知道该报文结构才可以进行解析)
特殊字符作为边界
HTTP报文格式如下:
可见,HTTP通过设置回车、换行符作为HTTP报文协议的边界。