V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amaranthf
V2EX  ›  程序员

TCP 连接中,如何判断一段数据的起始和结束?

  •  
  •   amaranthf ·
    regomne · 2016-01-08 22:42:37 +08:00 · 8011 次点击
    这是一个创建于 3001 天前的主题,其中的信息可能已经有所发展或是发生改变。
    最近用 nodejs 做一个代理,这个代理比较特殊的一点是,服务器和客户端之间只能建立一个 tcp 连接(因为建立连接的成本很高),而客户端上使用代理的程序(如浏览器)可能会尝试向本地代理 127.0.0.1:XXX 建立多个连接。

    我考虑的一个解决方案是,客户端收到数据之后,在数据前面加上一个字段表示端口,发给服务器,服务器据此处理、收到数据后,也在数据前面加上端口并返回给客户端。

    那么问题来了:
    client.on('data',data=>{...});
    我需要知道这段 data 是完整的,否则没办法提取发送时附带在“头部”的内容。
    那么是不是说,我远端 write 一个 buffer 进来,这里的 data 就一定是收到完整的那一段 Buffer 呢?如果不是的话,一般是怎么判断我收到的数据已经完整了呢?
    19 条回复    2016-01-12 23:04:48 +08:00
    Strikeactor
        1
    Strikeactor  
       2016-01-08 22:45:53 +08:00
    用 HTTP 协议吧
    jasontse
        2
    jasontse  
       2016-01-08 23:01:44 +08:00 via Android
    wikipedia 写的很明白了,包结构部分
    https://en.wikipedia.org/wiki/Transmission_Control_Protocol
    cmingxu
        3
    cmingxu  
       2016-01-08 23:12:52 +08:00   ❤️ 1
    tcp 连接是 stream , 所以没有头和尾的概念, 一种做法是传输的包前加上此包的长度信息。另外就是特殊的结束符(得注意传输内容和结束符一致的情况)。
    ryd994
        4
    ryd994  
       2016-01-08 23:16:17 +08:00   ❤️ 1
    带长度,每个数据段的开头先声明长度
    你会有这个问题说明你对 TCP 的理解不对。对应用程序, TCP 连接就是无限长的数据流,直到关闭,网络上的数据包程序是看不见的。
    而且你确定建立连接的成本有那么高?比 mutiplex 这些数据所需的各种头信息还要高?有没有考虑 fastopen ?有没有考虑 udp ?
    easing
        5
    easing  
       2016-01-08 23:37:14 +08:00 via Android   ❤️ 1
    这是上层协议的事情,传输层不负责内容数据逻辑上的起始。你可以模拟 http 协议,要么携带长度信息,要么规定特殊字符作为起始标志。
    amaranthf
        6
    amaranthf  
    OP
       2016-01-08 23:57:13 +08:00
    @ryd994 因为我的代理服务器是在内网,而且不能开端口映射,所以必须由服务器主动连接到客户端,为此需要使用另一方的服务器进行中转才能建立连接。
    znoodl
        7
    znoodl  
       2016-01-09 00:07:20 +08:00 via iPhone
    定义协议,比如最简单的前 4 个字节做长度,后面的长度是数据
    mengskysama
        8
    mengskysama  
       2016-01-09 00:12:27 +08:00
    自己实现好累的,你看这样行不行,在内网代理服务器搭一个代理,然后内网代理服务器通过 ssh 连接到公网服务器,并把自己的 S5 代理端口 ssh 映射到公网服务器上,公网服务器用 iptables 做个转发,这样一行代码都不用写。
    amaranthf
        9
    amaranthf  
    OP
       2016-01-09 00:19:17 +08:00
    @mengskysama 就是利用公网服务器做一个隧道呗,理论上是可以啦,不过我搞这个东西本来就是为了解决延迟问题,那台内网机器所在的网络非常快,我这儿 ping 过去只要 10ms ,再到外面的出口也是 10ms ,而通过公网服务器这么来回一中转……至少就 100 了……
    mengskysama
        10
    mengskysama  
       2016-01-09 00:26:18 +08:00
    @amaranthf 可能没明白你的要求,你意思是你(公网),那台服务器(内网),然后通过自己和那台服务器建立一个连接来访问公网资源?
    amaranthf
        11
    amaranthf  
    OP
       2016-01-09 00:33:17 +08:00
    @mengskysama 是这样……
    我的机器 A ,内网机器 B
    A 处于公网(实际上也是内网,不过路由这边可以做端口映射);
    B 处于内网,且不能与公网直接通讯。
    B 可以通过代理 P1 连接“外面的网络”,另外也可以通过代理 P2 连接正常的公网。 P1 和 P2 都属于 B 所在的内网,所以延迟可以忽略。另外最最重要的一点就是, P1 是开白名单的……而且速度稳稳碾压各种国外 vps 的直连……
    就是这么个复杂的情况……
    amaranthf
        12
    amaranthf  
    OP
       2016-01-09 00:40:23 +08:00
    @mengskysama 我的设计是这样的,需要利用公网服务器 C 。
    1. B 通过 P2 与 C 建立长连接
    2. A 需要连接的时候,通知 C 自己的 ip 和端口
    3. C 通知 B
    4. B 通过 P2 连接 A
    5. A-P2-B-P1-外面 建立这样的链路
    mengskysama
        13
    mengskysama  
       2016-01-09 00:43:46 +08:00
    @amaranthf
    A 映射一个端口 2222
    在 B 上开一个代理服务器听一个端口 0.0.0.0:3333
    B 通过 P1 建立 ssh 连接到 A 的 2222 ,并且映射端口 3333 到 A 的 3334
    这样。
    mengskysama
        14
    mengskysama  
       2016-01-09 00:45:56 +08:00
    @amaranthf 不需要 C 了,你直接 A 和 B 长连不就行了有个东西叫 Autossh 。。 IP 用 DDNS ,端口固定。
    amaranthf
        15
    amaranthf  
    OP
       2016-01-09 00:48:20 +08:00
    @mengskysama 呃,我是考虑做一个应用范围广一些的,比如我写个手机 app 就也能利用这套代理了……不过 ssh 隧道方面的东西确实没有仔细了解过,我查查相关资料,看怎么用那个简化一下吧,多谢啦
    r00tt
        16
    r00tt  
       2016-01-09 11:21:29 +08:00
    tcp 流式的,没有包的概念,这种情况一般都是自定义数据包,可以设计定长,也可以设计非定长( Tag + Length + Body + CheckSum 类似这种拉~),可以用 flatbuffers ,protobuf 来实现比较好
    hao123yinlong
        17
    hao123yinlong  
       2016-01-09 15:44:01 +08:00
    /**
    * 半包处理解码器
    *
    * 两端通信单向一个完整的数据包由 Length , Type , Message 三部分组成,固定占用 4 个字节的 Length 声明了 Message
    * 的字节长度, Type 占用一个字节, Length 不包含 Type 所占一个字节 Message 是一个接口调用所需参数 protobuf 对象编码后的字节数组
    *
    * 当 Lenght int 值为 0 时,表示是心跳数据包,向客户端回复心跳数据包 ,心跳数据包 5 个字节
    *
    * | Length | Type | Message | | (4 bytes) | (1 bytes) | (n bytes) |
    *
    * @author yintengfei
    *
    */
    shyling
        18
    shyling  
       2016-01-09 20:56:46 +08:00
    固定的结束符就可以了吧。。
    每次 on('data')把 chunk 插到 Buffer[]后面,然后从前面开始读到结束符,读不到就是没结束。。
    MiskoLee
        19
    MiskoLee  
       2016-01-12 23:04:48 +08:00   ❤️ 2
    这个问题,我们可以提炼一下。

    1. 什么叫结束?

    我们举个例子,对于手机来说,并不存在结束对话这个状态。只有接通,未接通,通话中,挂断等状态。因此,此时,手机网络无法获知与处理对话结束这个状态。那么,现实中,它是怎么工作的?

    A : 你好!
    B :你好!
    A :....
    B :.....
    A :先这样吧,下次再聊!( A 尝试发起结束对话)
    B : Bye !( B 确认结束通话)

    然后 A , B 互相挂断电话。


    同理, TCP 中并不存在所谓的数据接收完毕这种状态。因此,这种状态是我们人为附加上的,所以,需要我们人为的来处理这件事。

    首先,我们来看看 HTTP 是怎么工作的。

    在 Http 标准分为固定格式的 Header 与任意格式的 Body 构成。
    在 header 中有定义 Content-Length 字段。

    Http 的定义中,其实就包含了上述各位描述的:特定 chunk 符, DataLength 等技术手段。

    首先是 Http Header

    Http Header 就是一组固定格式的文本,内部通过特定符号完成断句。

    从 Header 中读取到 Content-Length 字段,读取等长的 Body ,然后关闭 TCP 连接。

    当 Content-Length 未读取成功的时候,则等待服务端断开连接。

    我们在使用浏览器下载稍微大型的文件的时候,通常会遇到两种情况

    1. 100KB/12.3M 预计 2 分钟后下载完毕
    2. 已接收 100KB

    原因,我们可以思考。

    现在,我们再来看一个经典的 TCP 协议。 MYSQL 协议。

    MYSQL 在 TCP 层,把数据封装成一个一个的 packet 。每个 packet 的信息非常少,我们可以粗略的理解为

    |DataLegnth|DataBody|

    读取每个 packet 的时候,首先读取 DataLength,接着读取 DataLength 长度的 DataBody.


    综上所述。

    在 TCP 中,数据传输必须要有一个协议。这个协议至少要有一个信息,就是定义一个完整数据的长度。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3951 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 10:23 · PVG 18:23 · LAX 03:23 · JFK 06:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.