curl编程实例-上传文件

之前的一篇文章,介绍过使用curl编程来下载文件。本篇,继续介绍如何使用curl编程来上传文件。

1 基础概念

1.1 文件上传的HTTP方法

PUT 和 POST 是 HTTP 协议中用于向服务器提交数据的请求方法

  • PUT方法:用于向服务器推送一个已明确标识的资源,强调 "更新" 的动作(若资源不存在则创建),是幂等操作
  • POST方法:用于向服务器提交新资源或触发一次非幂等的操作,强调 "创建"的动作

1.2 幂等性

幂等性 是 HTTP 协议中的核心概念,指多次执行同一个请求,得到的服务器资源状态和执行一次的结果完全一致,不会产生额外的副作用

  • 幂等:第一次上传会创建该文件,第二次上传同一个或不同内容的文件,会直接覆盖掉原来的文
  • 非幂等:每次请求服务器都会生成一个新的文件副本(比如自动命名 file1.txt file2.txt),多次请求会产生多个文件

2 curl文件上传

下面通过C语言编程来实现文件上传,这里将文件上传到本地计算器的一个文件服务器地址,文件服务器的创建,在下面会介绍。

先来看下文件上传这个客户端的代码:

c 复制代码
// gcc file_upload1.c -o file_upload1 -lcurl
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <curl/curl.h>
#include <string.h>
#include <libgen.h>  // 用于提取原始文件名

// 进度回调函数
static size_t upload_progress(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) 
{
    if (ultotal > 0) 
    {
        // 计算进度百分比,限制最大值为100%(实际的上传数据包含了HTTP头部等数据)
        float progress = (ulnow * 100.0) / ultotal;
        if (progress > 100.0) progress = 100.0;

        printf("progress: %lld/%lld (%.2f%%)\n", (long long)ulnow, (long long)ultotal, progress);
    }
    return 0;
}

// 提取文件路径中的原始文件名(兼容绝对/相对路径)
char* get_original_filename(const char *file_path) 
{
    if (!file_path) 
    {
        return strdup("unknown_file.dat");
    }
    
    char *path_copy = strdup(file_path);
    char *filename = basename(path_copy);
    char *result = strdup(filename);
    free(path_copy);
    return result;
}

// 获取文件实际大小(字节)
long get_file_size(const char *file_path) 
{
    struct stat st;
    if (stat(file_path, &st) == -1) 
    {
        return -1;
    }
    
    return st.st_size;
}

int main(int argc, char *argv[]) 
{
    CURL *curl = NULL;
    CURLcode res;
    FILE *fp = NULL;
    
    const char *upload_url = "http://192.168.5.104:8080/upload"; // 文件服务器的地址
    const char *file_path = "./test.txt";  // 要上传的本地文件
    curl_off_t file_size = 0;
    struct curl_slist *headers = NULL; // 自定义请求头
    
    const char *upload_mode = "PUT";
    if (argc > 1) 
    {
        upload_mode = argv[1]; // PUT或POST模式
    }

    // 打开待上传的文件(二进制模式)
    fp = fopen(file_path, "rb");
    if (!fp) 
    {
        printf("fopen:%s err\n", file_path);
        goto cleanup;  // 统一清理资源
    }
    
    // 获取文件大小
    file_size = get_file_size(file_path);
    if (file_size < 0) 
    {
        printf("%s file_size:%lld err\n", file_path, (long long)file_size);
        goto cleanup;
    }
    printf("%s file_size: %lld\n", file_path, (long long)file_size);
        
    // 初始化libcurl
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
    if (!curl) 
    {
        printf("curl_easy_init, err\n");
        goto cleanup;
    }

    // 构建自定义请求头(传递原始文件名)
    char *original_filename = get_original_filename(file_path);
    char header_buf[256];
    snprintf(header_buf, sizeof(header_buf), "X-File-Name: %s", original_filename);
    headers = curl_slist_append(headers, header_buf);
    // 禁用Expect头,解决POST上传阻塞问题
    headers = curl_slist_append(headers, "Expect:");

    // 设置上传URL
    curl_easy_setopt(curl, CURLOPT_URL, upload_url);
    // 设置自定义请求头
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    
    if (strcmp(upload_mode, "PUT") == 0)
    {
        // ========== PUT 上传模式 ==========
        printf("PUT mode\n");
        curl_easy_setopt(curl, CURLOPT_PUT, 1L);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, file_size);
    }
    else
    {
        // ========== POST 上传模式 ==========
        printf("POST mode\n");
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, file_size);
    }
    
    // 设置文件流(PUT/POST通用)
    curl_easy_setopt(curl, CURLOPT_READDATA, fp);
    // 启用进度回调
    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, upload_progress);
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
    // 调试用:打印请求头、响应头、传输细节
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

    // 执行上传
    printf("start uoload file: %s\n", file_path);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) 
    {
        printf("upload fail: %s\n", curl_easy_strerror(res));
    } 
    else 
    {
        printf("upload success\n");
    }

    // 资源清理(统一出口)
cleanup:
    if (original_filename) free(original_filename);
    if (headers) curl_slist_free_all(headers);
    if (curl) curl_easy_cleanup(curl);
    if (fp) fclose(fp);
    curl_global_cleanup();
    
    return res == CURLE_OK ? 0 : 1;
}

上述代码:

  • 文件上传地址未:http://192.168.5.104:8080/upload,可根据自己的实际IP进行修改
  • 上传当前目录的test.txt文件
  • 通过运行时参数,可指定使用PUT或POST方法,默认为PUT方法

3 搭建测试用的文件上传服务器

为了便于测试文件上传,需要有一个能接收文件上传的服务器。可以使用python代码来实现一个简易的服务器。

这里使用Python内置的HTTP服务器框架http.server,它提供的 BaseHTTPRequestHandler(处理 HTTP 请求的基类)和 HTTPServer(启动 HTTP 服务器的类),是实现自定义 HTTP 服务的核心。

do_POST/do_PUT是固定的名称,除了 do_POST/do_PUT,常用的还有:

方法名 对应 HTTP 方法 用途
do_GET GET 获取资源
do_HEAD HEAD 只获取响应头
do_DELETE DELETE 删除资源
do_OPTIONS OPTIONS 查询服务器支持的 HTTP 方法

本篇实例使用的服务端代码如下:

python 复制代码
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import threading

# 全局计数+锁:仅用于 POST 方法
file_count = {}
count_lock = threading.Lock()

class UploadHandler(BaseHTTPRequestHandler):
    def do_PUT(self):
        # PUT 方法:幂等,直接覆盖同名文件,不生成序号
        self._save_file(use_increment=False)
    
    def do_POST(self):
        # POST 方法:非幂等,自增序号生成新文件
        self._save_file(use_increment=True)

    def _save_file(self, use_increment):
        upload_dir = "./uploads"
        if not os.path.exists(upload_dir):
            os.makedirs(upload_dir)

        # 获取原始文件名
        original_filename = self.headers.get('X-File-Name', 'unknown_file.dat')
        original_filename = os.path.basename(original_filename)
        name, ext = os.path.splitext(original_filename)
        if not ext:
            ext = ".dat"
            name = original_filename

        # 根据 use_increment 决定是否自增序号
        if use_increment:
            # POST:自增序号
            with count_lock:
                file_count[original_filename] = file_count.get(original_filename, 0) + 1
                current_count = file_count[original_filename]
            if current_count == 1: # 第一次POST,则不带数字后缀
                new_filename = original_filename
            else:
                new_filename = f"{name}_{current_count}{ext}"
        else:
            # PUT:直接使用原始文件名,覆盖已有文件
            new_filename = original_filename

        filepath = os.path.join(upload_dir, new_filename)
        
        try:
            content_length = int(self.headers['Content-Length'])
            data = self.rfile.read(content_length)
            with open(filepath, 'wb') as f:
                f.write(data)
            
            self.send_response(200)
            self.end_headers()
            resp_msg = f"[{self.command}] 成功!保存为:{new_filename}".encode()
            self.wfile.write(resp_msg)
            print(f"[{self.command}] {original_filename} -> {new_filename}")
        except Exception as e:
            self.send_response(500)
            self.end_headers()
            self.wfile.write(f"上传失败:{str(e)}".encode())

if __name__ == "__main__":
    server_address = ('0.0.0.0', 8080)
    httpd = HTTPServer(server_address, UploadHandler)
    print("支持 PUT(幂等)/POST(非幂等) 的服务器已启动")
    print(f"监听地址:http://0.0.0.0:8080")
    httpd.serve_forever()

主要流程是:

  • 启动HTTP服务
  • 监听8080端口,接收请求
  • 判断请求方法,调用do_PUT或do_POST,然后调用自定义的_save_file来保存文件
    • 创建uploads目录(若不存在)
    • 解析请求头中的原始文件名
    • 根据是否自增,生成目标文件名
    • 读取请求体中的文件数据,写入本地
    • 返回200成功/500失败

4 测试结果

服务端程序在windows电脑中运行:

客户端程序的Ubuntu虚拟机中运行:

第1次上传后(默认使用PUT方法),可以看到服务端接收到文件:

第2次上传(仍使用PUT方法),这次上传前先修改下文件中的内容:

可以看到服务端接收到文件内容被更新:

第3次上传(改用POST方法)

因为服务端处理第一个POST时仍会使用不带后缀的文件名进行保存,所以这次仍是被覆盖保存。

可以再来一下POST方法上传:

上传后,可以看到这次保存为了test_2.txt

5 总结

本篇介绍了使用curl编程来实现文件上传,测试了PUT和POST两种方式,并使用python搭建简易的文件服务器来实际测试文件上传的效果。

相关推荐
龚建波18 天前
记录:vcpkg清单模式安装指定版本的curl和OpenSSL
openssl·curl·vcpkg
YouEmbedded18 天前
解码ARM 开发板 OpenSSL+cURL 交叉编译与 HTTPS 配置
https·openssl·curl
大米粥哥哥1 个月前
c++ libcurl报错Send failed since rewinding of the data stream failed【已解决】
开发语言·c++·http·curl·rewind
idolyXyz2 个月前
[curl-http3: 基于quiche+boringssl编译]
http3·curl·boringssl·quiche
叫我詹躲躲2 个月前
Linux 服务器磁盘满了?教你快速找到大文件,安全删掉不踩坑!
linux·前端·curl
叫我詹躲躲2 个月前
3 分钟搞定 Linux 磁盘清理:实用命令 + 自动脚本,新手也会
linux·curl
大米粥哥哥4 个月前
Qt libcurl的下载、配置及简单测试 (windows环境)
开发语言·c++·windows·qt·http·curl·libcurl
xchenhao4 个月前
Elasticsearch 常用任务管理命令及实战应用
elasticsearch·webhook·curl·查询·任务·告警·bot
xiaok4 个月前
使用curl在Linux中测试HTTP请求
curl