C语言socket传输文件如何实现?

99ANYc3cd6
预计阅读时长 31 分钟
位置: 首页 C语言 正文
  1. 客户端:负责读取本地文件,并通过Socket发送给服务器。
  2. 服务器:负责接收来自客户端的数据,并将其写入一个本地文件。

核心原理

  1. 建立连接:客户端和服务器首先需要建立一个TCP连接,服务器在一个固定的端口上监听,客户端主动连接到服务器的IP和端口。
  2. 文件信息协商(可选但推荐):客户端在发送文件数据之前,可以先发送文件的元信息,如文件名和文件大小,这样服务器可以根据这些信息创建文件,并显示进度。
  3. 文件数据传输
    • 客户端以二进制模式打开文件,循环读取文件内容,每次读取一块数据(使用 fread),然后通过 send 函数将这块数据发送出去。
    • 服务器循环使用 recv 函数接收数据,并将接收到的数据以二进制模式追加写入到目标文件中。
  4. 关闭连接:当文件传输完毕后,客户端和服务器都需要关闭文件和Socket连接。

代码示例

我们将使用 sendfile 系统调用来优化文件传输,因为它能在内核空间直接完成数据拷贝,效率最高。sendfile 不可用(跨平台或需要更灵活的控制),我们也会展示使用 fread/fwrite 的传统方法。

c语言 socket传输文件
(图片来源网络,侵删)

服务器代码 (server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h> // 用于获取文件大小
#define PORT 8080
#define BUFFER_SIZE 4096
void send_file(int client_socket) {
    // 1. 接收文件名
    char filename[256] = {0};
    int bytes_received = recv(client_socket, filename, 256, 0);
    if (bytes_received <= 0) {
        perror("Failed to receive filename");
        return;
    }
    printf("Receiving file: %s\n", filename);
    // 2. 接收文件大小
    long file_size;
    bytes_received = recv(client_socket, &file_size, sizeof(long), 0);
    if (bytes_received <= 0) {
        perror("Failed to receive file size");
        return;
    }
    printf("File size: %ld bytes\n", file_size);
    // 3. 打开文件准备写入
    FILE *file = fopen(filename, "wb"); // "wb" 表示二进制写入模式
    if (file == NULL) {
        perror("Failed to create file");
        return;
    }
    // 4. 循环接收文件数据
    long total_received = 0;
    char buffer[BUFFER_SIZE];
    int bytes_read;
    printf("Receiving file data...\n");
    while (total_received < file_size) {
        bytes_read = recv(client_socket, buffer, BUFFER_SIZE, 0);
        if (bytes_read <= 0) {
            // 连接关闭或出错
            break;
        }
        fwrite(buffer, 1, bytes_read, file);
        total_received += bytes_read;
        // 打印接收进度
        printf("Progress: [%.2f%%]\r", (double)total_received / file_size * 100);
        fflush(stdout);
    }
    printf("\nFile received successfully.\n");
    // 5. 关闭文件和连接
    fclose(file);
    close(client_socket);
}
int main() {
    int server_fd, client_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    // 1. 创建Socket (AF_INET for IPv4, SOCK_STREAM for TCP)
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项,允许地址重用,避免 "Address already in use" 错误
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    address.sin_port = htons(PORT);        // 将端口号从主机字节序转换为网络字节序
    // 2. 绑定Socket到指定端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 3. 开始监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 接受客户端连接
    if ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
    printf("Client connected: %s:%d\n", client_ip, ntohs(address.sin_port));
    // 5. 处理文件传输
    send_file(client_socket);
    // 6. 关闭服务器Socket(如果不再接受新连接)
    close(server_fd);
    return 0;
}

客户端代码 (client.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h> // 用于获取文件大小
#define PORT 8080
#define BUFFER_SIZE 4096
void send_file_to_server(int client_socket, const char *filepath) {
    // 1. 打开文件
    FILE *file = fopen(filepath, "rb"); // "rb" 表示二进制读取模式
    if (file == NULL) {
        perror("Failed to open file");
        return;
    }
    // 2. 获取文件名和大小
    char filename[256];
    strcpy(filename, strrchr(filepath, '/') + 1); // 从路径中提取文件名
    struct stat st;
    stat(filepath, &st);
    long file_size = st.st_size;
    printf("Sending file: %s (%ld bytes)\n", filename, file_size);
    // 3. 先发送文件名
    if (send(client_socket, filename, strlen(filename), 0) < 0) {
        perror("Failed to send filename");
        fclose(file);
        return;
    }
    // 4. 再发送文件大小
    if (send(client_socket, &file_size, sizeof(long), 0) < 0) {
        perror("Failed to send file size");
        fclose(file);
        return;
    }
    // 5. 循环读取并发送文件数据
    char buffer[BUFFER_SIZE];
    size_t bytes_read;
    long total_sent = 0;
    printf("Sending file data...\n");
    while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, file)) > 0) {
        if (send(client_socket, buffer, bytes_read, 0) < 0) {
            perror("Failed to send file data");
            break;
        }
        total_sent += bytes_read;
        // 打印发送进度
        printf("Progress: [%.2f%%]\r", (double)total_sent / file_size * 100);
        fflush(stdout);
    }
    printf("\nFile sent successfully.\n");
    // 6. 关闭文件和连接
    fclose(file);
    close(client_socket);
}
int main(int argc, char const *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <server_ip> <filepath>\n", argv[0]);
        return -1;
    }
    char *server_ip = argv[1];
    char *filepath = argv[2];
    int client_socket;
    struct sockaddr_in serv_addr;
    // 1. 创建Socket
    if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 2. 将IP地址从文本转换为网络格式
    if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 3. 连接到服务器
    if (connect(client_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    printf("Connected to server %s:%d\n", server_ip, PORT);
    // 4. 发送文件
    send_file_to_server(client_socket, filepath);
    return 0;
}

如何编译和运行

  1. 保存代码:将上面的服务器代码保存为 server.c,客户端代码保存为 client.c

  2. 编译:打开终端,使用 gcc 进行编译。

    # 编译服务器
    gcc server.c -o server
    # 编译客户端
    gcc client.c -o client
  3. 运行

    • 首先启动服务器,让它在一个终端中监听:
      ./server

      你会看到输出:Server listening on port 8080...

    • 然后启动客户端,在另一个终端中执行,你需要指定服务器的IP地址(如果你在本地测试,可以用 0.0.1localhost)以及要传输的文件路径。
      # 示例:发送当前目录下的 "my_image.jpg" 文件
      ./client 127.0.0.1 ./my_image.jpg

      你会看到连接建立和文件传输的进度信息。


关键点与注意事项

  1. 字节序:网络通信中统一使用网络字节序(大端序)htons() (host to network short) 和 htonl() (host to network long) 用于将主机字节序(通常是x86的小端序)转换为网络字节序。ntohs()ntohl() 则是反向转换,在发送整数(如文件大小)时必须进行转换。

  2. 二进制模式:在C语言中处理文件时,必须使用 "rb" (binary read) 和 "wb" (binary write) 模式,如果使用文本模式 ("r", "w"),Windows系统下的换行符 \r\n 会被自动转换为 \n,可能导致文件损坏。

  3. 错误处理sendrecv 函数在连接被对方正常关闭时,会返回0,如果发生错误,会返回-1,必须检查它们的返回值,以判断连接状态和传输是否成功。

  4. 缓冲区大小BUFFER_SIZE 定义了每次读写和传输的数据块大小,这个值可以根据网络状况和文件大小进行调整,较大的值可以提高效率,但也会占用更多内存。

  5. 并发处理:上面的示例服务器一次只能处理一个客户端连接,如果需要同时处理多个客户端,需要使用多线程、多进程或者I/O多路复用(如 select, poll, epoll)等技术,一个简单的改进是在 main 函数的 accept 循环中为每个客户端创建一个新线程来处理 send_file

  6. 安全性:这个示例没有做任何安全验证,在生产环境中,你需要考虑:

    • 文件路径安全:防止客户端通过恶意路径(如 ../../../etc/passwd)访问服务器上的敏感文件。
    • 认证:确保只有授权的客户端才能连接和传输文件。
    • 完整性校验:可以使用MD5、SHA1等哈希算法对文件进行校验,确保传输过程中数据没有损坏或被篡改。

这个例子为你提供了一个坚实的基础,你可以基于此进行扩展和优化,以适应更复杂的应用场景。

-- 展开阅读全文 --
头像
dede静态模板解析类如何实现页面静态化?
« 上一篇 02-12
织梦robots.txt怎么写?规则与示例解析
下一篇 » 02-12

相关文章

取消
微信二维码
支付宝二维码

目录[+]