绝大部分情况下, 从 TCP 接收数据都存在一个把 "TCP 流" 转成 "数据块" 的问题, 那么为什么 TCP 当初还要设计成 "流"

2018-07-08 10:19:14 +08:00
 c3824363

TCP 设计成 "流", 只是实现起来容易一些吧, 但用起来真不爽 实际上程序里用到的都是一块一块的内存, TCP 强行弄出一个"流"来. 这样在接收方必然要从 "流" 转换成 "块", 转换的方式有: 固定长度, 加包头指定长度, 用特殊的间隔标记.

固定长度: 简单无开销, 但是太死板不灵活, 适用场合很少. 加包头指定长度: 编程简单, 但会多一次读取开销. 用特殊的间隔标记: 比如 HTTP 这种, 就需要遍历全部内容

如果 TCP 原本就保留“块”信息, 则使用起来就会简单很多了。 比如这样定义

struct iovec {
   void  *iov_base;
   size_t iov_len;
};

/*
  返回发送出去的 struct iovec 的个数, 不要发送半个
  出错的情况的返回值和 write()/send() 一样
 */
int my_writev(int fd, struct iovec *vec, size_t n_vec);

/*
  返回接收到的 struct iovec 的个数, 不要接收半个
  出错的情况的返回值和 read()/recv() 一样
  接收的时候可以把 struct iovec *vec 预先分配,也可以不分配直接接收到 buf 里面同时把分块信息保存到 struct iovec *vec。
 */
int my_readv(int fd, struct iovec *vec, size_t n_vec, void *buf, size_t size_of_buf);

这样用来起来再很多场合就非常方便了。

6044 次点击
所在节点    程序员
53 条回复
bao3
2018-07-08 13:37:14 +08:00
楼主你把一杯水倒入另一个杯子,你期待的是水像冰块一样掉到另一个杯子里吗?可是你不确定另一个杯子的口径,你也无法提前分割冰块的大小。另外冰块掉到另一个杯子可能的先后顺序是乱的。

但当你用液态水来倒的话,你就不作关心对方的口径以及到达的顺序。
对你来说,你期待的是用块来发送数据还是用流发送?
CRVV
2018-07-08 14:06:19 +08:00
> 如果 TCP 原本就保留“块”信息, 则使用起来就会简单很多

如果程序说要发送一个 1200 字节的块,要求保证送达,当前链路的 MTU 只有 800,该怎么处理?

1. 返回错误,这太难用了
2. 把 1200 的块拆开发出去
2.1 用 2 个包只发 1200 字节,这样浪费了 400 字节( 2 个包本来可以发 1600 )
2.2 用第 1 个包发 800,第 2 个包发 400 再加上下一个块的 400

1 大约是带重传的 IPv6
2.1 大约是带重传的 IPv4
2.2 是 TCP 加上分块,所以新的问题是应该用哪个方法来分块? 固定长度, 加包头指定长度还是用特殊的间隔标记?

结论是 TCP 不能保留“块”信息,这样做只是把分块的问题推到了 TCP 上,而传输层比应用层更不知道需要什么分块方式
c3824363
2018-07-08 14:33:34 +08:00
@CRVV 用 UDP 那种方式就行, 现有的 iphdr 就能处理。参考 IP 分片
知道块的长度总会带来很多方便的, 很多用户态的代码都在处理下面这个事情

接收固定长度包头
根据包头信息接收指定长度的包
重复以上步骤

从实用的角度看,TCP 可以携带分块信息。
ipwx
2018-07-08 14:34:11 +08:00
那当然是因为“流”比“块”更底层啊。

楼主你以为 UDP 是发了多大的块,就接收到多大的块嘛? IP 协议允许的包大小不超过 64K,但实际中不一定能达标。而且事实上这个 64K 打包会发生分片传输,实际的包传输大小也不过几百字节。

https://en.wikipedia.org/wiki/IP_fragmentation
https://stackoverflow.com/questions/3712151/udp-ip-fragmentation-and-mtu

而且就算 IP fragmentation 默默地帮你搞定了重整,性能也实打实损失了的。
- - - -

总结一下,IP 协议中的“分块”是 IP 协议根据大部分传输介质的性质定出来的 IP 协议实现的标准,本身对于上层应用具有很有限的参考意义。由于 IP 包传输过程中大小不确定、分片机制不明,对于上层应用而言,“流”才是比“包”更底层的模型。
yanaraika
2018-07-08 14:47:17 +08:00
sctp/quic/http2 欢迎你
c3824363
2018-07-08 14:50:37 +08:00
@ipwx 我的意思是流加上分界信息, 给用户态程序多一种按块接收的选择。
redsonic
2018-07-08 14:52:09 +08:00
楼主你理解错了,TCP 设计之初主要是面向文件传输的,在这种情况下没人会关心或干涉“流”之中的“块”。另一种用途是远程登录,因为这是人机的不间断交互所以本质也是流,同样不会关心“块”。如果你需要通过某些精心设计的“块”来驱动应用程序,那么 UDP 是干这事的。或者是 sctp。
hjc4869
2018-07-08 15:03:01 +08:00
@c3824363 能否 NAT 跟 SEQPACKET 无关,关键在于路由设备是否支持特定传输层协议。例如如果路由器支持 SCTP NAT,那么自然也支持 SCTP 的 SEQPACKET。

HTTP 的头部内容直接顺序读取流即可,直到 \r\n\r\n 即头部结束。后续的 content 也不需要分包,是货真价实的流。
julyclyde
2018-07-08 18:15:47 +08:00
如果按块发,你还得自己拼顺序,就不只是从一个保证顺序的流里边抠出块那么简单了
wwqgtxx
2018-07-08 18:36:35 +08:00
@julyclyde 楼主也只说按块发,也没说不保留 TCP 的循序接受特性吧
julyclyde
2018-07-08 18:42:37 +08:00
@wwqgtxx 你就是杠精本精
q397064399
2018-07-08 19:53:58 +08:00
流是更低一层次的抽象,块是高层次的抽象,Unix 的哲学就是简单,一些都是文件的哲学 而文件正好就是流的形式,
你需要更高层次的接口,在这个抽象上进行封装就好了,
dacapoday
2018-07-08 20:21:08 +08:00
咋不看看当年有块设备吗?都是磁带,内存还是靠延时线存储的。
dacapoday
2018-07-08 20:22:01 +08:00
流这种抽象一直沿用到现在,说明它最实用。
akira
2018-07-08 21:23:19 +08:00
发送一个字节的时候怎么办
chinawrj
2018-07-08 21:36:53 +08:00
你们啊,还太年轻。哈哈
momocraft
2018-07-08 21:38:22 +08:00
首先流是個很好的抽象, tcp 不是一個對上層的消息完整性負責的協議.

另外發明 tcp 時不像現在, 隨便人寫個 protobuf 就能正確處理消息邊界 (看看中文互聯網有多少人糾結"粘包問題"). 在黎明時期一個連接只傳輸一個消息, 用關閉連接表示消息結束并不罕見, 比如 ftp 甚至很久后的 http0.9

協議和應用是互相推動的, 現在責怪 tcp 不是消息單位可説事後諸葛亮
goodniuniu
2018-07-08 23:17:51 +08:00
本质就是流+1
yankebupt
2018-07-09 00:54:45 +08:00
@c3824363 估计是参考当时的网速综合了实时性做出的妥协...
以包为单位,确认了这个包,这个包就算传到了,如果是实时聊天或者网游的话就可以拿去渲染了,延迟和 ping 一样...
如果是流,确认频次和包一样的话对比包没多大节省,如果确认间隔太长了碰到误码稍大,实时性差太远,即使 UDP 自定义纠错也比强行用 FEC 之类的纠错码压误码率来的性能略高(应该)。
Mirana
2018-07-09 01:24:03 +08:00
分成 N 个块,每块之间都有次序,拼起来不就是个完整的流吗

协议设计不应该考虑太多平台,实现细节方面的问题

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/468985

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX