在软件开发的工作中,不同编程语言有着各自独特的优势。有时候,为了充分发挥这些优势,我们需要让它们携手合作。本文将详细介绍如何使用 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
包。就像本项目中的StartHTTPServer
、StopHTTPServer
和UpdateDownFileMap
函数,它们都位于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++ 代码。