《Linux網路開發必學教程》13_資料收發的擴充套件用法 (上)

TianSong發表於2022-05-13
write() 和 send() 都可以收發資料,有什麼區別?
  • send 可以使用 flags 指定可選項資訊,其中 0 表示預設傳送行為
  • send 當 flags 為 0 時,會等待傳送緩衝區資料清空之後才將資料放入傳送緩衝器然後返回
  • write 不能指定可選項資訊,同時不會阻塞
read() 和 recv() 都可以接收資料,有什麼區別?
  • recv 可以使用 flags 指定可選項資訊,其中 0 表示預設接收行為
  • recv 當 flags 為 0 時,會等待接收緩衝區有資料之後才將資料從接收緩衝區中取出然後返回
  • read 不能指定可選項資訊,同時不會阻塞

資料收發選項

#include <sys/socket.h>

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 選項資訊 (部分)

可選項含義sendrecv
MSG_OOB用於傳輸帶外資料(Out Of Band Data),即:緊急資料(優先傳送)
MSG_PEEK驗證接收緩衝區是否存在資料(有什麼資料)
MSG_DONTROUTE資料傳輸過程不通過路由表,在本地區域網中尋找目的地
MSG_DONTWAIT非阻塞模式,資料收發時立即返回
MSG_WAITALL在接收到請求的全部資料之前,不提前返回
MSG_MORE有更多資料需要傳送,指示核心等待資料
......
注意: 不同的作業系統對上述可選項的支援不同,實際工程開發時,需要事先對目標系統中支援的可選項進行調研

MSG_OOB (帶外資料,緊急資料)

原生定義
  • 使用與普通資料不同的的通道獨立傳輸的資料
  • 帶外資料優先順序比普通資料高(優先傳輸,對端優先接收)
TCP 中的帶外資料
  • 由於原生設計的限制,TCP無法提供真正意義上的帶外資料
  • TCP 中僅能通過傳輸協議訊息頭中的標記,傳輸緊急資料,且長度僅1位元組

TCP 帶外資料實現原理

image.png

URG 指標指向緊急訊息的下一個位置,即:URG 指標指向位置的前一個位元組儲存了緊急訊息
接收端優先接收緊急資料,並將其儲存到特殊緩衝區,之後再接收普通資料

緊急資料:0x03
普通資料:0x01,0x02

TCP 帶外資料處理策略

  • 由於 TCP 設計為流式資料,因此,無法做到真正的帶外資料
  • 被標記的緊急資料可被提前接收,進入特殊緩衝區(僅一位元組)

    • 每個 TCP 包最多隻有一個緊急資料
    • 特殊緩衝區僅存放最近的緊急資料(不及時接收將丟失)

用下面的方式收發資料會發生什麼

傳送普通資料,普通方式接收:正常,資料按序到達
傳送普通資料,緊急方式接收:錯誤返回
傳送緊急資料,普通方式接收:普通資料 recv 時阻塞
傳送緊急資料,緊急方式接收:正常,收到緊急資料

程式設計實驗:TCP 緊急資料的傳送與接收

client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

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 <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

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 能夠及時處理緊急資料,並區分普通資料

相關文章