簡體   English   中英

使用 C/C++ (GCC/G++) 在 Linux 中的套接字編程中發送和接收文件

[英]Send and Receive a file in socket programming in Linux with C/C++ (GCC/G++)

我想使用能夠發送和接收文件的套接字和 C/C++ 語言來實現在 Linux 上運行的客戶端-服務器架構。 是否有任何庫可以簡化此任務? 任何人都可以提供一個例子嗎?

最便攜的解決方案是分塊讀取文件,然后在循環中將數據寫入套接字(同樣,在接收文件時也是如此)。 您分配一個緩沖區, read入該緩沖區,並write從緩沖區插入插座(你也可以使用sendrecv ,這是寫入和讀取數據的插座,具體方式)。 大綱看起來像這樣:

while (1) {
    // Read data into buffer.  We may not have enough to fill up buffer, so we
    // store how many bytes were actually read in bytes_read.
    int bytes_read = read(input_file, buffer, sizeof(buffer));
    if (bytes_read == 0) // We're done reading from the file
        break;
    
    if (bytes_read < 0) {
        // handle errors
    }
    
    // You need a loop for the write, because not all of the data may be written
    // in one call; write will return how many bytes were written. p keeps
    // track of where in the buffer we are, while we decrement bytes_read
    // to keep track of how many bytes are left to write.
    void *p = buffer;
    while (bytes_read > 0) {
        int bytes_written = write(output_socket, p, bytes_read);
        if (bytes_written <= 0) {
            // handle errors
        }
        bytes_read -= bytes_written;
        p += bytes_written;
    }
}

請務必閱讀文檔readwrite處理錯誤時小心翼翼,尤其是。 一些錯誤代碼意味着您應該再試一次,例如只需使用continue語句再次循環,而其他錯誤代碼意味着某些東西已損壞,您需要停止。

要將文件發送到套接字,有一個系統調用sendfile可以執行您想要的操作。 它告訴內核將文件從一個文件描述符發送到另一個文件描述符,然后內核可以處理其余的事情。 有一個警告,源文件描述符必須支持mmap (例如,是一個實際的文件,而不是一個套接字),並且目標必須是一個套接字(所以你不能用它來復制文件,或直接從一個插座到另一個); 它旨在支持您描述的將文件發送到套接字的用法。 但是,它對接收文件沒有幫助; 你需要自己做這個循環。 我不能告訴你為什么有一個sendfile調用但沒有類似的recvfile

請注意sendfile是 Linux 特定的; 它不可移植到其他系統。 其他系統通常有自己的sendfile版本,但確切的界面可能會有所不同(FreeBSDMac OS XSolaris )。

在 Linux 2.6.17 中,引入splice系統調用,從 2.6.23 開始, 內部使用它來實現sendfile splice是比sendfile更通用的 API。 有關splicetee詳細描述,請參閱Linus 本人的相當不錯的解釋 他指出使用splice基本上就像上面的循環一樣,使用readwrite ,只是緩沖區在內核中,因此數據不必在內核和用戶空間之間傳輸,甚至可能永遠不會通過通過 CPU(稱為“零拷貝 I/O”)。

做一個man 2 sendfile 你只需要打開客戶端上的源文件和服務器上的目標文件,然后調用sendfile,內核就會切割和移動數據。

最小可運行 POSIX read + write示例

用法:

  1. 局域網上弄兩台電腦。

    例如,如果在大多數情況下兩台計算機都連接到您的家用路由器,這將起作用,我就是這樣測試的。

  2. 在服務器計算機上:

    1. 使用ifconfig查找服務器本地 IP,例如192.168.0.10

    2. 運行:

       ./server output.tmp 12345
  3. 在客戶端計算機上:

     printf 'ab\\ncd\\n' > input.tmp ./client input.tmp 192.168.0.10 12345
  4. 結果:在包含'ab\\ncd\\n'的服務器計算機上創建了一個文件output.tmp

服務器.c

/*
Receive a file over a socket.

Saves it to output.tmp by default.

Interface:

    ./executable [<output_file> [<port>]]

Defaults:

- output_file: output.tmp
- port: 12345
*/

#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>

#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char **argv) {
    char *file_path = "output.tmp";
    char buffer[BUFSIZ];
    char protoname[] = "tcp";
    int client_sockfd;
    int enable = 1;
    int filefd;
    int i;
    int server_sockfd;
    socklen_t client_len;
    ssize_t read_return;
    struct protoent *protoent;
    struct sockaddr_in client_address, server_address;
    unsigned short server_port = 12345u;

    if (argc > 1) {
        file_path = argv[1];
        if (argc > 2) {
            server_port = strtol(argv[2], NULL, 10);
        }
    }

    /* Create a socket and listen to it.. */
    protoent = getprotobyname(protoname);
    if (protoent == NULL) {
        perror("getprotobyname");
        exit(EXIT_FAILURE);
    }
    server_sockfd = socket(
        AF_INET,
        SOCK_STREAM,
        protoent->p_proto
    );
    if (server_sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
        perror("setsockopt(SO_REUSEADDR) failed");
        exit(EXIT_FAILURE);
    }
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(server_port);
    if (bind(
            server_sockfd,
            (struct sockaddr*)&server_address,
            sizeof(server_address)
        ) == -1
    ) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    if (listen(server_sockfd, 5) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    fprintf(stderr, "listening on port %d\n", server_port);

    while (1) {
        client_len = sizeof(client_address);
        puts("waiting for client");
        client_sockfd = accept(
            server_sockfd,
            (struct sockaddr*)&client_address,
            &client_len
        );
        filefd = open(file_path,
                O_WRONLY | O_CREAT | O_TRUNC,
                S_IRUSR | S_IWUSR);
        if (filefd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }
        do {
            read_return = read(client_sockfd, buffer, BUFSIZ);
            if (read_return == -1) {
                perror("read");
                exit(EXIT_FAILURE);
            }
            if (write(filefd, buffer, read_return) == -1) {
                perror("write");
                exit(EXIT_FAILURE);
            }
        } while (read_return > 0);
        close(filefd);
        close(client_sockfd);
    }
    return EXIT_SUCCESS;
}

客戶端

/*
Send a file over a socket.

Interface:

    ./executable [<input_path> [<sever_hostname> [<port>]]]

Defaults:

- input_path: input.tmp
- server_hostname: 127.0.0.1
- port: 12345
*/

#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>

#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char **argv) {
    char protoname[] = "tcp";
    struct protoent *protoent;
    char *file_path = "input.tmp";
    char *server_hostname = "127.0.0.1";
    char *server_reply = NULL;
    char *user_input = NULL;
    char buffer[BUFSIZ];
    in_addr_t in_addr;
    in_addr_t server_addr;
    int filefd;
    int sockfd;
    ssize_t i;
    ssize_t read_return;
    struct hostent *hostent;
    struct sockaddr_in sockaddr_in;
    unsigned short server_port = 12345;

    if (argc > 1) {
        file_path = argv[1];
        if (argc > 2) {
            server_hostname = argv[2];
            if (argc > 3) {
                server_port = strtol(argv[3], NULL, 10);
            }
        }
    }

    filefd = open(file_path, O_RDONLY);
    if (filefd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    /* Get socket. */
    protoent = getprotobyname(protoname);
    if (protoent == NULL) {
        perror("getprotobyname");
        exit(EXIT_FAILURE);
    }
    sockfd = socket(AF_INET, SOCK_STREAM, protoent->p_proto);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    /* Prepare sockaddr_in. */
    hostent = gethostbyname(server_hostname);
    if (hostent == NULL) {
        fprintf(stderr, "error: gethostbyname(\"%s\")\n", server_hostname);
        exit(EXIT_FAILURE);
    }
    in_addr = inet_addr(inet_ntoa(*(struct in_addr*)*(hostent->h_addr_list)));
    if (in_addr == (in_addr_t)-1) {
        fprintf(stderr, "error: inet_addr(\"%s\")\n", *(hostent->h_addr_list));
        exit(EXIT_FAILURE);
    }
    sockaddr_in.sin_addr.s_addr = in_addr;
    sockaddr_in.sin_family = AF_INET;
    sockaddr_in.sin_port = htons(server_port);
    /* Do the actual connection. */
    if (connect(sockfd, (struct sockaddr*)&sockaddr_in, sizeof(sockaddr_in)) == -1) {
        perror("connect");
        return EXIT_FAILURE;
    }

    while (1) {
        read_return = read(filefd, buffer, BUFSIZ);
        if (read_return == 0)
            break;
        if (read_return == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        /* TODO use write loop: https://stackoverflow.com/questions/24259640/writing-a-full-buffer-using-write-system-call */
        if (write(sockfd, buffer, read_return) == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }
    }
    free(user_input);
    free(server_reply);
    close(filefd);
    exit(EXIT_SUCCESS);
}

GitHub 上游.

進一步評論

可能的改進:

  • 當前每次發送完成時output.tmp都會被覆蓋。

    這要求創建一個允許傳遞文件名的簡單協議,以便可以上傳多個文件,例如:文件名最多為第一個換行符,文件名最大為 256 個字符,其余內容直到套接字關閉為止。 當然,這需要衛生設施以避免路徑橫向脆弱性

    或者,我們可以制作一個服務器來對文件進行散列以查找文件名,並將原始路徑映射到磁盤上(在數據庫上)的散列。

  • 一次只能連接一個客戶端。

    如果有連接持續很長時間的慢速客戶端,這是特別有害的:慢速連接使每個人都停下來。

    解決此問題的一種方法是為每個accept分叉一個進程/線程,立即再次開始偵聽,並對文件使用文件鎖同步。

  • 添加超時,如果時間過長則關閉客戶端。 否則很容易進行 DoS。

    pollselect是一些選項: 如何在讀取函數調用中實現超時?

一個簡單的 HTTP wget實現如下所示: How to make an HTTP get request in C without libcurl?

在 Ubuntu 15.10 上測試。

該文件將成為你的好sendfile例如: http://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM