TCP 数据”流“

在发送端,当我们调用 send 函数完成数据“发送”以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中,至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能假设每次 send 调用发送的数据,都会作为一个整体完整地被发送出去。

假设发送端陆续调用 send 函数先后发送 network 和 program 报文,那么实际的发送很有可能是这个样子:

  1. 一次性将 network 和 program 在一个 TCP 分组中发送出去
    …xxxnetworkprogramxxx…
  2. program 的部分随 network 在一个 TCP 分组中发送出去
  3. network 的一部分随 TCP 分组被发送出去,另一部分和 program 一起随另一个 TCP 分组发送出去
    …xxxxxxxxxxxnet

我们不知道 network 和 program 这两个报文是如何进行 TCP 分组传输的

接收端

  1. 这里 netwrok 和 program 的顺序肯定是会保持的,也就是说,先调用 send 函数发送的字节,总在后调用 send 函数发送字节的前面,这个是由 TCP 严格保证的;
  2. 如果发送过程中有 TCP 分组丢失,但是其后续分组陆续到达,那么 TCP 协议栈会缓存后续分组,直到前面丢失的分组到达,最终,形成可以被应用程序读取的数据流。
报文读取和解析

报文是以字节流的形式呈现给应用程序的

报文格式实际上定义了字节的组织形式,发送端和接收端都按照统一的报文格式进行数据传输和解析,这样就可以保证彼此能够完成交流。

报文格式最重要的是如何确定报文的边界

  • 发送端把要发送的报文长度预先通过报文告知给接收端;
  • 通过一些特殊的字符来进行边界的划分。
显式编码报文长度

1.报文格式
在这里插入图片描述

  • 先 4 个字节大小的消息长度,其目的是将真正发送的字节流的大小显式通过报文告知接收端,接下来是 4 个字节大小的消息类型,而真正需要发送的数据则紧随其后。

2.发送报文
htonl 函数将字节大小转化为了网络字节顺序,实际发送的字节流大小:消息长度 4 字节+消息类型 4 字节+标准输入的字符串大小。

3.解析报文:程序
循环处理字节流,调用 read_message 函数进行报文解析工作。

4.解析报文:readn 函数
读取报文预设大小的字节,readn 调用会一直循环,尝试读取预设大小的字节,如果接收缓冲区数据空,readn 函数会阻塞在那里,直到有数据到达。

5.解析报文: read_message 函数


size_t read_message(int fd, char *buffer, size_t length) {
    u_int32_t msg_length;
    u_int32_t msg_type;
    int rc;

    rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;
    msg_length = ntohl(msg_length);

    rc = readn(fd, (char *) &msg_type, sizeof(msg_type));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;

    if (msg_length > length) {
        return -1;
    }

    rc = readn(fd, buffer, msg_length);
    if (rc != msg_length)
        return rc < 0 ? -1 : 0;
    return rc;
}
  • 第 6 行通过调用 readn 函数获取 4 个字节的消息长度数据
  • 第 11 行通过调用 readn 函数获取 4 个字节的消息类型数据
  • 15 行判断消息的长度是不是太大,如果大到本地缓冲区不能容纳,则直接返回错误
  • 19 行调用 readn 一次性读取已知长度的消息体。
特殊字符作为边界

在这里插入图片描述


int read_line(int fd, char *buf, int size) {
    int i = 0;
    char c = '\0';
    int n;

    while ((i < size - 1) && (c != '\n')) {
        n = recv(fd, &c, 1, 0);
        if (n > 0) {
            if (c == '\r') {
                n = recv(fd, &c, 1, MSG_PEEK);
                if ((n > 0) && (c == '\n'))
                    recv(fd, &c, 1, 0);
                else
                    c = '\n';
            }
            buf[i] = c;
            i++;
        } else
            c = '\n';
    }
    buf[i] = '\0';

    return (i);
}
  • read_line 函数就是在尝试读取一行数据,也就是读到回车符\r,或者读到回车换行符\r\n为止。这个函数每次尝试读取一个字节
  • 第 9 行如果读到了回车符\r
  • 11 行的“观察”下看有没有换行符,如果有就在第 12 行读取这个换行符;如果没有读到回车符,就在第 16-17 行将字符放到缓冲区,并移动指针。
总结

TCP 数据流特性决定了字节流本身是没有边界的,一般我们通过显式编码报文长度的方式,以及选取特殊字符区分报文边界的方式来进行报文格式的设计。而对报文解析的工作就是要在知道报文格式的情况下,有效地对报文信息进行还原。

本文核心观点:

1:TCP 数据是流式的——0/1的组合——符合规范的直流电——符合规范的交流电
2:在发送端,当我们调用 send 函数完成数据“发送”以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中,至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能假设每次 send 调用发送的数据,都会作为一个整体完整地被发送出去。
3:接收端缓冲区保留了没有被取走的数据,随着应用程序不断从接收端缓冲区读出数据,接收端缓冲区就可以容纳更多新的数据。如果我们使用 recv 从接收端缓冲区读取数据,发送端缓冲区的数据是以字节流的方式存在的,无论发送端如何构造 TCP 分组,接收端最终受到的字节流总是有序的完整的,这些都有TCP严格保证。
4:数据存储有大小端之别,只要统一就行,网络传输字节序使用大端
5:都是0/1咋区分数据的边界,常用方式有两种,一是标明字节长度,二是使用特殊分隔符

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值