VC++ 与 Golang 的协作:实现 HTTP 文件传输服务

发布于:2025-06-21 ⋅ 阅读:(18) ⋅ 点赞:(0)

在软件开发的工作中,不同编程语言有着各自独特的优势。有时候,为了充分发挥这些优势,我们需要让它们携手合作。本文将详细介绍如何使用 VC++ 调用 Golang 编写的 HTTP 文件传输服务,通过这种跨语言的协作,实现高效的文件传输功能。

项目背景与需求分析

在笔者一个项目,用VC++开发的程序需要用到一个HTTP文件服务。如果用VC++来写可能有些复杂,而golang构建一个稳定高效的HTTP文件传输服务比较容易且高效。因此,将两者结合起来,利用 Golang 构建 HTTP 文件传输服务,再通过 VC++ 进行调用,是一个非常不错的选择。

项目结构与代码实现

1. Golang 部分:构建 HTTP 文件传输服务

我们使用 Golang 编写了一个名为HttpFileServer.go的文件,它包含了 HTTP 文件上传和下载的核心逻辑。以下是该文件的主要内容:

package main

import (
    "C"
    "crypto/rand"
    _ "encoding/base64"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    _ "mime"
    "net/http"
    "net/url"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strconv"
    "strings"
)
import "os/signal"

const maxUploadSize = 1024 * 1024 * 1024
const uploadPath = "/upload"
const downloadPath = "/download"

var gDownFileMap map[string]string
var server *http.Server

//export StartHTTPServer
func StartHTTPServer() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    downfolderPath := filepath.Join(getCurrentPath(), downloadPath)
    os.MkdirAll(downfolderPath, os.ModePerm)
    upfolderPath := filepath.Join(getCurrentPath(), uploadPath)
    os.MkdirAll(upfolderPath, os.ModePerm)
    gDownFileMap = make(map[string]string)

    log.SetFlags(log.LstdFlags | log.Lshortfile)
    file, err := os.Create(filepath.Join(getCurrentPath(), "hfs.log"))
    if err != nil {
        log.Println("| hfs create log file error: ", err)
    } else {
        defer file.Close()

        writers := []io.Writer{
            file,
            os.Stdout,
        }
        log.SetOutput(io.MultiWriter(writers...))
    }

    // 一个通知退出的chan
    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    go func() {
        // 接收退出信号
        <-quit
        if err := server.Close(); err != nil {
            log.Println("| hfs::StartHTTPServerClose server:", err)
        }
    }()

    server := &http.Server{
        Addr: ":8080",
    }

    http.HandleFunc("/upload", uploadFileHandler)
    http.HandleFunc("/upload/", uploadFileHandler)
    http.HandleFunc("/download/", downloadFileHandler)
    log.Println("| hfs::StartHTTPServer download:", downfolderPath, ", upload:", upfolderPath)
    log.Print("| hfs::StartHTTPServer Started Listen Port:8080, use /upload/ for uploading files and /download/{fileName} for downloading")
    err = server.ListenAndServe()
    if err != nil {
        log.Println("| hfs::StartHTTPServer http server error: ", err)
    }
}

//export StopHTTPServer
func StopHTTPServer() {
    err := server.Shutdown(nil)
    if err != nil {
        log.Println("| hfs::StopHTTPServer shutdown the server err: ", err)
    }
}

//export UpdateDownFileMap
func UpdateDownFileMap(key, downPath string) {
    //通过make的方式,新构建一段内存来存放从C++处传入的字符串,深度拷贝防止C++中修改影响Go
    deepCopyStr := func(data string) string {
        byVaule := make([]byte, len(data))
        copy(byVaule, data)
        strVal := string(byVaule)
        return strVal
    }

    fStrKey := deepCopyStr(key)
    fStrDownPath := deepCopyStr(downPath)

    gDownFileMap[fStrKey] = fStrDownPath

    log.Println("UpdateDownFileMap: ", gDownFileMap)
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    downfolderPath := filepath.Join(getCurrentPath(), downloadPath)
    os.MkdirAll(downfolderPath, os.ModePerm)
    upfolderPath := filepath.Join(getCurrentPath(), uploadPath)
    os.MkdirAll(upfolderPath, os.ModePerm)
    gDownFileMap = make(map[string]string)

    http.HandleFunc("/upload/", uploadFileHandler)
    http.HandleFunc("/upload", uploadFileHandler)
    http.HandleFunc("/download/", downloadFileHandler)
    fmt.Println("| hfs: download:", downfolderPath, ", upload:", upfolderPath)
    log.Print("Server Started Listen Port:8080, use /upload/ for uploading files and /download/{fileName} for downloading")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func downloadFileHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    filePath := r.Form["filename"]
    log.Println(filePath)
    if len(filePath) < 1 {
        renderError(w, "INVALID_PARAM\n", http.StatusNotFound)
        log.Printf("downloadFileHandler: INVALID_PARAM\n")
        return
    }

    fileKey := filePath[0]
    url.QueryEscape(fileKey)
    var fileDir string
    if v, ok := gDownFileMap[fileKey]; ok {
        fileDir = v
    } else {
        renderError(w, "INVALID_FILE_KEY_MAP\n", http.StatusNotFound)
        log.Printf("downloadFileHandler: INVALID_FILE_KEY_MAP\n")
        return
    }

    fName := filepath.Base(fileDir)
    log.Println(fName)

    file, err := os.Open(fileDir)
    if err != nil {
        renderError(w, "INVALID_FILE_PATH\n", http.StatusNotFound)
        log.Printf("downloadFileHandler: File(%s) INVALID_FILE_PATH:%s\n", fileDir, err.Error())
        return
    }

    defer file.Close()

    fileHeader := make([]byte, 512)
    file.Read(fileHeader)

    fileStat, _ := file.Stat()
    w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fName))
    w.Header().Set("Content-Type", http.DetectContentType(fileHeader))
    w.Header().Set("Content-Length", strconv.FormatInt(fileStat.Size(), 10))
    w.WriteHeader(http.StatusOK)
    file.Seek(0, 0)
    io.Copy(w, file)
}

func uploadFileHandler(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseMultipartForm(maxUploadSize); err != nil {
        log.Printf("| hfs::uploadFileHandler: Could not parse multipart form: %v\n", err)
        renderError(w, "CANT_PARSE_FORM\n", http.StatusInternalServerError)
        return
    }

    md5 := r.FormValue("md5Code")
    oldFileName := r.FormValue("oldFileName")
    devSN := r.FormValue("devSn")
    contName := r.FormValue("containerName")
    appName := r.FormValue("appName")
    log.Printf("| hfs::uploadFileHandler: md5=%s, oldFileName=%s, devSN=%s", md5, oldFileName, devSN)
    if len(contName) > 0 {
        log.Printf(",containerName=%s", contName)
    }
    if len(appName) > 0 {
        log.Printf(", appName=%s", appName)
    }
    log.Println()

    // parse and validate file and post parameters
    file, fileHeader, err := r.FormFile("file")
    if err != nil {
        renderError(w, "INVALID_FILE\n", http.StatusBadRequest)
        log.Printf("| hfs: uploadFileHandler: File(%s) INVALID_FILE\n", oldFileName)
        return
    }
    defer file.Close()
    // Get and print out file size
    fileSize := fileHeader.Size
    log.Printf("| hfs::uploadFileHandler: File(%s) size (bytes): %v\n", fileHeader.Filename, fileSize)
    // validate file size
    if fileSize > maxUploadSize {
        renderError(w, "FILE_TOO_BIG\n", http.StatusBadRequest)
        log.Printf("| hfs::uploadFileHandler: File(%s) FILE_TOO_BIG\n", fileHeader.Filename)
        return
    }
    fileBytes, err := ioutil.ReadAll(file)
    if err != nil {
        renderError(w, "READ INVALID_FILE\n", http.StatusBadRequest)
        log.Printf("| hfs::uploadFileHandler: File(%s) READ INVALID_FILE\n", fileHeader.Filename)
        return
    }

    upFilePath := filepath.Join(getCurrentPath(), uploadPath)
    upFilePath = filepath.Join(upFilePath, devSN)
    if len(contName) > 0 {
        upFilePath = filepath.Join(upFilePath, contName)
    }
    newPath := filepath.Join(upFilePath, oldFileName)
    log.Printf("| hfs::uploadFileHandler: FileType: %s, File: %s\n", detectedFileType, newPath)

    // write file
    err = os.MkdirAll(upFilePath, os.ModePerm)
    if err != nil {
        renderError(w, "CANT_CREATE_FOLDER\n", http.StatusInternalServerError)
        log.Printf("| hfs::uploadFileHandler: File(%s) CANT_CREATE_FOLDER\n", fileHeader.Filename)
        return
    }
    newFile, err := os.Create(newPath)
    if err != nil {
        renderError(w, "CANT_WRITE_FILE\n", http.StatusInternalServerError)
        log.Printf("| hfs::uploadFileHandler: File(%s) CANT_WRITE_FILE\n", fileHeader.Filename)
        return
    }
    defer newFile.Close()
    if _, err := newFile.Write(fileBytes); err != nil || newFile.Close() != nil {
        renderError(w, "CANT_WRITE_FILE\n", http.StatusInternalServerError)
        log.Printf("| hfs::uploadFileHandler: File(%s) CANT_WRITE_FILE\n", fileHeader.Filename)
        return
    }

    renderError(w, "SUCCESS\n", http.StatusOK)
}

func renderError(w http.ResponseWriter, message string, statusCode int) {
    w.WriteHeader(statusCode)
    w.Write([]byte(message))
}

func randToken(len int) string {
    b := make([]byte, len)
    rand.Read(b)
    return fmt.Sprintf("%x", b)
}

func getCurrentPath() string {
    file, err := exec.LookPath(os.Args[0])
    if err != nil {
        return string("")
    }
    path, err := filepath.Abs(file)
    if err != nil {
        return string("")
    }
    i := strings.LastIndex(path, "/")
    if i < 0 {
        i = strings.LastIndex(path, "\\")
    }
    if i < 0 {
        return string("")
    }
    return string(path[0 : i+1])
}
Golang 导出函数的注意事项

在 Golang 中使用//export 关键字导出函数供 C/C++ 调用时,需要注意以下几点:

  • 导出函数的包必须为main:在 Golang 里,若要将函数导出给 C/C++ 调用,函数所在的包必须是main包。就像本项目中的StartHTTPServerStopHTTPServerUpdateDownFileMap函数,它们都位于main包中。
  • 导出函数的参数和返回值类型限制:C/C++ 与 Golang 的数据类型存在差异,所以导出函数的参数和返回值类型必须是 C/C++ 能够识别的类型。在 Golang 里,借助cgo将这些类型映射到 C/C++ 类型。例如,字符串类型使用GoString ,这是一个包含指针和长度的结构体。
  • 内存管理:在 C/C++ 和 Golang 之间传递数据时,要特别留意内存管理问题。因为 C/C++ 和 Golang 采用不同的内存管理机制,所以在传递字符串等动态分配内存的数据时,要确保不会出现内存泄漏或悬空指针的情况。像在UpdateDownFileMap函数中,通过深度拷贝字符串来避免 C++ 修改影响 Golang。
  • 线程安全:Golang 拥有自己的调度器和 goroutine 机制,而 C/C++ 通常使用操作系统线程。在导出函数中,如果涉及共享资源,要确保线程安全。可以使用互斥锁等同步机制来保护共享资源。
  • 编译选项:编译 Golang 代码为 C 共享库时,需要设置正确的编译选项。例如,使用-buildmode=c-shared 选项将 Golang 代码编译成动态链接库。

2. VC++ 部分:调用 Golang 服务

为了在 VC++ 中调用 Golang 编写的 HTTP 服务,我们创建了HFSProxy.h和HFSProxy.cpp两个文件。以下是它们的主要内容:

HFSProxy.h
#pragma once
#include "hfs.h"

class CHFSProxy
{
public:
    CHFSProxy();
    ~CHFSProxy();

    BOOL Open();
    void Close();
    void UpdateDownFileMap(const std::string& key, const std::string& filePath);

private:
    static unsigned int __stdcall WorkThread(void * lpParameter);
    void WorkThread();
    GoString CHFSProxy::buildGoString(const std::string& data);

private:
    int             m_iThreadCount;    
    Mutex           m_oCSThreadCount;

    HMODULE         m_hDll;
};

HFSProxy.cpp

#include "stdafx.h"
#include "HFSProxy.h"

CHFSProxy::CHFSProxy()
    : m_iThreadCount(0)
    , m_hDll(NULL)
{

}

CHFSProxy::~CHFSProxy()
{

}

BOOL CHFSProxy::Open()
{
    m_hDll = LoadLibrary("hfs.dll");
    if (NULL == m_hDll)
    {
        return FALSE;
    }

    unsigned int luThreadID = 0;
    uintptr_t hThreadHandle = _beginthreadex(NULL, 0, WorkThread, this, 0, &luThreadID);
    CloseHandle((HANDLE)hThreadHandle);

    return TRUE;
}

void CHFSProxy::UpdateDownFileMap(const std::string& key, const std::string& filePath)
{
    if (NULL == m_hDll)
    {
        return;
    }

    typedef void (*UpdateDownFileMap)(GoString key, GoString downPath);
    UpdateDownFileMap lpHFS = NULL;
    lpHFS = (UpdateDownFileMap)GetProcAddress(m_hDll, "UpdateDownFileMap");
    if (lpHFS)
    {
        lpHFS(buildGoString(key), buildGoString(filePath));
    }
}

void CHFSProxy::Close()
{
    typedef void (__stdcall *StopHTTPServer)();
    StopHTTPServer lpHFS = NULL;
    lpHFS = (StopHTTPServer)GetProcAddress(m_hDll, "StopHTTPServer");
    if (lpHFS)
    {
        lpHFS();
    }

    while (m_iThreadCount > 0)
    {
        ::Sleep(5);
    }
    FreeLibrary(m_hDll);
    m_hDll = NULL;
}

unsigned int __stdcall CHFSProxy::WorkThread(void * lpParameter)
{
    CHFSProxy *pThis = (CHFSProxy*)lpParameter;
    if (pThis)
    {
        pThis->m_oCSThreadCount.lock();
        pThis->m_iThreadCount++;
        pThis->m_oCSThreadCount.unlock();

        pThis->WorkThread();

        pThis->m_oCSThreadCount.lock();
        pThis->m_iThreadCount--;
        pThis->m_oCSThreadCount.unlock();
    }
    return 0;
}

void CHFSProxy::WorkThread()
{
    if (NULL == m_hDll)
    {
        return;
    }
    
    typedef void (__stdcall *StartHTTPServer)();
    StartHTTPServer lpHFS = NULL;
    lpHFS = (StartHTTPServer)GetProcAddress(m_hDll, "StartHTTPServer");
    if (lpHFS)
    {
        lpHFS();
    }
}

// 构建GoString结构对象
GoString CHFSProxy::buildGoString(const std::string& data)
{
    //typedef struct {const char *p; ptrdiff_t n;} _GoString_;
    //typedef _GoString_ GoString;
    GoString loGoStr;
    loGoStr.p = data.c_str();
    loGoStr.n = static_cast<ptrdiff_t>(data.length());
    return loGoStr;
}

在HFSProxy.cpp中,我们通过LoadLibrary函数加载 Golang 生成的动态链接库hfs.dll,并使用GetProcAddress函数获取导出函数的地址,从而实现对 Golang 服务的调用。

项目编译与运行

1. 编译 Golang 代码

在命令行中执行以下命令,将 Golang 代码编译成动态链接库:

set GOARCH=386
set GOOS=windows
set CGO_ENABLED=1
go build -ldflags "-w -s" -buildmode=c-shared -o hfs.dll HttpFileServer.go

2. 编译 VC++ 代码

使用 Visual Studio 打开 VC++ 项目,将生成的hfs.dll和hfs.h文件复制到项目目录下,然后编译运行 VC++ 代码。


网站公告

今日签到

点亮在社区的每一天
去签到