手把手写C++服务器(18):TCP紧急传输的方法——带外数据 (原理与代码示例)

Posted 沉迷单车的追风少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器(18):TCP紧急传输的方法——带外数据 (原理与代码示例)相关的知识,希望对你有一定的参考价值。

前言:TCP的三次握手四次挥手的面向连接的传输特定,本质上保证了传输的可靠性,此外,还有字节编号机制、滑动窗口机制、超时重传机制、选择性确认机制等,最大程度上保证了其可靠性传输。但是凡事利弊相依,福祸相生,保证可靠性的同时,必然牺牲了其他的特性,紧急数据传输就是其中之一。如果需要传输紧急数据,需要进行什么样的步骤?要在三次握手四次挥手、字节编号机制、滑动窗口机制、超时重传机制、选择性确认机制等等这些框架之下进行,是不是会耽误了紧急的实时性和优先性呢?

目录

从TCP固定头结构聊起

紧急数据的必要性

带外数据概念

TCP带外数据传输流程

发送端

接收端

异常处理

socket识别带外数据

代码示例

发送端

接收端

参考


从TCP固定头结构聊起

TCP固定头结构如下图所示:

本文关注的重点是16位紧急指针部分,其他的部分详解可见:https://xduwq.blog.csdn.net/article/details/105891603

紧急指针,一共16位,是一个正偏移量,它和序号字段的值相加表示最后一个紧急数据的下一字节的序号,用于发送端向接收端发送紧急数据的方法。

那么问题来了,此处的紧急指针究竟要在什么时候使用呢?下面便是本文的重点!

紧急数据的必要性

TCP的三次握手四次挥手的面向连接的传输特定,本质上保证了传输的可靠性,此外,还有字节编号机制、滑动窗口机制、超时重传机制、选择性确认机制等,最大程度上保证了其可靠性传输。但是凡事利弊相依,福祸相生,保证可靠性的同时,必然牺牲了其他的特性,紧急数据传输就是其中之一。如果需要传输紧急数据,需要进行什么样的步骤?要在三次握手四次挥手、字节编号机制、滑动窗口机制、超时重传机制、选择性确认机制等等这些框架之下进行,是不是会耽误了紧急的实时性和优先性呢?所以,带外数据就出场了!

带外数据概念

Out Of Band,是和带内数据(普通数据)相对应的概念,用于迅速通告对方的重要事件。带外数据比带内数据有更高的优先级,他会被立即发送,不管发送缓冲区是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中,仅有Telnet、FTP等非活跃程序使用。

注意:TCP和UDP都没有带外数据,但是上面说过TCP固定头结构当中紧急指针和紧急指针标识位,为应用层提供了一种紧急方式。所以一般将TCP紧急数据称为带外数据

TCP带外数据传输流程

发送端

将待发送的TCP报文的头部设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一个字节(进一步减去当前TCP报文段值得得到的紧急偏移值),所以发送端一次发送的多字节带外数据中只有最后一个字节被当作带外数据,举个例子,想发送带外数据“abcdef”,只有最后一个字节的数据“f”才会被当作为带外数据,其他数据都会被当作普通数据。

如果TCP多个报文段发送缓冲区中的内容,每个TCP报文段都会被设置为URG标志,并且紧急指针会被指向同一个位置。但是只有一个TCP报文段指针携带那个“最后一个字节的带外数据”。

即使发送端TCP因流量控制而暂停发送数据(接受缓冲区的套接字接受缓冲区已满,导致其TCP向发送端通告了一个值为0 的窗口),紧急通知照样不伴随任何数据的发送。也就是说:即使数据的流动会因为TCP的流量控制而停止,紧急通知却总是无障碍的发送到对端TCP

接收端

只有接收到紧急指针标志的时候,接收端才会检查紧急指针,并根据紧急指针所指的位置来确定带外数据的位置,并将其读入一个特殊的缓存中,这个缓存只有1个字节,被称为带外缓存

当由紧急指针指向的实际数据字节到达接受端TCP时,数据字节会有两个存储地区:一个是和普通数据一样的在线留存,另外一个是独立的单字节带外缓冲区,接受进程从这个单字节带外缓冲区读入数据的唯一方法是指定MSG_OOB调用recvrecvfromrecvmsg。如果放在和普通数据一起的带内区域,接受进程就得通过检查该连接的带外标记OOB来获悉何时访问带这个数据字节。两个区域的使用通过套接字选项SO_OOBLINE来使用,默认情况下将带外数据字节放入独立的单字节带外缓冲区内。

异常处理

  1. 如果接受进程请求读入数据(通过MSG_OOB标志),但是对端并没有发送任何带外数据,读入操作将返回EINVAL

  2. 在接受进程已被告知对端发送了一个带外字节(SIGURGselect)的前提下,如果接受进程试图读入该字节,但是该字节尚未到达,读入操作返回EWOULDBLOCK。接受进程此时做的就是从缓冲区中读入数据,腾出空间,以允许对端TCP发送出那个带外字节。

  3. 如果接受进程试图多次读入同一个带外字节,读入操作返回EINVAL

  4. 如果开启了SO_OOBINLINE套接字选项,接受进程如果还是通过MSG_OOB读入带外数据,读入操作将返回EINVAL

socket识别带外数据

Linux内核检测到TCP紧急标志的时候,将通知应用程序带有带外数据需要接收。内核通知的常见方式是:I/O复用产生异常事件和SIGRG信号,但是即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置。

具体的方法如下见注释:

#include <sys/stocket.h>
// 判断sockfd是否处于带外标记,即下一个读取到的数据是否是带外数据
// 如果是则sockatmark返回1,不是则返回0
int sockatmark(int sockfd);

代码示例

在TCP数据发送和接收的API当中,flags提供了标志位MSG_OOB,用于发送和接收紧急数据,下面就利用上述的知识点写一个带我数据发送和接收的demo。

发送端

#include <sys/socket.h>
#include <assert.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

int main(int argc, char* argv[]) {

    if (argc != 3) {
        cout << "input parameter error!" << endl;
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(sockfd >=0);
    if (connect(sockfd, (struct sockaddr*)&
                server_address, sizeof(server_address)) < 0) {
        cout << "connect fail!" << endl;
    } else {
        const char* oob_data = "this is oob data";
        const char* normal_data = "this is normal data";
        send(sockfd, normal_data, strlen(normal_data), 0);
        send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
    }

    close(sockfd);
    return 0;
}

接收端

#include <sys/socket.h>
#include <assert.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

using namespace std;

const int BUF_SIZE = 1024;

int main (int argc, char* argv[]) {

    if (argc <= 2) {
        cout << "input parameter error!" << endl;
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >=0);

    int ret = bind(sock, (struct sockaddr*)& address, sizeof(address));
    assert(ret >= 0);

    struct sockaddr_in client;
    socklen_t client_addrlen = sizeof(client);
    int confd = accept(sock, (struct sockaddr*)& client, &client_addrlen);
    if (confd < 0) {
        cout << "error! " << errno << endl;
    } else {
        char buffer[BUF_SIZE];

        memset(buffer, '\\0', BUF_SIZE);
        ret = recv(confd, buffer, BUF_SIZE - 1, 0);
        cout << "normal data byte is " << ret << "buffer is " << buffer <<endl;

        memset(buffer, '\\0', BUF_SIZE);
        ret = recv(confd, buffer, BUF_SIZE - 1, MSG_OOB);
        cout << "oob data byte is " << ret << "buffer is " << buffer <<endl;
    }

    close(sock);
    return 0;
}

参考

以上是关于手把手写C++服务器(18):TCP紧急传输的方法——带外数据 (原理与代码示例)的主要内容,如果未能解决你的问题,请参考以下文章

手把手写C++服务器(19):序列化数据网络传输解决方案

手把手写C++服务器(30):手撕代码——基于TCP/IP的抛弃服务discard

手把手写C++服务器(17):自测!TCP协议面试经典十连问

手把手写C++服务器(17):自测!TCP协议面试经典十连问

手把手写C++服务器(24):socket响应一般框架TCP修改缓冲区内核监听listen最大长度

手把手写C++服务器:网络编程常见误区