• 正文
  • 相关推荐
申请入驻 产业图谱

curl编程实例-上传文件

12/23 14:45
135
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

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

1 基础概念

1.1 文件上传的HTTP方法

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

PUT方法:用于向服务器推送一个已明确标识的资源,强调 “更新”  的动作(若资源不存在则创建),是幂等操作

POST方法:用于向服务器提交新资源或触发一次非幂等的操作,强调 “创建”的动作

1.2 幂等性

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

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

    file1.txtfile2.txt

      ),多次请求会产生多个文件

2 curl文件上传

下面通过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", (longlong)ulnow, (longlong)ultotal, progress);
    }
    return0;
}

// 提取文件路径中的原始文件名(兼容绝对/相对路径)
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;
    
    constchar *upload_url = "http://192.168.5.104:8080/upload"; // 文件服务器的地址
    constchar *file_path = "./test.txt";  // 要上传的本地文件
    curl_off_t file_size = 0;
    struct curl_slist *headers = NULL;// 自定义请求头
    
    constchar *upload_mode = "PUT";
    if (argc > 1) 
    {
        upload_mode = argv[1]; // PUT或POST模式
    }

    // 打开待上传的文件(二进制模式)
    fp = fopen(file_path, "rb");
    if (!fp) 
    {
        printf("fopen:%s errn", file_path);
        goto cleanup;  // 统一清理资源
    }
    
    // 获取文件大小
    file_size = get_file_size(file_path);
    if (file_size < 0) 
    {
        printf("%s file_size:%lld errn", file_path, (longlong)file_size);
        goto cleanup;
    }
    printf("%s file_size: %lldn", file_path, (longlong)file_size);
        
    // 初始化libcurl
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
    if (!curl) 
    {
        printf("curl_easy_init, errn");
        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 moden");
        curl_easy_setopt(curl, CURLOPT_PUT, 1L);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, file_size);
    }
    else
    {
        // ========== POST 上传模式 ==========
        printf("POST moden");
        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: %sn", file_path);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) 
    {
        printf("upload fail: %sn", curl_easy_strerror(res));
    } 
    else
    {
        printf("upload successn");
    }

    // 资源清理(统一出口)
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 方法

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

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"
        ifnot 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)
        ifnot 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搭建简易的文件服务器来实际测试文件上传的效果。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录