《Linux网络开发必学教程》13_数据收发的扩展用法 (上)

write() 和 send() 都可以收发数据,有什么区别?
  • send 可以使用 flags 指定可选项信息,其中 0 表示默认发送行为
  • send 当 flags 为 0 时,会等待发送缓冲区数据清空之后才将数据放入发送缓冲器然后返回
  • write 不能指定可选项信息,同时不会阻塞
read() 和 recv() 都可以接收数据,有什么区别?
  • recv 可以使用 flags 指定可选项信息,其中 0 表示默认接收行为
  • recv 当 flags 为 0 时,会等待接收缓冲区有数据之后才将数据从接收缓冲区中取出然后返回
  • read 不能指定可选项信息,同时不会阻塞

数据收发选项

#include 

ssize_t send(int socketfd, const void *buf, size_t nbytes, int flags);
ssize_t recv(int socketfd, void *buf, size_t nbytes, int flags);

flags - 收发数据时指定可选项信息,其中 0 为默认收发行为

flags 选项信息 (部分)

可选项 含义 send recv
MSG_OOB 用于传输带外数据(Out Of Band Data),即:紧急数据(优先发送)
MSG_PEEK 验证接收缓冲区是否存在数据(有什么数据)
MSG_DONTROUTE 数据传输过程不通过路由表,在本地局域网中寻找目的地
MSG_DONTWAIT 非阻塞模式,数据收发时立即返回
MSG_WAITALL 在接收到请求的全部数据之前,不提前返回
MSG_MORE 有更多数据需要发送,指示内核等待数据
... ...
注意: 不同的操作系统对上述可选项的支持不同,实际工程开发时,需要事先对目标系统中支持的可选项进行调研

MSG_OOB (带外数据,紧急数据)

原生定义
  • 使用与普通数据不同的的通道独立传输的数据
  • 带外数据优先级比普通数据高(优先传输,对端优先接收)
TCP 中的带外数据
  • 由于原生设计的限制,TCP无法提供真正意义上的带外数据
  • TCP 中仅能通过传输协议消息头中的标记,传输紧急数据,且长度仅1字节

TCP 带外数据实现原理

《Linux网络开发必学教程》13_数据收发的扩展用法 (上)_第1张图片

URG 指针指向紧急消息的下一个位置,即:URG 指针指向位置的前一个字节存储了紧急消息
接收端优先接收紧急数据,并将其存储到特殊缓冲区,之后再接收普通数据

紧急数据:0x03
普通数据:0x01,0x02

TCP 带外数据处理策略

  • 由于 TCP 设计为流式数据,因此,无法做到真正的带外数据
  • 被标记的紧急数据可被提前接收,进入特殊缓冲区(仅一字节)

    • 每个 TCP 包最多只有一个紧急数据
    • 特殊缓冲区仅存放最近的紧急数据(不及时接收将丢失)

用下面的方式收发数据会发生什么

发送普通数据,普通方式接收:正常,数据按序到达
发送普通数据,紧急方式接收:错误返回
发送紧急数据,普通方式接收:普通数据 recv 时阻塞
发送紧急数据,紧急方式接收:正常,收到紧急数据

编程实验:TCP 紧急数据的发送与接收

client.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int sock = {0};
    struct sockaddr_in addr = {0};
    int len = 0;
    char *test = "Delpin-Tang";

    sock = socket(PF_INET, SOCK_STREAM, 0);

    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(8888);

    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        printf("connect error\n");
        return -1;
    }

    printf("connect success\n");

    len = send(sock, test, strlen(test), MSG_OOB);

    getchar();

    close(sock);

    return 0;
}
server.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int server = 0;
    struct sockaddr_in saddr = {0};
    int client = 0;
    struct sockaddr_in caddr = {0};
    socklen_t asize = 0;
    int len = 0;
    char buf[32] = {0};
    int r = 0;

    server = socket(PF_INET, SOCK_STREAM, 0);

    if (server == -1) {
        printf("server socket error\n");
        return -1;
    }

    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(8888);

    if (bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1) {
        printf("server bind error\n");
        return -1;
    }

    if (listen(server, 1) == -1) {
        printf("server listen error\n");
        return -1;
    }

    printf("server start success\n");

    while (1) {
        asize = sizeof(caddr);

        client = accept(server, (struct sockaddr*)&caddr, &asize);

        if (client == -1) {
            printf("client accept error");
            return -1;
        }

        printf("client: %d\n", client);

        do {
            r = recv(client, buf, sizeof(buf), MSG_OOB);

            if (r > 0) {
                buf[r] = 0;
                printf("OOB: %s\n", buf);
            }

            r = recv(client, buf, sizeof(buf), 0);

            if (r > 0) {
                buf[r] = 0;
                printf("NORMAL: %s\n", buf);
            }
        }while (r > 0);

        close(client);
    }

    close(server);

    return 0;
}
输出:
server start success
client: 4
NORMAL: Delpin-Tan   // 注意,普通数据先输出 (因为当 flags 为 MSG_OOB 时不阻塞,而为 0 时会阻塞,直到接收到数据)
OOB: g               // 注意,仅输出最后一个字符 !!
小问题:实际开发中,如何高效的接收 TCP 紧急数据?

使用 select 接收紧急数据

socket 上收到普通数据和紧急数据时都会使得 select 立即返回
  • 普通数据:socket 处于数据可读状态(可读取普通数据)
  • 紧急数据:socket 处理异常状态(可读取紧急数据)

紧急数据接收示例

num = select(max + 1, &temp, 0, &except, &timeout);

if (num > 0) {
    for (i=1; i<=max; ++i) {
        if (FD_ISSET(i, &except)) {
            if (i != server) {
                char buf[32] = {0};
                int r = recv(i, buf, sizeof(buf), MSG_OOB);
                if (r > 0) {
                    buf[r] = 0;
                    printf("OOB: %s\n", buf);
                }
            }
        }

        if (FD_ISSET(I, &temp)) {
            // ...
        }
    }
}

编程实验:使用 select 接收紧急数据

client.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int sock = {0};
    struct sockaddr_in addr = {0};
    int len = 0;
    char *test = "Delpin-Tang";

    sock = socket(PF_INET, SOCK_STREAM, 0);

    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(8888);

    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        printf("connect error\n");
        return -1;
    }

    printf("connect success\n");

    len = send(sock, test, strlen(test), MSG_OOB);

    getchar();

    close(sock);

    return 0;
}
select-server.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int server_handler(int server) 
{
    struct sockaddr_in addr = {0};
    socklen_t asize = sizeof(addr);
    return accept(server, (struct sockaddr*)&addr, &asize);
}

int client_handler(int client) 
{
    char buf[32] = {0};

    int ret = recv(client, buf, sizeof(buf) - 1, 0);

    if (ret > 0) {
        buf[ret] = 0;

        printf("Recv: %s\n", buf);
    }

    return ret;
}

int clint_except_handler(int client) 
{
    char buf[2] = {0};
    int r = recv(client, buf, sizeof(buf), MSG_OOB);

    if (r > 0) {
        buf[r] = 0;
        printf("OOB: %s\n", buf);
    }

    return r;
}

int main()
{
    int server = 0;
    struct sockaddr_in saddr = {0};
    int max = 0;
    int num = 0;
    fd_set reads = {0};
    fd_set temps = {0};
    fd_set except = {0};
    struct timeval timeout = {0};

    server = socket(PF_INET, SOCK_STREAM, 0);

    if (server == -1) {
        printf("server socket error\n");
        return -1;
    }

    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(8888);

    if (bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1) {
        printf("server bind error\n");
        return -1;
    }

    if (listen(server, 1) == -1) {
        printf("server listeb error\n");
        return -1;
    }

    printf("server start success\n");

    FD_ZERO(&reads);
    FD_SET(server, &reads);

    max = server;

    while (1) {
        temps = reads;
        except = reads;

        timeout.tv_sec = 0;
        timeout.tv_usec = 10000;

        num = select(max + 1, &temps, 0, &except, &timeout);

        if (num > 0) {
            int i = 0;

            for (i=0; i<=max; ++i) {
                if (FD_ISSET(i, &except)) {
                    if (i != server) {
                        clint_except_handler(i);
                    }
                }

                if (FD_ISSET(i, &temps)) {
                    if (i == server) {
                        int client = server_handler(server);

                        if (client > -1) {
                            FD_SET(client, &reads);
                            max = (client > max) ? client : max;
                            printf("accept client: %d\n", client);
                        }
                    } else {
                        int r = client_handler(i);

                        if (r == -1) {
                            FD_CLR(i, &reads);
                            close(i);
                        }
                    }
                }
            }

            int client = server_handler(server);
        }
    }
}
输出:
server start success
accept client: 4
OOB: g
Recv: Delpin-Tan

小结

  • read() / write() 可用于收发普通数据(不具备扩展功能)
  • send() / recv() 可通过选项信息扩展更多功能
  • TCP 紧急数据可标识 256 种紧急事件(异常事件)
  • 通过 select 能够及时处理紧急数据,并区分普通数据

你可能感兴趣的