TCP 带外数据传输

带外数据说明

TCP 的带外数据可传输一字节内容,实际上带外数据和其他数据是一起发送,一起接收。区别在于:

对于发送端:

发送带外数据,会将当前发送缓冲区待发送的 TCP 报文 header 设置 flag 的 URG 标志和紧急指针 Urgent pointer 的值,仅仅如此而已。带外数据的位置为该次发送带外数据调用的最后一个字节。

对于接收端:

接收端,则是读取接口的行为的差异。默认情况下,带外数据需要专用的 socket API 才能读取,recv、recvmfg、recvfrom。当然,发送也需要send、sendto、sendmsg才行。

带外数据的通知方式为发送 SIGURG 信号,首先需要设置文件描述符所属的进程,并注册 SIGURG 信号的处理函数。

代码与输出

serv.c 代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

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

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[3000] = {0};

    int i = 0;
    for(; i < 3000; ++i)
    {
        message[i] = '6';
    }

    message[2999] = 'x';

    char msg2[]="yyyyyyyyyz";    //10 elements

    char temp[] = "oooooooooo";

    printf("sizeof msg2 : %d\n", sizeof(msg2));

    if(argc!=2){
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    int opt = 1;

    // sockfd为需要端口复用的套接字
    setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));




    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
        error_handling("bind() error");

    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");

    clnt_addr_size=sizeof(clnt_addr);
    clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
    if(clnt_sock==-1)
        error_handling("accept() error");

    send(clnt_sock, message, sizeof(message), MSG_OOB);  //包括末尾的\0
    //send(clnt_sock, message, sizeof(message), 0);

    //sleep(1);
    printf("second write\n");

    //write(clnt_sock, temp, sizeof(temp) - 1);
    //sleep(1);


    //write(clnt_sock, msg2, sizeof(msg2));
    send(clnt_sock, msg2, sizeof(msg2) - 1, MSG_OOB);
    //send(clnt_sock, msg2, sizeof(msg2) - 1, 0);   //不包括末尾的\0


    printf("start to sleep\n");
    sleep(5);
    printf("awake\n");
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

client.c 代码


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>


void error_handling(char *message);

int sock;
int oob_flag = 1;

void sig_urg(int signo)
{
    //sleep(5);

    if(oob_flag == 1)
    {
        int     n;
        char    buff[100];
        if(signo != 0)
        {
            printf("\nSIGURG received %d\n\n", signo);
        }
        n = recv(sock, buff, sizeof(buff) - 1, MSG_OOB);
        if(n == 0)
        {
            printf("empty oob\n");
        }
        else if(n < 0)
        {
            if(errno == EAGAIN)
            {
                printf("oob data has not arrived yet\n");
            }
            else
            {
                 perror("");
            }
        }
        else
        {
            buff[n] = 0;                /* null terminate */
            {
                printf("read %d OOB byte: %s\n", n, buff);
            }

            oob_flag = 0;
        }
    }
}

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

    struct sockaddr_in serv_addr;
    char message[4000] = {0};
    int str_len;

    if(argc!=3){
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock=socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    int nrecvBuf = 1280;//设置接收缓冲区
    int set_ret = setsockopt(sock, SOL_SOCKET, SO_RCVBUF,(const char*)&nrecvBuf, sizeof(int));
    if(set_ret < 0)
    {
        //perror("");
    }
    printf("set recvbuf %d %d\n", set_ret, nrecvBuf);


    //带外数据普通数据接收
    //int opt = 1;
    //setsockopt(sock, SOL_SOCKET, SO_OOBINLINE, (const void *)&opt, sizeof(opt));


    fcntl(sock, F_SETOWN, getpid());
    signal(SIGURG, sig_urg);

    int type_size = 0;
    int get_ret = getsockopt(sock, SOL_SOCKET, SO_RCVBUF,(char*)&nrecvBuf, (__socklen_t*)&type_size);
    printf("get recvbuf %d %d\n", get_ret, nrecvBuf);


    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
        error_handling("connect() error!");



    do
    {
        str_len=read(sock, message, sizeof(message)-1);
        if(str_len != 0)
        {
            message[str_len] = 0;
            printf("read %s -- %d bytes\n", message, str_len);
        }
        sig_urg(0);
        sleep(3);
    }while(str_len != 0);



    /*
    int flags;
    if((flags = fcntl(sock,F_GETFL,0))==-1)
    {
            perror("fcntl F_GETFL fail:");
            exit(1);
    }
    flags |= O_NONBLOCK;
    if(fcntl(sock,F_SETFL,flags)==-1)
    {
            perror("fcntl F_SETFL fail:");
            exit(1);
    }


    str_len=read(sock, message, sizeof(message)-1);
    printf("second read : %d\n", str_len);



    if(str_len==-1)
    {
        if(errno == EAGAIN)
        {
            printf("no data\n");
        }
        else
        {
            perror("");
        }

    }*/


    //printf("Message from server: %s %d\n", message, str_len);
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

设置接收缓冲区的大小,使得发送端一次写入的数据无法通过一个数据包发送到接收端。 tcpdump 抓包之后,使用 wireshark 查看。

扫描二维码关注公众号,回复: 3086232 查看本文章

 发送端一次将 3000 字节写入内核缓冲区,而数据包最大只能传输 640 字节。

 SO_RCVBUF
              Sets or gets the maximum socket receive buffer in bytes.  The kernel doubles this value
              (to allow space for bookkeeping
              overhead) when it is set using setsockopt(2), and this doubled
              value is returned by getsockopt(2).  The default value is set
              by the /proc/sys/net/core/rmem_default file, and the maximum
              allowed value is set by the /proc/sys/net/core/rmem_max file.
              The minimum (doubled) value for this option is 256.

实际数据包的大小为接收缓冲区大小的一半,当然不能超过最大报文长度 MSS.抓包显示的通告窗口 Win=1280.

中间客户端 recv 完之后主动休眠,导致内核接收缓冲区满,内核协议栈直接给对端发送通告窗口 Win=0. 在带外数据真正的传输前,即使是payload 为 0 的单纯 ACK 数据包,也是含有 UGR 标记的。所以说,对于发送端,发送带外数据只有 TCP header 的差别而已。

关于 wireshark TCP windows 的提示

1、TCP Window Full  表示发送的该数据包大小刚好会使接收端的接收缓冲区满。

2、TCP ZeroWindows 表示通知对端,本端的接收缓冲区满,请对端停止发送。

3、TCP Windows Update 表示本端的通告窗口大小变化,上图中是接收缓冲区空,通知对端当前的接收缓冲区大小为 1280,可以继续发送。

输出结果分析

发送端的发送缓冲区足够大,首次 3000 字节全部写入发送缓冲区,包括第二次写入的10字节,一共 640 * 4 + 450 = 3010 个字节分 5 个数据包发送。

1、从第一个数据包开始,就包含 URG 标志,而且只有第一个数据包触发了信号。(从包含带外数据的内容写入到内核缓冲区,从内核缓冲区发送的数据包开始,直到代码带外数据发送出去之前这期间,都会一直包含 URG 标志)。

2、缺省的 recv 是阻塞的,但是设置 MSG_OOB 标志读取带外数据时,是非阻塞的。这也是合理的,因为在包含带外数据的数据包发送之前,就已经开始通知接收端进入紧急状态,如果此前读取代码数据是阻塞的,一旦缓冲区满,无法接受新的数据,这将导致接收端一直阻塞下去。

3、读取时,遇到带外数据会返回,即使当前缓冲区还有数据。最后 10 字节还需另外一次读取才获取。

通常读取会在这几种情况返回:

a、遇到PUH标记。调用写入接口写入的数据的最后一个数据包,一般会包含PUH标志,数据包 20.

b、遇到带外数据。

c、缓冲区满,一次读空。

d、读够指定的字节量。

带外数据与普通数据一同读取

放开注释,设置带外数据的读取行为和普通数据相同。

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_OOBINLINE, (const void *)&opt, sizeof(opt));

 

其实还是有细微差别,读取到带外数据前返回,带外数据和剩余的普通数据一起返回。

带外数据覆盖

在两次发送之间加上延迟,放开两次发送之间的休眠的注释,并插入一个不含带外数据的数据包。

    sleep(1);
    printf("second write\n");

    write(clnt_sock, temp, sizeof(temp) - 1);
    sleep(1);

注释到接收端的设置缓冲区。使得发送端的发次发送刚好使用两个数据包(因为设置有 1s 延迟)。

    //int set_ret = setsockopt(sock, SOL_SOCKET, SO_RCVBUF,(const char*)&nrecvBuf, sizeof(int));
    //if(set_ret < 0)
    //{
        //perror("");
    //}
    //printf("set recvbuf %d %d\n", set_ret, nrecvBuf);

由于休眠被信号打断之后,无法继续,在信号处理函数中添加休眠,休眠时间足够接收端内核缓冲区已经收到三个数据包。

sleep(5);

此举主要是为了在第二个带外数据(第三个数据包)到来之前,不读取。

虽然第一个带外数据丢失,但是读取仍然在第一个带外数据前一位置返回。

看到最终读取的是第二个带外数据,第一个带外数据丢失。第三个数据包也是有 URG 标志的,但是并没有触发信号,这是因为 SIGURG 不是实时信号,不能排队,第二个信号被丢失。

猜你喜欢

转载自blog.csdn.net/zhouguoqionghai/article/details/82469256