V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
c3824363
V2EX  ›  程序员

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

  •  
  •   c3824363 · 2018-07-08 10:19:14 +08:00 · 6257 次点击
    这是一个创建于 2359 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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);
    
    

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

    53 条回复    2018-07-09 21:59:54 +08:00
    shijingshijing
        1
    shijingshijing  
       2018-07-08 10:25:49 +08:00   ❤️ 1
    主要是考虑到通用性吧,在 Kernel 内部都以 Stream 方式来操作数据,这样 pipe,文件,网络都可以有一个很好的模型 cover 到,编程也简单,给 application 更大的自由度。
    MeteorCat
        2
    MeteorCat  
       2018-07-08 10:27:49 +08:00 via Android
    TCP 的流只是说,数据到达顺序的正确性,但是其中受限于 MTU,缓冲区可能会把“ 123456789 ”一次性发动(假设 MTU>9 位),也可能超过最大传输分别发“ 123 ”,“ 456 ”,“ 789 ”这样的,为了抑制最大传输>MTU,就按你所说来转成“数据块”,从最开始说设计成流保证了传输数据正确性(流没有乱序包问题),而之所以应用层要又改成块则是为了适应 MTU
    des
        3
    des  
       2018-07-08 10:29:12 +08:00 via Android   ❤️ 1
    你可以选用 sctp

    这种设计还是通用性和简单性吧
    hjc4869
        4
    hjc4869  
       2018-07-08 10:29:24 +08:00 via iPhone   ❤️ 1
    楼主是想要 SEQPACKET ?
    SYP
        5
    SYP  
       2018-07-08 10:33:51 +08:00
    流这个是为了帮助理解,实际在 TCP 层处理的数据也是一块一块的小块,你说的这种封装属于上层协议要处理的内容。
    whileFalse
        6
    whileFalse  
       2018-07-08 10:36:09 +08:00
    因为人家本来就是流。
    xmadi
        7
    xmadi  
       2018-07-08 10:41:21 +08:00 via iPhone
    本来就是流 +1 我思考了下 更倾向这个说法 因为再往下到物理层电信号光信号的传输也是流
    iwtbauh
        8
    iwtbauh  
       2018-07-08 10:44:43 +08:00 via Android
    TCP/IP 是在 Unix 上开发的
    这其实就是 Unix 的终极设计思想:“一切都是文件”
    “一切都是文件”意味着“一切都是数据流”
    受到这种理念的影响,协议和编程接口有意设计成这种形式。而事实是,这种形式的接口( BSD socket )战胜了其他的接口,并成为标准。
    再说一下你的想法,知道 TCP/IP 为什么要分层吗,因为分层降低了软件的整体复杂度,TCP/IP 协议栈的每一层只需要考虑自己这一层的任务,并给上层提供机制。你的需要只是在 TCP 上再加一层,如果你愿意,就写成一个库的形式(参考 OpenSSL )
    yulon
        9
    yulon  
       2018-07-08 10:51:32 +08:00
    因为可靠协议有序不丢包做成流是最好的,你要无序丢包直接用 UDP,你要无序不丢包直接用 UDP 做丢包验证。
    teleme
        10
    teleme  
       2018-07-08 11:19:33 +08:00
    数据块的处理成本高,在规模型生产环境下,主要是基于流进行运算处理。
    zhujinliang
        11
    zhujinliang  
       2018-07-08 11:34:47 +08:00
    什么是流,给“块”加上 FIFO 缓冲就是流,为什么要加缓冲,可以想一下,通信线路不停地把数据送入 IO 设备,假设每毫秒到达一个字节
    1. 如果没有缓冲,CPU 必须每毫秒至少检查一次设备寄存器,如果没来得及检查,要么设备丢弃后来的数据,要么后来的数据覆盖之前的数据,总之,因为没地方存放新来的数据,必须丢弃一个
    2. 如果有一个 16KB 的缓冲,不考虑延迟的话,系统只需 8K 毫秒左右查询一次缓冲即可,如果缓冲有数据,就全部取出,批量处理,既减少了在查询操作上的开销,又降低了丢弃数据的概率
    3. 还有一个方法是增加一个设备忙信号,IO 设备接收到一个数据后置为设备忙状态,直到 CPU 从设备中取走数据,这种方式可避免丢弃数据,但得到的结果是通信线路大量闲置,本可以 CPU 处理数据与线路传输同时进行,但因为没有缓冲,必须接收、处理轮流进行
    c3824363
        12
    c3824363  
    OP
       2018-07-08 11:44:33 +08:00
    @hjc4869 是的就是要这个,bbr 对它有效么。 但是 windows 平台应该不支持吧

    @des SEQPACKET 就是用 sctp 实现的吧

    @shijingshijing @iwtbauh 看一些很早期的代码发现一个习惯就是要尽可能的节省内存, 感觉是这个原因导致的用"流"不用"块"

    @MeteorCat @SYP @whileFalse @xmadi 我觉得只有模拟音频信号通信才算绝对的流,TCP 是因为把数据弄到一块丢失了分块信息才看起来变成了“流”

    @teleme 是因为内存开销么
    c3824363
        13
    c3824363  
    OP
       2018-07-08 11:47:04 +08:00
    @zhujinliang 还是不能解释既然有 UDP 也有 TCP,为什么不早早就弄一个有 UDP 优点的 TCP 呢。 而是近些年才有的 SEQPACKET
    hjc4869
        14
    hjc4869  
       2018-07-08 12:05:57 +08:00
    @c3824363 SEQPACKET 也需要 congestion control,但是目前 Linux 的 bbr 似乎只是针对 TCP。
    SCTP 既实现了 stream 也实现了 seqpacket,Windows 上有第三方驱动可以用,也可以用 raw socket 在用户态实现 SCTP。但是各类 NAT 设备可能没有很好地支持 SCTP。
    hjc4869
        15
    hjc4869  
       2018-07-08 12:12:58 +08:00   ❤️ 3
    另外楼主所说的,“绝大部分情况下”实际上是不成立的,例如 HTTP 协议就没有这么做。
    MeteorCat
        16
    MeteorCat  
       2018-07-08 12:26:34 +08:00 via Android
    @c3824363 模拟音频信号通信和网络信号不能混为一谈,如果按照这样的类比那完全没有说下去的必要
    jtsai
        17
    jtsai  
       2018-07-08 12:30:28 +08:00 via Android
    流节省内存,流就是缓存的块。
    liuminghao233
        18
    liuminghao233  
       2018-07-08 12:31:47 +08:00 via iPhone
    现在的基于 proactor 模型的网络库
    你可以在包头加一个长度
    跟 udp 比也就多一次 callback
    还不够简单吗
    c3824363
        19
    c3824363  
    OP
       2018-07-08 13:07:14 +08:00
    @hjc4869 SEQPACKET 到底能不能过 大部分 NAT 呢。
    HTTP 1.1 头部的内容肯定要遍历吧,可能是边读边遍历。
    owenliang
        20
    owenliang  
       2018-07-08 13:33:51 +08:00 via Android
    xml 流了解一下,并不是所有协议都是 package。
    bao3
        21
    bao3  
       2018-07-08 13:37:14 +08:00 via iPhone   ❤️ 7
    楼主你把一杯水倒入另一个杯子,你期待的是水像冰块一样掉到另一个杯子里吗?可是你不确定另一个杯子的口径,你也无法提前分割冰块的大小。另外冰块掉到另一个杯子可能的先后顺序是乱的。

    但当你用液态水来倒的话,你就不作关心对方的口径以及到达的顺序。
    对你来说,你期待的是用块来发送数据还是用流发送?
    CRVV
        22
    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
        23
    c3824363  
    OP
       2018-07-08 14:33:34 +08:00
    @CRVV 用 UDP 那种方式就行, 现有的 iphdr 就能处理。参考 IP 分片
    知道块的长度总会带来很多方便的, 很多用户态的代码都在处理下面这个事情

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

    从实用的角度看,TCP 可以携带分块信息。
    ipwx
        24
    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
        25
    yanaraika  
       2018-07-08 14:47:17 +08:00
    sctp/quic/http2 欢迎你
    c3824363
        26
    c3824363  
    OP
       2018-07-08 14:50:37 +08:00
    @ipwx 我的意思是流加上分界信息, 给用户态程序多一种按块接收的选择。
    redsonic
        27
    redsonic  
       2018-07-08 14:52:09 +08:00
    楼主你理解错了,TCP 设计之初主要是面向文件传输的,在这种情况下没人会关心或干涉“流”之中的“块”。另一种用途是远程登录,因为这是人机的不间断交互所以本质也是流,同样不会关心“块”。如果你需要通过某些精心设计的“块”来驱动应用程序,那么 UDP 是干这事的。或者是 sctp。
    hjc4869
        28
    hjc4869  
       2018-07-08 15:03:01 +08:00
    @c3824363 能否 NAT 跟 SEQPACKET 无关,关键在于路由设备是否支持特定传输层协议。例如如果路由器支持 SCTP NAT,那么自然也支持 SCTP 的 SEQPACKET。

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

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

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

    协议设计不应该考虑太多平台,实现细节方面的问题
    GTim
        41
    GTim  
       2018-07-09 08:36:04 +08:00
    @Mirana 哈哈

    多多观察:水和蓄水池,还有流速 应该就会理解了
    ca1123
        42
    ca1123  
       2018-07-09 08:56:22 +08:00
    不用流,你发的时候岂不是得知道尾巴在哪?有些应用,发的时候根本不知道什么时候会有尾巴。其实流的意思是,我的块就这样,全是标准的,看你怎么用吧。
    enenaaa
        43
    enenaaa  
       2018-07-09 09:13:13 +08:00
    流比块更好用。
    如果设计成块结构, 就会有人问, 我想像文件流那样读数据还得自己写代码?
    我想自定义块结构, 还得在块上加块?
    c3824363
        44
    c3824363  
    OP
       2018-07-09 09:24:23 +08:00
    @enenaaa 从编程的角度看 块可以当成流,但反之不行
    enenaaa
        45
    enenaaa  
       2018-07-09 09:28:22 +08:00
    @c3824363 现在应用层协议照样可以自定义块结构。
    zhicheng
        46
    zhicheng  
       2018-07-09 09:43:16 +08:00 via iPhone   ❤️ 1
    不是要设计成流,而是只能设计成流,不然设计成块,你打算让 OS 怎么办?传送一个 1G 的块,OS 要把它全 Cache 到内存里?内存不够怎么办?存到磁盘?一下可用场景就少了。如果一开始加上最大容量,比如 4G,现在又会嫌不够,又要处理多块变成流了。如果你真的需要块,请在创建连接的时候发一个 4 字节或 8 字节的长度,然后不停的 recv,至于存到内存里还是磁盘里还是转发给另一个设备,取决于 App 自己。如果一个工程师连这都解决不了,不建议去吐槽 TCP 的设计。
    deadEgg
        47
    deadEgg  
       2018-07-09 10:03:57 +08:00
    流实际上就是更小块的块。设计成流是为了 os 更方便的读,如果涉及成块,足够大的块,失败回退的可能远远放大。放大的原因不仅仅是 os 缓冲区大小受限(#46 老哥也说到了)。更为重要的原因在于,失败回退导致不断重发容易导致拥堵。

    换句话说,就好比在一条大道上,各种大小宽度不同的车辆, 没有规定大小的概念,所有车辆都一直往前开,是不是拥堵而又效率低下。

    设计成流个人认为就是块结构更小的划分,虽然多了一些冗余的字段,但是更好地保证了交通的畅通。

    个人观点。
    mengzhuo
        48
    mengzhuo  
       2018-07-09 10:07:01 +08:00
    如果传输的数据始终小于 68 字节,不讲究顺序,那可以用 udp。
    mingl0280
        49
    mingl0280  
       2018-07-09 10:22:52 +08:00
    这就是没搞清楚为什么 TCP 叫可靠的基于字节流的协议的人问出来的问题,我估计这个题主恐怕也是深受“ tcp 粘包”问题的困扰啊。
    人家 TCP 就不管你上层协议传了什么东西的,分包和分块都是上层协议的事情,TCP 只负责传递的数据包是可靠有序的,它为什么还要管你上层数据包传了什么东西?如果要管的话你 TCP 协议还要去验证上层包发对没,既容易出错也不好解决数据过大导致的块过大的问题。
    还有,有些数据根本没有长度限制的,如果按上层协议进行检查怕不是分分钟一个文件发送吃完整个系统的内存……
    BOYPT
        50
    BOYPT  
       2018-07-09 10:50:03 +08:00
    “数据块”需求很明显是较为上层的需求。了解一下 OSI 模型,TCP/UDP 处于中低层;“流”是颗粒度更小的“块”,流到块是应该根据应用层需求,在上层模型实现的。
    newtype0092
        51
    newtype0092  
       2018-07-09 11:18:28 +08:00   ❤️ 2
    为什么自来水管不设计成以桶为单位送水呢?拧一下水龙头就掉出来一桶水,到时候收水费也方便,直接算你用了多少桶就好了。
    什么?你说你只要接一杯水?为了效率这点浪费是不可避免的,习惯就好了。。。
    sampeng
        52
    sampeng  
       2018-07-09 14:05:27 +08:00
    你自己发 udp。封装一下。就是你要的块的方式。然后再加保证到达,超时,巴拉巴拉巴拉机制,恭喜你,你实现了 tcp 协议的轮子
    flynaj
        53
    flynaj  
       2018-07-09 21:59:54 +08:00
    OSI 模型了解一下,基础不好就是这样,限制自己发展!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   941 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 22:43 · PVG 06:43 · LAX 14:43 · JFK 17:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.