之前的一篇文章,介绍过使用curl编程来下载文件。本篇,继续介绍如何使用curl编程来上传文件。
1 基础概念
1.1 文件上传的HTTP方法
PUT 和 POST 是 HTTP 协议中用于向服务器提交数据的请求方法
- PUT方法:用于向服务器推送一个已明确标识的资源,强调 "更新" 的动作(若资源不存在则创建),是幂等操作
- POST方法:用于向服务器提交新资源或触发一次非幂等的操作,强调 "创建"的动作
1.2 幂等性
幂等性 是 HTTP 协议中的核心概念,指多次执行同一个请求,得到的服务器资源状态和执行一次的结果完全一致,不会产生额外的副作用
- 幂等:第一次上传会创建该文件,第二次上传同一个或不同内容的文件,会直接覆盖掉原来的文
- 非幂等:每次请求服务器都会生成一个新的文件副本(比如自动命名
file1.txtfile2.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搭建简易的文件服务器来实际测试文件上传的效果。