curl库应用-c++客户端示例及golang服务端应用示例
1. curl简介
curl是一个开源的命令行工具和库,用于在各种协议(如HTTP、HTTPS、FTP等)上进行数据传输。它支持多种平台,包括Windows、Linux和macOS,是许多应用程序进行网络通信的重要工具。
在本项目中,我们使用libcurl库来实现HTTP请求功能,包括GET和POST请求、文件上传和下载等功能。libcurl提供了简单易用的API,允许我们在C++应用程序中轻松地与Web服务器进行通信。
libcurl的主要特点:
- 支持多种协议:HTTP、HTTPS、FTP、FTPS、SCP、SFTP等
- 跨平台支持:Windows、Linux、macOS等
- 多种编程语言接口
- 稳定可靠,广泛应用于各种项目中
- 支持SSL/TLS加密通信
2. curl下载安装配置
下载和安装
- 访问curl官方网站 curl.se/download.ht... 下载适用于您系统的预编译版本。
- 对于Windows平台,推荐下载预编译的二进制包,例如Win64 - Generic (带有SSL)版本。
- 解压下载的压缩包到指定目录,例如
C:/MinGW/curl-8.15.0_4-win64-mingw。
配置
在本项目中,curl已经通过CMake进行了配置。主要配置项包括:
- 包含头文件路径:
#include <curl/curl.h> - 链接库文件:在CMakeLists.txt中链接curl库
cmake
# 查找并链接curl库
find_package(CURL REQUIRED)
target_link_libraries(${PROJECT_NAME} ${CURL_LIBRARIES})
3. HttpClient中curl的封装及使用示例
3.1 HttpClient类结构
HttpClient类是对curl库的封装,提供了以下主要方法:
- HttpGet: 发送HTTP GET请求
- HttpPost: 发送HTTP POST请求
- HttpUploadFile: 上传文件到服务器
- HttpDownloadFile: 从服务器下载文件
3.2 GET和POST方法封装及调用示例
HttpClient.h头文件定义
cpp
#ifndef HTTPCLIENT_H
#define HTTPCLIENT_H
#include <wx/wx.h>
#include <string>
#include <curl/curl.h>
// 用于存储 cURL 接收的数据
struct MemoryStruct {
char* memory;
size_t size;
};
class HttpClient {
public:
// 服务器地址常量
static const std::string SERVER_ADDRESS;
HttpClient();
~HttpClient();
// 原MyFrame中的网络相关方法
std::string HttpPost(const std::string& url, const std::string& jsonData);
std::string HttpGet(const std::string& url);
bool HttpUploadFile(const std::string& url, const wxString& filePath);
bool HttpDownloadFile(const std::string& url, const std::string& filePath);
wxString URLEncode(const wxString& str);
// 合并URL的方法
std::string BuildUrl(const std::string& relativePath) const;
private:
// 原MyFrame中的回调函数
static size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb, void* userp);
static size_t WriteFileCallback(void* contents, size_t size, size_t nmemb, FILE* fp);
};
#endif // HTTPCLIENT_H
GET方法封装示例
cpp
// 发送 GET 请求下载 JSON 数据
std::string HttpClient::HttpGet(const std::string& url) {
// 构建完整URL
std::string fullUrl = BuildUrl(url);
CURL* curl;
CURLcode res;
MemoryStruct chunk;
// 初始化内存块
chunk.memory = static_cast<char*>(malloc(1)); // 初始化为空字符串
chunk.size = 0;
curl = curl_easy_init();
if (curl) {
// 设置 URL
curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str());
// 设置回调函数以接收响应数据
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&chunk);
// 执行请求
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
// 请求失败,记录错误信息
std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
}
else {
// 请求成功,chunk.memory 中存储了响应数据
// 将响应数据转换为 std::string
std::string response(chunk.memory);
std::cout << "Response: " << response << std::endl;
// 返回响应数据
return response;
}
// 清理
curl_easy_cleanup(curl);
}
else {
// curl 初始化失败
std::cerr << "curl_easy_init() failed" << std::endl;
}
free(chunk.memory); // 释放内存
// 如果发生错误,返回空字符串
return "";
}
POST方法封装示例
cpp
// 发送 POST 请求
std::string HttpClient::HttpPost(const std::string& url, const std::string& jsonData) {
// 构建完整URL
std::string fullUrl = BuildUrl(url);
CURL* curl;
CURLcode res;
MemoryStruct chunk;
std::string returnData = ""; // 用于存储返回的 JSON 字符串
chunk.memory = static_cast<char*>(malloc(1)); // 初始化为空字符串
chunk.size = 0;
curl = curl_easy_init();
if (curl) {
// 设置 URL
curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str());
// 设置 POST 数据
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str());
// 设置回调函数以接收响应数据
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&chunk);
// 设置 HTTP 头(可选,例如设置 Content-Type 为 application/json)
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json; charset=utf-8");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// 执行请求
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
// 请求失败,记录错误信息
std::string errorMsg = "curl_easy_perform() failed: " + std::string(curl_easy_strerror(res));
wxLogError(wxString::FromUTF8(errorMsg.c_str()));
returnData = errorMsg; // 返回错误信息
return returnData;
}
else {
// 请求成功,chunk.memory 中存储了响应数据
std::string response(chunk.memory); // 将响应数据转换为 std::string
return response;
}
// 清理
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
else {
// curl 初始化失败
std::string curlInitErrorMsg = "curl_easy_init() failed";
wxLogError(wxString::FromUTF8(curlInitErrorMsg.c_str()));
returnData = curlInitErrorMsg; // 返回初始化失败信息
return returnData;
}
free(chunk.memory); // 释放内存
return ""; // 返回响应数据或错误信息
}
回调函数实现
cpp
// cURL 的回调函数,用于接收数据
size_t HttpClient::WriteMemoryCallback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t totalSize = size * nmemb;
MemoryStruct* mem = static_cast<MemoryStruct*>(userp);
char* ptr = static_cast<char*>(realloc(mem->memory, mem->size + totalSize + 1));
if (!ptr) {
std::cerr << "Not enough memory (realloc returned NULL)" << std::endl;
return 0;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, totalSize);
mem->size += totalSize;
mem->memory[mem->size] = 0; // Null-terminate the string
return totalSize;
}
3.3 文件上传和下载方法封装及调用示例
文件上传方法封装
cpp
// 上传文件到服务器
bool HttpClient::HttpUploadFile(const std::string& url, const wxString& filePath) {
// 构建完整URL
std::string fullUrl = BuildUrl(url);
// 使用curl上传文件
CURL* curl;
CURLcode res;
struct curl_httppost* formpost = NULL;
struct curl_httppost* lastptr = NULL;
struct curl_slist* headerlist = NULL;
// 获取文件名
wxFileName fileName(filePath);
wxString fileNameStr = fileName.GetFullName();
// 初始化curl
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if (curl) {
// 创建表单数据
// 对中文路径进行处理
std::string utf8FilePath = wxString::FromUTF8(filePath.ToUTF8());
// 调试:输出转换后的路径(方便排查问题)
wxLogMessage("转换后的文件路径: %s", utf8FilePath.c_str());
curl_formadd(&formpost,
&lastptr,
CURLFORM_COPYNAME, "file",
CURLFORM_FILE, utf8FilePath.c_str(),
CURLFORM_END);
// 对中文文件名进行特殊处理
wxString encodedFileName = fileNameStr;
// 将文件名转换为UTF-8格式的std::string
std::string utf8FileName = URLEncode(wxString::FromUTF8(fileNameStr.ToUTF8()));
wxLogMessage("转换后的文件名: %s", utf8FileName.c_str());
curl_formadd(&formpost,
&lastptr,
CURLFORM_COPYNAME, "filename",
CURLFORM_COPYCONTENTS, utf8FileName.c_str(),
CURLFORM_END);
// 设置请求头,明确指定UTF-8编码
headerlist = curl_slist_append(headerlist, "Accept: application/json");
headerlist = curl_slist_append(headerlist, "Content-Type: multipart/form-data; charset=utf-8");
headerlist = curl_slist_append(headerlist, "Expect:"); // 禁用100-Continue预期
// 设置curl选项
curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 仅用于测试,生产环境需启用
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // 仅用于测试,生产环境需启用
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
// 执行上传请求
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
wxString errorMsg = wxString::Format("文件上传失败: %s", curl_easy_strerror(res));
wxLogError(errorMsg);
wxMessageBox(errorMsg, "错误", wxOK | wxICON_ERROR);
// 清理资源
curl_formfree(formpost);
curl_slist_free_all(headerlist);
curl_easy_cleanup(curl);
curl_global_cleanup();
return false;
}
else {
long response_code;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
if (response_code == 200) {
wxString successMsg = wxString::Format("文件上传成功: %s", fileNameStr);
wxLogMessage(successMsg);
wxMessageBox(successMsg, "成功", wxOK | wxICON_INFORMATION);
return true;
}
else {
wxString errorMsg = wxString::Format("文件上传失败,服务器返回状态码: %d", response_code);
wxLogError(errorMsg);
wxMessageBox(errorMsg, "错误", wxOK | wxICON_ERROR);
return false;
}
}
// 清理资源
curl_formfree(formpost);
curl_slist_free_all(headerlist);
curl_easy_cleanup(curl);
}
else {
wxLogError("curl初始化失败");
wxMessageBox("curl初始化失败", "错误", wxOK | wxICON_ERROR);
return false;
}
curl_global_cleanup();
return true;
}
文件下载方法封装
cpp
// 为文件下载创建专用的写入回调函数
size_t HttpClient::WriteFileCallback(void* contents, size_t size, size_t nmemb, FILE* fp) {
size_t totalSize = size * nmemb;
if (fwrite(contents, size, nmemb, fp) != nmemb) {
std::cerr << "Failed to write to file" << std::endl;
return 0;
}
return totalSize;
}
// 下载文件到本地
bool HttpClient::HttpDownloadFile(const std::string& url, const std::string& filePath) {
// 构建完整URL
std::string fullUrl = BuildUrl(url);
CURL* curl;
CURLcode res;
FILE* fp;
// 初始化libcurl
curl = curl_easy_init();
if (curl) {
// 打开文件以写入二进制数据
fp = fopen(filePath.c_str(), "wb");
if (!fp) {
std::cerr << "Failed to open file for writing: " << filePath << std::endl;
curl_easy_cleanup(curl);
return false;
}
// 设置 URL
curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str());
// 设置写入文件的回调函数和数据
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteFileCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
// 设置跟随重定向
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// 设置用户代理
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
// 设置连接超时
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
// 设置接受PDF内容类型
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
// 执行请求
res = curl_easy_perform(curl);
// 检查执行结果
if (res != CURLE_OK) {
std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
fclose(fp);
// 删除可能创建的空文件
remove(filePath.c_str());
curl_easy_cleanup(curl);
return false;
}
else {
// 获取HTTP响应码
long response_code;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
if (response_code == 200) {
std::cout << "File downloaded successfully: " << filePath << std::endl;
}
else {
std::cerr << "HTTP error: " << response_code << std::endl;
fclose(fp);
// 删除可能创建的无效文件
remove(filePath.c_str());
curl_easy_cleanup(curl);
return false;
}
}
// 清理
fclose(fp);
curl_easy_cleanup(curl);
}
else {
std::cerr << "Failed to initialize curl" << std::endl;
return false;
}
return true; // 返回是否成功
}
URL编码方法
cpp
// URL编码函数,用于处理包含中文的参数
wxString HttpClient::URLEncode(const wxString& str) {
wxString encodedStr;
// 将整个字符串转换为UTF-8
wxCharBuffer utf8Buf = str.ToUTF8();
const char* utf8Data = utf8Buf.data();
if (!utf8Data) return "";
size_t utf8Len = strlen(utf8Data);
// 对UTF-8字节序列中的每个字节进行处理
for (size_t i = 0; i < utf8Len; ++i) {
unsigned char byte = static_cast<unsigned char>(utf8Data[i]);
// 对于字母、数字和一些特殊字符,直接添加
if ((byte >= 'A' && byte <= 'Z') ||
(byte >= 'a' && byte <= 'z') ||
(byte >= '0' && byte <= '9') ||
byte == '-' || byte == '_' || byte == '.' || byte == '~') {
encodedStr += static_cast<wxChar>(byte);
}
else {
// 对于其他字符,进行百分号编码
wxString hex;
hex.Printf("%%%02X", byte);
encodedStr += hex;
}
}
return encodedStr;
}
URL构建方法
cpp
// 合并URL的方法
std::string HttpClient::BuildUrl(const std::string& relativePath) const {
// 如果relativePath已经是完整URL,则直接返回
if (relativePath.find("http://") == 0 || relativePath.find("https://") == 0) {
return relativePath;
}
// 确保SERVER_ADDRESS末尾没有斜杠
std::string base = SERVER_ADDRESS;
if (!base.empty() && base.back() == '/') {
base.pop_back();
}
// 确保relativePath开头有斜杠
std::string path = relativePath;
if (path.empty()) {
return base;
}
if (path.front() != '/') {
path = "/" + path;
}
return base + path;
}
4. MyFrame.cpp中调用HttpClient示例
在MyFrame.cpp中,HttpClient被作为成员变量使用,通过调用其方法实现各种网络操作。
4.1 POST请求调用示例
cpp
// Post方式发送Json数据
void MyFrame::OnPost(wxCommandEvent& event) {
// 示例 JSON 数据
std::string jsonData = R"({"name": "John", "age": 30})";
// 目标 URL
std::string url = "/json";
// 发送 POST 请求
std::string response = httpClient.HttpPost(url, jsonData);
if (response.empty()) {
// 如果返回的响应为空,可能是请求失败
wxLogError("POST request failed or returned empty response.");
m_textCtrl->SetValue("POST Request Failed: Empty response.");
return;
}
// 处理响应...
}
4.2 GET请求调用示例
cpp
// Get方式获取Json数据
void MyFrame::OnGet(wxCommandEvent& event) {
// 目标 URL
std::string url = "/json";
// 发送 GET 请求
std::string response = httpClient.HttpGet(url);
if (response.empty()) {
// 如果返回的响应为空,可能是请求失败
wxLogError("GET request failed or returned empty response.");
m_textCtrl->SetValue("GET Request Failed: Empty response.");
return;
}
// 处理响应...
}
4.3 文件上传调用示例
cpp
// 上传文件
void MyFrame::OnUploadFile(wxCommandEvent& event) {
// 获取文件路径
wxString filePath = m_textCtrl_FilePath->GetValue();
if (filePath.IsEmpty()) {
wxMessageBox("请先选择文件", "提示", wxOK | wxICON_INFORMATION);
return;
}
// 检查文件是否存在
if (!wxFile::Exists(filePath)) {
wxMessageBox("选择的文件不存在", "错误", wxOK | wxICON_ERROR);
return;
}
// 构造上传URL
std::string uploadUrl = "/upload";
// 上传文件
if (httpClient.HttpUploadFile(uploadUrl, filePath)) {
// 上传成功后刷新文件列表
OnGetFileLists(event);
}
else {
m_textCtrl->SetValue("File download failed.");
}
}
4.4 文件下载调用示例
cpp
// 下载文件
void MyFrame::OnDownloadFile(wxCommandEvent& event) {
wxString selectedFile = m_comboBox->GetStringSelection();
if (selectedFile.IsEmpty()) {
wxMessageBox("请先选择文件", "提示", wxOK | wxICON_INFORMATION);
return;
}
// 获取文件名
wxFileName fileName(selectedFile);
std::string utf8FileName = httpClient.URLEncode(wxString::FromUTF8(fileName.ToUTF8()));
wxLogMessage("转换后的文件名: %s", utf8FileName.c_str());
// 构造下载URL,将文件名作为查询参数传递
std::string fileUrl = wxString::Format("/download?filename=%s", utf8FileName.c_str());
// 保存文件的本地路径 - 使用绝对路径
std::string filePath = wxGetCwd().ToStdString() + "/download/" + wxFileName::GetPathSeparator() + fileName.ToStdString();
// 输出日志
wxLogMessage("Saving file to: %s", filePath);
// 下载文件
if (httpClient.HttpDownloadFile(fileUrl, filePath)) {
// 处理下载后的文件...
}
}
总结
以上展示了在本项目中如何使用libcurl库实现HTTP通信功能。通过HttpClient类的封装,我们可以方便地进行GET/POST请求以及文件上传下载操作。这些功能对于现代C++应用程序来说至关重要,特别是在需要与Web服务进行交互的场景中。
在实际使用中,需要注意以下几点:
- 正确处理内存分配和释放,避免内存泄漏
- 对于包含中文的内容,需要正确进行编码转换
- 合理设置超时时间,提高程序健壮性
- 错误处理要全面,提供友好的错误提示
- 在MyFrame类中通过成员变量的方式使用HttpClient,实现了良好的封装性
通过这种设计模式,我们将网络通信功能与UI逻辑分离,提高了代码的可维护性和可扩展性。
Go服务端应用示例
1. 目录结构
csharp
goServer/
├── files/ # 上传文件存储目录
│ ├── *.pdf
│ ├── *.xlsx
│ ├── *.md
│ └── *.txt
├── static/ # 静态资源目录
│ ├── css/
│ ├── js/
│ └── views/
├── go.mod # Go模块定义文件
├── go.sum # Go模块校验和文件
└── main.go # 主程序文件
2. main.go主程序文件
2.1 导入包和数据结构定义
go
package main
import (
// "encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
_ "github.com/denisenkom/go-mssqldb"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"xorm.io/xorm"
"github.com/xuri/excelize/v2"
)
// 定义一个简单的 JSON 数据结构,用于接收客户端发送的 POST 数据
type Data struct {
Name string `json:"name"`
Age int `json:"age"`
}
// 在main函数中添加新的处理函数之前,先定义一个用于表示文件信息的结构体
type FileInfo struct {
Name string `json:"name"`
Size string `json:"size"`
Path string `json:"path"`
}
// User 用户模型
type User struct {
Id int64 `json:"id" xorm:"bigint pk autoincr"`
Name string `json:"name" xorm:"varchar(255)"`
Email string `json:"email" xorm:"varchar(255)"`
Age int `json:"age" xorm:"int"`
}
2.2 主函数实现
go
func main() {
// 初始化XORM引擎连接MSSQL数据库
// 需要替换为实际的数据库连接信息
engine, err := xorm.NewEngine("mssql", "server=localhost,1888;user id=sa;password=19801122;database=invoice;encrypt=disable")
if err != nil {
fmt.Printf("Failed to connect to database: %v\n", err)
return
}
defer engine.Close()
// 显示SQL语句
engine.ShowSQL(true)
// 同步表结构
err = engine.Sync2(new(User))
if err != nil {
fmt.Printf("Failed to sync database: %v\n", err)
return
}
app := iris.New()
// 然后在路由配置中使用这个中间件
app.Use(noCacheMiddleware)
// 提供静态文件服务,将 resources 目录映射为 /static/ 路径
app.HandleDir("/static", iris.Dir("static"))
// 设置用户控制器的MVC路由
userRouter := mvc.New(app.Party("/users"))
userRouter.Register(engine)
userRouter.Handle(new(UserController))
// 添加自定义路由支持按姓名、邮箱和年龄查询
users := app.Party("/users")
users.Get("/name/{name}", func(ctx iris.Context) {
name := ctx.Params().Get("name")
userController := &UserController{Engine: engine}
ctx.JSON(userController.GetByName(name))
})
users.Get("/email/{email}", func(ctx iris.Context) {
email := ctx.Params().Get("email")
userController := &UserController{Engine: engine}
ctx.JSON(userController.GetByEmail(email))
})
users.Get("/age/{age:int}", func(ctx iris.Context) {
age, _ := ctx.Params().GetInt("age")
userController := &UserController{Engine: engine}
ctx.JSON(userController.GetByAge(age))
})
// 设置文件控制器的MVC路由
fileRouter := mvc.New(app.Party("/"))
fileRouter.Handle(new(FileController))
// 启动服务器
app.Listen(":8080")
}
3. 服务端GET和POST接收curl上传的数据示例代码
3.1 GET请求处理示例
go
// GetJson 处理 GET 请求,返回 JSON 数据
func (fc *FileController) GetJson(ctx iris.Context) {
// 原来的JSON数据返回功能
jsonData := []map[string]interface{}{
{"name": "John", "age": 30},
{"name": "Mike", "age": 25},
}
// 返回一个简单的 JSON 数据
response := map[string]interface{}{
"message": "Get from Go server!",
"data": map[string]interface{}{
"status": "success",
"code": 200,
"datalist": jsonData,
},
}
ctx.JSON(response)
}
3.2 POST请求处理示例
go
// PostJson 处理 POST 请求,接收 JSON 数据
func (fc *FileController) PostJson(ctx iris.Context) {
// 解析客户端发送的 JSON 数据
var data Data
err := ctx.ReadJSON(&data)
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.JSON(map[string]string{"error": "Invalid JSON"})
return
}
// 打印接收到的 JSON 数据
fmt.Printf("Received JSON data: %+v\n", data)
// 返回响应,表示数据已接收
jsonData := []map[string]interface{}{
{"name": "赵大", "age": 46},
{"name": "钱二", "age": 45},
{"name": "张三", "age": 35},
{"name": "李四", "age": 44},
{"name": "周五", "age": 39},
{"name": "吴六", "age": 42},
{"name": "郑七", "age": 38},
{"name": "王八", "age": 42},
}
// 返回一个简单的 JSON 数据
response := map[string]interface{}{
"message": "Post from Go server!",
"data": map[string]interface{}{
"status": "success",
"code": 200,
"datalist": jsonData,
},
}
ctx.JSON(response)
}
4. 接收curl上传的文件或curl下载文件的示例代码
4.1 文件上传处理示例
go
// PostUpload 处理 POST 请求,接收上传的文件
func (fc *FileController) PostUpload(ctx iris.Context) {
// 解析表单数据,最大内存为 32MB
err := ctx.Request().ParseMultipartForm(32 << 20)
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Text("File upload error")
return
}
// 获取上传的文件
file, fileHeader, err := ctx.FormFile("file")
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Text("File upload error")
return
}
defer file.Close()
// 获取文件名
filename := ctx.FormValue("filename")
// 进行URL解码,将c++上传的中文文件名进行解码
decodedStr, err := url.QueryUnescape(filename)
if err != nil {
fmt.Printf("解码失败: %v\n", err)
ctx.StatusCode(iris.StatusBadRequest)
ctx.Text("Filename decode error")
return
}
fmt.Printf("解码前: %s\n", filename)
fmt.Printf("解码后: %s\n", decodedStr) // 输出: 王明
// 示例:打印文件名
println("接收的文件名:", decodedStr)
// 创建目标文件路径
// 注意:这里将文件保存到 files 目录下
// 在实际应用中,你可能需要根据文件类型或用户信息创建子目录
dst, err := os.Create("./files/" + decodedStr)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("File save error")
return
}
defer dst.Close()
// 将上传的文件内容复制到目标文件
_, err = io.Copy(dst, file)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("File save error")
return
}
// 返回成功响应
ctx.JSON(map[string]interface{}{
"message": "File uploaded successfully",
"data:": map[string]interface{}{
"status": "success",
"code": 200,
"filename": decodedStr,
"size": fileHeader.Size,
},
})
fmt.Printf("File uploaded: %s\n", decodedStr)
}
4.2 文件下载处理示例
go
// GetDownload 处理 GET 请求,提供文件下载功能
func (fc *FileController) GetDownload(ctx iris.Context) {
// 从查询参数获取文件名
fileName := ctx.URLParam("filename")
if fileName == "" {
fileName = "default.txt" // 默认文件名
}
// 进行URL解码,将c++上传的中文文件名进行解码
filePath, err := url.QueryUnescape(fileName)
if err != nil {
fmt.Printf("解码失败: %v\n", err)
return
}
fmt.Printf("解码前: %s\n", fileName)
fmt.Printf("解码后: %s\n", filePath) // 输出: 王明
// 示例:打印文件名
println("接收的文件名:", filePath)
filePath = "./files/" + filePath
// 构造文件路径(这里可以根据需要调整)
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// 如果文件不存在,创建一个示例文件并写入内容
err := createSampleFile(filePath)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Failed to create sample file")
return
}
}
// 打开文件
file, err := os.Open(filePath)
if err != nil {
ctx.StatusCode(iris.StatusNotFound)
ctx.Text("File not found")
return
}
defer file.Close()
// 获取文件信息
fileInfo, err := file.Stat()
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Error getting file info")
return
}
// 设置响应头,指定文件下载
ctx.Header("Content-Disposition", "attachment; filename="+fileInfo.Name())
contentType := getContentTypeWithMime(filePath)
ctx.ContentType(contentType)
ctx.Header("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
// 将文件内容写入响应
_, err = io.Copy(ctx.ResponseWriter(), file)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Error sending the file")
return
}
fmt.Printf("File downloaded: %s\n", fileInfo.Name())
}
4.3 获取Content-Type的辅助函数
go
// 使用mime包根据扩展名获取Content-Type
func getContentTypeWithMime(filePath string) string {
ext := strings.ToLower(filepath.Ext(filePath))
contentType := mime.TypeByExtension(ext)
// 如果找不到对应的MIME类型,使用默认值
if contentType == "" {
return "application/octet-stream"
}
return contentType
}
4.4 文件预览处理示例
go
// GetPreview 处理文件预览请求
func (fc *FileController) GetPreview(ctx iris.Context) {
// 从查询参数获取文件名
fileName := ctx.URLParam("filename")
if fileName == "" {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Text("Filename not specified")
return
}
// 获取action参数
queryAction := ctx.URLParam("action")
// 构造文件路径
filePath := fileName
// 检查文件是否存在
if _, err := os.Stat("./files/" + filePath); os.IsNotExist(err) {
ctx.StatusCode(iris.StatusNotFound)
ctx.Text("File not found")
return
}
// 根据文件扩展名处理不同类型的文件
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".html", ".htm":
// HTML文件直接显示,但确保编码正确
content, err := os.ReadFile("./files/" + filePath)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Error reading the file")
return
}
ctx.ContentType("text/html; charset=utf-8")
ctx.Write(content)
case ".md", ".markdown":
// Markdown文件转换为HTML显示
file, err := os.Open("./files/" + filePath)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Error opening file")
return
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Error reading the file")
return
}
// 对content中的`进行转义\`
newContent := []byte(strings.ReplaceAll(string(content), "`", "`"))
// 包装在完整的HTML页面中
html := convertMarkdownToHTML(string(newContent))
ctx.ContentType("text/html; charset=utf-8")
ctx.Write([]byte(html))
case ".xlsx":
// XLSX文件处理
if queryAction == "edit" {
// 编辑模式
htmlContent, err := convertXlsxToEditableHTML(filePath, fileName)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("%s", "Error converting XLSX to editable HTML: " + err.Error())
return
}
ctx.ContentType("text/html; charset=utf-8")
ctx.Write([]byte(htmlContent))
} else {
// 预览模式
htmlContent, err := convertXlsxToPreviewHTML(filePath)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("%s", "Error converting XLSX to HTML: " + err.Error())
return
}
ctx.ContentType("text/html; charset=utf-8")
ctx.Write([]byte(htmlContent))
}
default:
// 其他文件类型处理
// 检查是否是文本文件
contentType := getContentTypeWithMime("./files/" + filePath)
if strings.HasPrefix(contentType, "text/") {
// 文本文件直接显示
content, err := os.ReadFile("./files/" + filePath)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Text("Error reading the file")
return
}
ctx.ContentType("text/plain; charset=utf-8")
ctx.Write(content)
} else {
// 其他类型文件作为附件下载
ctx.Header("Content-Disposition", "attachment; filename="+fileName)
ctx.ContentType(contentType)
ctx.ServeFile("./files/" + filePath)
}
}
}
4.5 Go服务端HTML转换函数示例
4.5.1. convertMarkdownToHTML函数
go
// 简单的Markdown转HTML函数(实际项目中建议使用专业的库如github.com/russross/blackfriday)
func convertMarkdownToHTML(markdown string) string {
html := `<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown 预览</title>
<script src="/static/js/marked.min.js"></script>
<script src="/static/js/purify.min.js"></script>
<link rel="stylesheet" href="/static/css/default.min.css">
<script src="/static/js/highlight.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
line-height: 1.6;
}
#content {
padding: 10px;
}
pre {
background: #f5f5f5;
padding: 10px;
border-radius: 3px;
overflow-x: auto;
}
code {
font-family: Consolas, monospace;
}
</style>
</head>
<body>
<div id="content"></div>
<script>
marked.setOptions({
highlight: function (code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
}
});
const markdownContent = ` + "`" + markdown + "`;" + `
document.getElementById('content').innerHTML =
DOMPurify.sanitize(marked.parse(markdownContent));
hljs.highlightAll();
</script>
</body>
</html>`
return html
}
该函数用于将Markdown格式的文本转换为带有语法高亮的HTML页面。主要特点包括:
- 使用marked.js库解析Markdown语法
- 使用DOMPurify库进行HTML净化,防止XSS攻击
- 使用highlight.js库实现代码语法高亮
- 包含基础的CSS样式以提升可读性
4.5.2. convertXlsxToPreviewHTML函数
go
// 将XLSX文件转换为预览HTML
func convertXlsxToPreviewHTML(filePath string) (string, error) {
f, err := excelize.OpenFile("./files/" +filePath)
if err != nil {
return "", err
}
defer f.Close()
// 获取所有工作表名称
sheets := f.GetSheetList()
html := `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>XLSX文件预览</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.sheet-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.sheet-tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid #ddd;
border-bottom: none;
border-radius: 5px 5px 0 0;
background-color: #f0f0f0;
margin-right: 5px;
}
.sheet-tab.active {
background-color: #007bff;
color: white;
}
.sheet-content {
display: none;
}
.sheet-content.active {
display: block;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.edit-link {
background-color: #28a745;
color: white;
padding: 8px 16px;
text-decoration: none;
border-radius: 4px;
}
.edit-link:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
`
html += fmt.Sprintf(
`<li><h1>XLSX文件预览<a href="/preview?filename=%s&action=edit" class="edit-link">编辑</a></h1></li>`+"\n",
filePath)
html += ` <div class="sheet-tabs">`
// 生成工作表标签
for i, sheet := range sheets {
class := ""
if i == 0 {
class = "active"
}
html += fmt.Sprintf(`<div class="sheet-tab %s" onclick="showSheet('%s')">%s</div>`, class, sheet, sheet)
}
html += `
</div>
`
// 为每个工作表生成内容
for i, sheet := range sheets {
class := ""
if i == 0 {
class = "active"
}
rows, err := f.GetRows(sheet)
if err != nil {
return "", err
}
html += fmt.Sprintf(`<div id="%s" class="sheet-content %s">`, sheet, class)
html += "<table>"
// 生成表格行
for rowNum, row := range rows {
html += "<tr>"
// 添加行号
html += fmt.Sprintf("<td style='background-color: #f2f2f2; font-weight: bold;'>%d</td>", rowNum+1)
for _, cell := range row {
html += "<td>" + cell + "</td>"
}
html += "</tr>"
}
html += "</table>"
html += "</div>"
}
html += `
</div>
<script>
function showSheet(sheetName) {
// 隐藏所有工作表内容
var contents = document.getElementsByClassName('sheet-content');
for (var i = 0; i < contents.length; i++) {
contents[i].classList.remove('active');
}
// 移除所有标签的活动状态
var tabs = document.getElementsByClassName('sheet-tab');
for (var i = 0; i < tabs.length; i++) {
tabs[i].classList.remove('active');
}
// 显示选中的工作表内容
document.getElementById(sheetName).classList.add('active');
// 设置选中标签为活动状态
event.target.classList.add('active');
}
</script>
</body>
</html>`
return html, nil
}
该函数用于将XLSX文件转换为只读预览的HTML页面。主要特点包括:
- 使用excelize库读取XLSX文件内容
- 支持多工作表展示,通过标签页切换
- 为每个工作表生成表格形式的展示
- 提供编辑链接,可以跳转到编辑页面
- 包含美观的CSS样式和交互式JavaScript
4.5.3. convertXlsxToEditableHTML函数
go
// 将XLSX文件转换为可编辑的HTML
func convertXlsxToEditableHTML(filePath, fileName string) (string, error) {
f, err := excelize.OpenFile("./files/" +filePath)
if err != nil {
return "", err
}
defer f.Close()
// 获取所有工作表名称
sheets := f.GetSheetList()
html := `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>编辑XLSX文件</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 20px;
}
.container {
background-color: white;
padding: 20px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.controls {
margin-bottom: 20px;
}
.btn {
padding: 8px 16px;
background-color: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.btn:hover {
background-color: #5a6268;
}
.btn-success {
background-color: #28a745;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
}
.sheet-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.sheet-tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid #ddd;
border-bottom: none;
border-radius: 5px 5px 0 0;
background-color: #f0f0f0;
margin-right: 5px;
}
.sheet-tab.active {
background-color: #007bff;
color: white;
}
.sheet-content {
display: none;
}
.sheet-content.active {
display: block;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
input[type="text"] {
width: 100%;
box-sizing: border-box;
border: none;
background: transparent;
}
</style>
</head>
<body>
<div class="header">
<h1>编辑XLSX文件</h1>
</div>
<div class="container">
<h1>编辑XLSX文件: ` + fileName + `</h1>
<div class="controls">
<button class="btn btn-success" onclick="saveChanges()">保存更改</button>
<button class="btn" onclick="addRow()">添加行</button>
<button class="btn" onclick="addColumn()">添加列</button>
`
html += fmt.Sprintf(
`<button class="btn btn-danger" onclick="window.location.href='/preview?filename=%s'">取消编辑</button>`,
fileName)
html += `</div><div class="sheet-tabs">`
// 生成工作表标签
for i, sheet := range sheets {
class := ""
if i == 0 {
class = "active"
}
html += fmt.Sprintf(`<div class="sheet-tab %s" onclick="showSheet('%s')">%s</div>`, class, sheet, sheet)
}
html += `
</div>
`
// 为每个工作表生成可编辑内容
for i, sheet := range sheets {
class := ""
if i == 0 {
class = "active"
}
rows, err := f.GetRows(sheet)
if err != nil {
return "", err
}
html += fmt.Sprintf(`<div id="%s" class="sheet-content %s" data-sheet="%s">`, sheet, class, sheet)
html += "<table>"
// 生成表格行
for rowNum, row := range rows {
html += "<tr>"
// 添加行号
html += fmt.Sprintf("<td style='background-color: #f2f2f2; font-weight: bold;'>%d</td>", rowNum+1)
for colNum, cell := range row {
// 转换列号为字母形式 (A, B, C, ...)
colName, _ := excelize.ColumnNumberToName(colNum + 1)
cellName := colName + fmt.Sprintf("%d", rowNum+1)
html += fmt.Sprintf("<td><input type='text' value='%s' data-cell='%s'></td>", cell, cellName)
}
html += "</tr>"
}
html += "</table>"
html += "</div>"
}
html += `
</div>
<script>
function showSheet(sheetName) {
// 隐藏所有工作表内容
var contents = document.getElementsByClassName('sheet-content');
for (var i = 0; i < contents.length; i++) {
contents[i].classList.remove('active');
}
// 移除所有标签的活动状态
var tabs = document.getElementsByClassName('sheet-tab');
for (var i = 0; i < tabs.length; i++) {
tabs[i].classList.remove('active');
}
// 显示选中的工作表内容
document.getElementById(sheetName).classList.add('active');
// 设置选中标签为活动状态
event.target.classList.add('active');
}
function saveChanges() {
var activeSheet = document.querySelector('.sheet-content.active');
var sheetName = activeSheet.getAttribute('data-sheet');
var inputs = activeSheet.querySelectorAll('input');
var changes = [];
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
var cell = input.getAttribute('data-cell');
var value = input.value;
changes.push({cell: cell, value: value});
}
// 发送更改到服务器
fetch('/save/xlsx', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: '` + fileName + `',
sheet: sheetName,
changes: changes
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('保存成功');
} else {
alert('保存失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('保存失败: ' + error.message);
});
}
function addRow() {
var activeSheet = document.querySelector('.sheet-content.active');
var table = activeSheet.querySelector('table');
var rows = table.querySelectorAll('tr');
var cellCount = rows[0].querySelectorAll('td').length - 1; // 减去行号列
var newRow = document.createElement('tr');
// 添加行号
var rowNumber = rows.length;
newRow.innerHTML = '<td style="background-color: #f2f2f2; font-weight: bold;">' + rowNumber + '</td>';
// 添加单元格
for (var i = 0; i < cellCount; i++) {
var colName = String.fromCharCode(65 + i); // A, B, C...
var cellName = colName + rowNumber;
newRow.innerHTML += '<td><input type="text" data-cell="' + cellName + '"></td>';
}
table.appendChild(newRow);
}
function addColumn() {
var activeSheet = document.querySelector('.sheet-content.active');
var table = activeSheet.querySelector('table');
var rows = table.querySelectorAll('tr');
var cellCount = rows[0].querySelectorAll('td').length - 1; // 减去行号列
// 计算新的列名 (如: A, B, ..., Z, AA, AB...)
var colName = "";
var colIndex = cellCount;
while (colIndex >= 0) {
colName = String.fromCharCode(65 + (colIndex % 26)) + colName;
colIndex = Math.floor(colIndex / 26) - 1;
}
// 为每一行添加新列
for (var i = 0; i < rows.length; i++) {
var rowNum = i + 1;
var cellName = colName + rowNum;
var newCell = document.createElement('td');
if (i === 0) {
// 表头行
newCell.style.backgroundColor = '#f2f2f2';
newCell.style.fontWeight = 'bold';
newCell.textContent = colName;
} else {
// 数据行
newCell.innerHTML = '<input type="text" data-cell="' + cellName + '">';
}
rows[i].appendChild(newCell);
}
}
</script>
</body>
</html>`
return html, nil
}
- 该函数用于将XLSX文件转换为可编辑的HTML页面。主要特点包括:
- 使用excelize库读取XLSX文件内容
- 为每个单元格生成可编辑的输入框
- 支持多工作表展示和切换
- 提供保存、添加行、添加列等功能
- 通过JavaScript实现前端交互和后端通信
- 这三个函数分别处理了不同类型的文件转换需求:
- convertMarkdownToHTML - 专门处理Markdown文档,转换为带有语法高亮的HTML页面
- convertXlsxToPreviewHTML - 将XLSX文件转换为只读预览页面,支持多工作表展示
- convertXlsxToEditableHTML - 将XLSX文件转换为可编辑页面,支持在线编辑和保存功能
这些函数共同构成了Go服务端的文件预览和编辑功能,为用户提供了一个完整的在线文档处理解决方案。
总结
以上展示了Go服务端如何处理curl客户端发送的GET/POST请求以及文件上传下载功能。通过Iris框架,我们可以方便地处理各种HTTP请求,并根据文件类型提供不同的处理方式。
主要特点包括:
- 支持JSON数据的接收和返回
- 支持文件上传和下载
- 支持多种文件类型的预览(HTML、Markdown、XLSX等)
- 对中文文件名进行URL解码处理
- 根据文件扩展名设置正确的Content-Type
- 提供错误处理和日志记录功能
这些功能使得Go服务端能够与使用curl库的C++客户端进行有效通信,实现完整的文件传输和数据交互功能。