Linux網絡編程 - 發送/接收數據 & 緩沖區


發送數據

可以用以下三個函數發送數據。每個函數都是單獨使用的,使用的場景略有不同。

ssize_t write (int socketfd, const void *buffer, size_t size);
  
#include <sys/socket.h>
ssize_t send (int socketfd, const void *buffer, size_t size, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
  • write 就是常見的文件寫函數(Linux中一切皆為文件)。

    • 對於普通文件描述符而言,操作系統內核不斷地往文件系統中寫入字節流。
      • 寫入的字節流大小通常和輸入參數 size 的值是相同的,否則表示出錯。
    • 對於套接字描述符而言,它代表了一個雙向連接。
      • 寫入的字節數有可能比請求的數量少
  • send 可以指定選項,發送帶外數據(一種基於 TCP 協議的緊急數據,用於客戶端 - 服務器在特定場景下的緊急處理)。

  • sendmsg 指定多重緩沖區傳輸數據,以結構體 msghdr 的方式發送數據。

    msghdr定義:

    struct iovec {                    /* Scatter/gather array items */
     void  *iov_base;              /* Starting address */
     size_t iov_len;               /* Number of bytes to transfer */
    };
    struct msghdr {
     void         *msg_name;       /* Optional address */
     socklen_t     msg_namelen;    /* Size of address */
     struct iovec *msg_iov;        /* Scatter/gather array */
     size_t        msg_iovlen;     /* # elements in msg_iov */
     void         *msg_control;    /* Ancillary data, see below */
     size_t        msg_controllen; /* Ancillary data buffer len */
     int           msg_flags;      /* Flags on received message */
    };
    

發送緩沖區

當 TCP 三次握手成功,TCP 連接成功建立后,操作系統內核會為每一個連接創建配套的基礎設施,比如發送緩沖區

發送緩沖區的大小可以通過套接字選項來改變,當應用程序調用 write 函數時,實際所做的事情是把數據從應用程序中拷貝到操作系統內核的發送緩沖區中,並不一定是把數據通過套接字寫出去。

image-20211101133547721

wirte() 的返回時機:直到某一個時刻,應用程序的數據可以完全放置到發送緩沖區里,write() 阻塞調用返回。

讀取數據

ssize_t read (int socketfd, void *buffer, size_t size);

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

read 函數要求操作系統內核從套接字描述字 socketfd 讀取最多多少個字節(size),並將結果存儲到 buffer 中。

返回值表示實際讀取的字節數目,如果返回值為 0,表示 EOF(end-of-file),這在網絡中表示對端發送了 FIN 包,要處理斷連的情況;如果返回值為 -1,表示出錯。

read 是最多讀取 size 個字節。循環讀取,每次都讀到 size 個字節的函數:

size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = (char *)buffer;
    int length = size;

    while (length > 0) {
        int result = read(fd, buffer_pointer, length);

        if (result < 0) {
            if (errno == EINTR)
                continue;     /* 考慮非阻塞的情況,這里需要再次調用read */
            else
                return (-1);
        } else if (result == 0)
            break;                /* EOF(End of File)表示套接字關閉 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length);        /* 返回的是實際讀取的字節數*/
}

例子

common.h :

#pragma once

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

/* error - print a diagnostic and optionally exit */
void error(int status, int err, char *fmt, ...) {
    va_list ap;

    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    if (err) {
        fprintf(stderr, ": %s (%d)\n", strerror(err), err);
    }
    if (status) {
        exit(status);
    }
}

int tcp_client(char *address, int port) {
    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(port);
    inet_pton(AF_INET, address, &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 ");
    }

    return socket_fd;
}

size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = (char *)buffer;
    int length = size;

    while (length > 0) {
        int result = read(fd, buffer_pointer, length);

        if (result < 0) {
            if (errno == EINTR)
                continue;     /* 考慮非阻塞的情況,這里需要再次調用read */
            else
                return (-1);
        } else if (result == 0)
            break;                /* EOF(End of File)表示套接字關閉 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length);        /* 返回的是實際讀取的字節數*/
}

Client:

#include "tcp_send_read/common/commom.h"

# define MESSAGE_SIZE 1024 * 1024 * 100

void send_data(int sockfd) {
    char *query;
    query = (char *)malloc(MESSAGE_SIZE + 1);
    for (int i = 0; i < MESSAGE_SIZE; i++) {
        query[i] = 'a';
    }
    query[MESSAGE_SIZE] = '\0';

    const char *cp;
    cp = query;
    size_t remaining = strlen(query);
    while (remaining) {
        int n_written = send(sockfd, cp, remaining, 0);
        fprintf(stdout, "send into buffer %ld \n", n_written);
        if (n_written <= 0) {
            error(1, errno, "send failed");
            return;
        }
        remaining -= n_written;
        cp += n_written;
    }

    return;
}

int main(int argc, char **argv) {
    struct sockaddr_in servaddr;

    if (argc != 2)
        error(1, 0, "usage: tcpclient <IPaddress>");

    int sockfd = tcp_client(argv[1], 12345);
    
    send_data(sockfd);
    exit(0);
}

Server:

#include "tcp_send_read/common/commom.h"

#define BUFFER_SIZE 1024 * 1024

void read_data(int sockfd) {
    ssize_t n;
    char buf[BUFFER_SIZE];

    int time = 0;
    for (;;) {
        fprintf(stdout, "block in read\n");
        if ((n = readn(sockfd, buf, BUFFER_SIZE)) == 0) { // EOF
            fprintf(stdout, "read 0, close it!\n");
            fflush(stdout); // must flust it!
            return;
        }

        time++;
        fprintf(stdout, "1M read for %d \n", time);
        usleep(2000);
    }
}


int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(12345);

    /* bind到本地地址,端口為12345 */
    bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    /* listen的backlog為1024 */
    listen(listenfd, 1024);

    /* 循環處理用戶請求 */
    for (;;) {
        clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
        read_data(connfd);   /* 讀取數據 */
        close(connfd);          /* 關閉連接套接字,注意不是監聽套接字*/
    }
}

此程序能說明幾個問題:

  1. 阻塞式套接字最終發送返回的實際寫入字節數和請求字節數是相等的

  2. 對於 send 來說,返回成功僅表示數據寫到發送緩沖區成功,並不意味着對端已經成功收到(對端何時收到,對發送者透明

reference

[1] 極客時間 · 網絡編程實戰 :05 | 使用套接字進行讀寫:開始交流吧

[2] Linux高性能服務器編程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM