0 今日话题
为什么TCP要做成流式协议,而非包呢?
哦莫西多 。 先假设TCP做成“包式协议 “的ROI,再探讨可⾏性。先要澄清,作为传输层协议,实际上有对上和对下两套抽象。
1 对下
即⽹络层,⼀般是IP协议,再下就是⽹络层如以太⽹协议 。
TCP对下层的抽象从第⼀天开始就是包。TCP失败重传、拥塞控制、流量控制等机制都建⽴在“TCP packet”前提下。
对以太⽹MTU这类问题,可假设TCP对下总是能处理好,⽽不会因为某个包太⼤就傻乎乎丢数据。因此不过多讨论。
2 向上
所以,题目中的"包"只能是面向上层或说是"面向用户的包”。可简单理解为类似把HTTP1.0里的request/response (或叫message) 的设计直接放到TCP直接实现。这样发送方就很方便写入一个应用的"包",读取方就能读取整个这样一个应用的包。
对一般的需求看上去很合适。并且看起来这个设计也不会太破坏TCP对下层的抽象。只要在发送和接收buffer里增加标记每个用户包的开始、结束位置,到了地方就给上层recv返回即可。
3 有麻烦了
但这样有问题:必须给这个包设计⼀个最⼤⼤⼩,否则TCP不知道要分配多⼤内存。若⽤户包>接收buffer,很难设计⼀个合理接⼝⾏为,因为这时recv:
- 是返回⼀部分数据
- 还是返回错误?
- 还是必须逼发送⽅⼿⼯拆包?
- ⼜或⼀个上层应⽤在启动时要告诉TCP⾃⼰需要有多⼤的包。但上层应⽤可能并不能提前知道到底⼀个包多⼤
如上层要上传⼀个⽂件,⽂件多⼤⽆法提前知晓。所以,为了可⽤,上层必然根据⾃⼰的业务需求⼀定要实现⼀个⾃⼰拆包的协议。⽽⼀旦上层实现了这协议,那TCP层拆包就完全丧失意义。
3.1 UDP咋做的?
UDP就是定义最⼤的包⼤⼩。因此使⽤UDP的应⽤层⼀般会设计为“绝对不会超 过这个⼤⼩”;或者如果要超了就改成TCP。⼀个典型的例⼦就是DNS的协议设计。这种形式应 付⼀下简单的场景还凑合,但对于⼀般业务开发是绝对不可接受的。
⽽某些应⽤场景,⾃身就是数据流。若TCP设计为⾯向⽤户包,这些场景就须引⼊“流转包 ”。技术上也可⾏。或者TCP同时提供⾯向包、⾯向流两种语义,TCP需要定义“包连接”和“流连接”两种连接,各传各?这⼤⼤增加协议设计和使⽤复杂度。
更郁闷的,若业务场景需要进⾏有序数据传输,开头是个包,包完了是流,⼜咋处理?对应⽤层协议还好,可针对场景定制设计;问题是TCP定位是通⽤传输协议,若搞这种定制设计就没法⽤。
4 包拆分
更麻烦其实是不同应⽤场景,包拆分机制不⼀。常⻅⽅式提供⼀个字段明确标记包的字节数。
有的根据请求中的特殊字符区分是否要拆包。⽽特殊字符⼜涉及编码和escape之类问题。⽽这些字节流⼜可能被加密,得到结果还跟密钥相关。这些数据到TCP这⾥就就没法搞了。
合理做法还是应⽤层先拆,拆好了加密,加完密再传输。
5 应⽤层拆包
因此发现,⽆论TCP层咋折腾,都不太可能绕开“应⽤层拆包 ”。所以,也许更好处理⽅式:完全⼲掉“流式协议”这层的抽象,但保留底层包的保序/重传/流控/拥塞控制等机制⸺QUIC就是这样。但对⼏⼗年前的⽹络协议设计,TCP流式协议是⾜够好选择。在众多协议下能活下来并得到⼴泛应⽤⾜以证明。
6 总结
BTW,的确存在⽀持message的传输层协议SCTP,但过于复杂得不到⼴泛⽀持(Windows压根不⽀持),濒临灭绝。
⽽那些适合⽤“⽤户包”来实现的功能(如微服务的RPC ),可直接⽤HTTP做传输层。即把⼀个本设计为应⽤层的协议,保留其Header,编码,拆包等基本能⼒;同时将如Cache协商,⽤不到的编码格式等扔掉不实现。在不对基础协议做更改的前提下就能run。
另外⼀个选项⽤ZeroMQ,天然封装Req/Resp能⼒,可在此基础构造应⽤层协议。钱多⼤⼚,⾃⼰手写协议满足业务也完全可。