TCP 是一种流式协议

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

如果考虑实际网络传输过程中的各种影响,假设发送端陆续调用send函数 先后发送 network 和 program 报文,实际的发送情况有多种可能。
(1) 第一种情况:一次性将 network 和 program 在一个 TCP 分组中发送出去。
...xxxnetworkprogramxxx...
(2) 第二种情况:program 的一部分随 network 在一个 TCP 分组中发送出去,另一部分随另一个 TCP 分组发送出去。
...xxxxxnetworkpro
gramxxxxxxxxxx...
(3) 第三种情况:network 的一部分随一个 TCP 分组被发送出去,另一部分和 program 一起随另一个 TCP 分组发送出去。
...xxxxxxxxxxxnet
workprogramxxx...
注:实际上类似的组合可以枚举出无数种。不管是哪一种,核心的问题就是,无法知道 network 和 program 这两个报文是如何进行 TCP 分组传输的。换言之,在发送数据的时候,不应该假设 “数据流和 TCP 分组是一种映射关系”

如果使用 recv 从接收端缓冲区读取数据,发送端缓冲区的数据是 以字节流的方式存在的,无论发送端如何构造 TCP 分组,接收端最终收到的字节流总是一致的。
xxxxxxxxxxxxxxxxxnetworkprogramxxxxxxxxxxxx
接收端字节流,有两点需要注意:
  1. 有序:这里 netwrok 和 program 的顺序肯定是会保持的。也就是说,先调用 send 函数发送的字节,总在后调用 send 函数发送字节的前面,这个是由 TCP 严格保证的;
  2. 完整:如果发送过程中有 TCP 分组丢失,但是其后续分组陆续到达,那么 TCP 协议栈会缓存后续分组,直到前面丢失的分组到达,最终,形成可以被应用程序读取的数据流。

网络字节排序

如果需要传输数字,比如 0x0201,对应的二进制为 0000001000000001,那么两个字节的数据到底是先传 0x01,还是相反?

在计算机发展的历史上,对于如何存储这个数据没有形成标准。不同的系统就会有两种存法,一种是将 0x02 高字节存放在起始地址,这个叫做大端字节序(Big-Endian)。另一种相反,将 0x01 低字节存放在起始地址,这个叫做小端字节序(Little-Endian)。
但是在网络传输中,必须保证双方都用同一种标准来表达,网络协议使用的是 大端字节序
uint16_t htons (uint16_t hostshort)
uint16_t ntohs (uint16_t netshort)
uint32_t htonl (uint32_t hostlong)
uint32_t ntohl (uint32_t netlong)

// 函数中的 n 代表的就是 network,h 代表的是 host,s 表示的是 short,l 表示的是 long,分别表示 16 位和 32 位的整数。
这些函数可以帮助 主机(host)和网络(network)格式间的 灵活转换。

报文读取和解析

报文是以字节流的形式 呈现给应用程序的,那么随之而来的一个问题就是,应用程序如何解读字节流呢?
只有知道了报文格式,接收端才能针对性地 进行报文读取和解析工作。报文格式最重要的是 如何确定报文的边界
常见的报文格式有两种方法:
  1. 发送报文长度:发送端把 要发送的报文的长度 预先通过报文告知给接收端;
  2. 指定特殊字符作为报文边界:是通过一些 特殊的字符 来进行边界的划分;

显式编码报文长度

报文格式:

由图可以看出,这个报文的格式很简单。首先 4 个字节大小的消息长度,其目的是将真正发送的字节流的大小 显式通过报文告知接收端,接下来是 4 个字节大小的消息类型,而真正需要发送的数据则紧随其后。

发送报文:
int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: tcpclient <IPaddress>");
    }

    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    struct {
        u_int32_t message_length;
        u_int32_t message_type;
        char buf[128];
    } message;

    int n;

    while (fgets(message.buf, sizeof(message.buf), stdin) != NULL) {
        n = strlen(message.buf);
        message.message_length = htonl(n);
        message.message_type = 1;
        if (send(socket_fd, (char *) &message, sizeof(message.message_length) + sizeof(message.message_type) + n, 0) <
            0)
            error(1, errno, "send failure");

    }
    exit(0);
}
1-20 行:  创建套接字和地址,建立连接的过程;
21-25 行:图示的报文格式转化为结构体;
29-37 行:从标准输入读入数据,分别对消息长度、类型进行了初始化,这里使用了 htonl 函数 将字节大小转化为了网络字节顺序,这一点很重要。(最后可以看到 23 行实际发送的字节流大小为:消息长度 4 字节 + 消息类型 4 字节 + 标准输入的字符串大小)

解析报文:

static int count;

static void sig_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}


int main(int argc, char **argv) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    char buf[128];
    count = 0;

    while (1) {
        int n = read_message(connfd, buf, sizeof(buf));
        if (n < 0) {
            error(1, errno, "error read message");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }
        buf[n] = 0;
        printf("received %d bytes: %s\n", n, buf);
        count++;
    }

    exit(0);

}
1-41 行:创建套接字,等待连接建立。
45-55 行:循环处理字节流,调用 read_message 函数进行报文解析工作,并把报文的主体通过标准输出打印出来。