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

2024-10-18 09:00:00
admin
原创
73
摘要:问题描述:我想使用套接字和 C/C++ 语言实现在 Linux 上运行的客户端-服务器架构,该架构能够发送和接收文件。是否有任何库可以轻松完成此任务?有人可以提供一个例子吗?解决方案 1:最可移植的解决方案就是以块的形式读取文件,然后循环将数据写入套接字(接收文件时也一样)。您分配一个缓冲区,将数据read写...

问题描述:

我想使用套接字和 C/C++ 语言实现在 Linux 上运行的客户端-服务器架构,该架构能够发送和接收文件。是否有任何库可以轻松完成此任务?有人可以提供一个例子吗?


解决方案 1:

最可移植的解决方案就是以块的形式读取文件,然后循环将数据写入套接字(接收文件时也一样)。您分配一个缓冲区,将数据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;
    }
}

read请务必仔细阅读和的文档write,尤其是在处理错误时。一些错误代码意味着您应该再试一次,例如只需再次循环语句continue,而其他错误代码则意味着出现问题,您需要停止。

要将文件发送到套接字,有一个系统调用sendfile可以满足您的要求。它告诉内核将文件从一个文件描述符发送到另一个文件描述符,然后内核可以处理其余的事情。有一个警告,即源文件描述符必须支持mmap(即,是实际文件,而不是套接字),并且目标必须是套接字(因此您不能使用它来复制文件或将数据直接从一个套接字发送到另一个套接字);它旨在支持您描述的将文件发送到套接字的用法。但是,它对接收文件没有帮助;您需要自己执行循环。我无法告诉您为什么有调用sendfile但没有类似的调用recvfile

请注意,这sendfile是 Linux 独有的;它不能移植到其他系统。其他系统通常有自己的版本sendfile,但确切的界面可能有所不同(FreeBSD、Mac OS X、Solaris)。

在 Linux 2.6.17 中,引入了splice系统调用,从 2.6.23 开始,系统调用在内部用于实现。是比更通用的 API 。有关和的详细说明,请参阅Linus 本人的相当不错的解释。他指出,使用基本上就像上面的循环一样,使用和,只是缓冲区在内核中,因此数据不必在内核和用户空间之间传输,甚至可能永远不会通过 CPU(称为“零拷贝 I/O”)。sendfile`splicesendfilespliceteesplicereadwrite`

解决方案 2:

做一个man 2 sendfile。您只需要在客户端上打开源文件并在服务器上打开目标文件,然后调用 sendfile,内核就会切碎并移动数据。

解决方案 3:

最小可运行 POSIX read+write示例

用法:

  1. 在局域网上安装两台计算机。

例如,大多数情况下,如果两台计算机都连接到家庭路由器,这种方法就会有效,这就是我测试的方法。

  1. 在服务器计算机上:

1. 使用 查找服务器本地 IP `ifconfig`,例如`192.168.0.10`
2. 跑步:


./server output.tmp 12345
  1. 在客户端计算机上:

printf 'ab
cd
' > input.tmp
./client input.tmp 192.168.0.10 12345
  1. output.tmp结果:在服务器计算机上创建一个包含的文件`'ab
    cd

'`!

服务器端

/*
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
", 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;
}

客户端.c

/*
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\")
", 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\")
", *(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。

poll或者select有一些选项:如何在读取函数调用中实现超时?

一个简单的 HTTPwget实现如下所示:如何在没有 libcurl 的情况下用 C 发出 HTTP 获取请求?

在 Ubuntu 15.10 上测试。

解决方案 4:

该文件将作为一个很好的sendfile例子:http ://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用