Golang实践录:在go中使用curl实现https请求

发布于:2025-05-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

之前曾经在一个 golang 工程调用 libcur 实现 https的请求,当前自测是通过的。后来迁移到另一个小系统出现段错误,于是对该模块代码改造,并再次自测。

问题提出

大约2年前,在某golang项目使用libcurl进行https请求(参见容器《Golang实践录:go-curl的使用》),由于使用的docker镜像不支持glibc,又不想重新制作,且该功能不是核心的,因此,就没有上线。现在,另一个工程也使用这个模块,迁移代码后自测出现问题。

主要出错信息如下:

fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x80 addr=0x0 pc=0x7fa55160bbf4]

经定位,在调用curl_easy_perform函数时出错,回顾了curl一般写法,未发现问题,只好借助AI工具,一边提问一边搜索。

场景描述

本次重提https请求,主要是因为某个测试工程需要用https向另一个服务请求,该服务的证书固定了某个生产环境的IP,而又需要将该服务部署在测试环境,但测试环境无法使用证书,因此无法验证一些模块功能。为保证生产环境版本的正确,需要在测试环境解决证书请求问题。

在此之前,自己没有想到解决办法,问了AI,也没给出满意的回答(可能问的方式不恰当)。实际上,借助docker容器,可以很方便解决上述问题。

  • 创建docker网段,网段与生产环境的服务相同。
  • 利用容器部署上述测试工程和服务,两者在同一网段中,并且将部署服务的容器IP设置为生产环境的IP,这样使得https证书可用。
  • 在测试工程请求时,使用固定IP和固定URL请求。这样能够模拟在生产环境中的请求场景。

重新实现

核心文件代码如下:

/*
curlApi_linux.go
使用 curl 库封装的请求接口
为减少cgo开销,在 C 中实现完整的初始化、请求过程,使用静态变量减少内存碎片
编译、运行的系统必须有libcurl、libssh2等库
*/

package mypostservice

/*
#cgo linux LDFLAGS: -lcurl
#cgo darwin LDFLAGS: -lcurl
#cgo windows LDFLAGS: -lcurl
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>

static void GetDateTimeStr(char *buf, int len)
{
	int Year = 0;
	int Month = 0;
	int Day = 0;
	int Hour = 0;
	int Minute = 0;
	int Second = 0;
	long mSecond = 0;

	struct timeval theTime;
	gettimeofday(&theTime, NULL);
	struct tm * timeinfo = localtime(&(theTime.tv_sec));

	Year   = 1900 + timeinfo->tm_year;
	Month  = 1 + timeinfo->tm_mon;
	Day    = timeinfo->tm_mday;

	Hour   = timeinfo->tm_hour;
	Minute = timeinfo->tm_min;
	Second = timeinfo->tm_sec;
	mSecond = theTime.tv_usec / 1000;

	snprintf(buf, len, "%04d%02d%02d%02d%02d%02d%03ld",
		Year, Month, Day, Hour, Minute, Second, mSecond);
}

typedef struct {
    char *url;
    char *postfile;
	char *cafile;
    char *clifile;
    char *keyfile;
    int timeout;
    char *jsonStr;
    int jsonLen;
} CRequestParams;

typedef struct {
    char *data;
    size_t len;
} CResponseData;

typedef struct {
    char *respBody;       // 响应结果
    char *filename;     // 响应文件名
    int retcode;        // 是否成功标志
} CReturnData;

static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t realsize = size * nmemb;
    CResponseData *mem = (CResponseData *)userp;

    char *ptr = realloc(mem->data, mem->len + realsize + 1);
    if(!ptr) return 0;

    mem->data = ptr;
    memcpy(&(mem->data[mem->len]), contents, realsize);
    mem->len += realsize;
    mem->data[mem->len] = 0;

    return realsize;
}

// 头部回调函数用于获取文件名
static size_t header_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
    char *header = strndup(ptr, size * nmemb);
    char *filename = (char *)userdata;

    // 从Content-Disposition头部提取文件名
    if(strstr(header, "Content-Disposition") != NULL) {
        char *start = strstr(header, "filename=");
        if(start) {
            start += 9; // 跳过"filename="
            char *end = strchr(start, ';');
            if(!end) end = start + strlen(start);

            // 去除可能的引号
            if(*start == '"') start++;
            if(*(end-1) == '"') end--;

            strncpy(filename, start, end - start);
            filename[end - start] = '\0';
        }
    }

    free(header);
    return size * nmemb;
}

static CReturnData perform_request(CRequestParams *params) {
    CURL *curl;
    CURLcode res;
    CResponseData chunk = {0};
	CReturnData ret = {0};
    char resp_filename[128] = {0};  // 存储文件名

    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
    if(!curl) {
        ret.respBody = strdup("curl_easy_init failed");
        ret.retcode = -1;
        goto cleanup;
    }

    // 设置基本选项
    curl_easy_setopt(curl, CURLOPT_URL, params->url); // 服务器URL
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);  // // 设置线程安全选项
    curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)params->timeout); // 超时时间,单位为毫秒
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, (long)params->timeout);

    // HTTPS设置
    if(strncmp(params->url, "https://", 8) == 0) {
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1L);
        curl_easy_setopt(curl, CURLOPT_CAINFO, params->cafile);
        curl_easy_setopt(curl, CURLOPT_SSLCERT, params->clifile);
        curl_easy_setopt(curl, CURLOPT_SSLCERTPASSWD, "123456");
        curl_easy_setopt(curl, CURLOPT_SSLKEY, params->keyfile);
        curl_easy_setopt(curl, CURLOPT_SSLKEYPASSWD, "123456");
    }
	// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); // 调试信息
	curl_easy_setopt(curl, CURLOPT_SSLVERSION, 4);

    // 设置回调
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);

    // 构建表单
    struct curl_httppost *formpost = NULL;
    struct curl_httppost *lastptr = NULL;

    curl_formadd(&formpost, &lastptr,
        CURLFORM_COPYNAME, "file",
        CURLFORM_BUFFER, params->postfile,
        CURLFORM_BUFFERPTR, params->jsonStr,
        CURLFORM_BUFFERLENGTH, (long)params->jsonLen,
        CURLFORM_CONTENTTYPE, "application/json",
        CURLFORM_END);

    curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);

	// 设置头部回调以获取文件名
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, resp_filename);

    // 执行请求
    res = curl_easy_perform(curl);

    if(res != CURLE_OK) {
        const char *err = curl_easy_strerror(res);
        ret.respBody = malloc(strlen(err) + 32);
        sprintf(ret.respBody, "curl_easy_perform failed: %s", err);
        ret.retcode = -2;
        goto cleanup;
    }

	// printf("debug %s %d  resp data len: \n", __func__, __LINE__, chunk.len);
    // 成功则复制结果
    if(chunk.data) {
        ret.respBody = strdup(chunk.data);
        ret.filename = strdup(resp_filename);
        ret.retcode = 0;
    } else {
        ret.respBody = strdup("No data received");
        ret.retcode = -3;
    }

cleanup:
    if(chunk.data) free(chunk.data);
    if(formpost) curl_formfree(formpost);
    if(curl) curl_easy_cleanup(curl);
    curl_global_cleanup();

    return ret;
}
*/
import "C"
import (
	"unsafe"
)

type CurlResponse struct {
	respBody string
	filename string
	retcode  int
}

type MyCURL struct {
	url, postfile, cafile, clifile, keyfile string
	timeout                                 int
}

func NewCurl() *MyCURL {
	return &MyCURL{
		timeout: 5000, // 默认超时
	}
}

func (c *MyCURL) SetOpt(url, postfile, cafile, clientfile, keyfile string, timeout int) {
	c.url = url
	c.postfile = postfile
	c.cafile = cafile
	c.clifile = clientfile
	c.keyfile = keyfile
	c.timeout = timeout
}

func (c *MyCURL) PostFiledata(jsonStr []byte) CurlResponse {
	// 将 Go的json数据复制到C的内存中
	cJsonStr := C.CBytes(jsonStr)
	defer C.free(cJsonStr)

	params := C.CRequestParams{
		url:      C.CString(c.url),
		postfile: C.CString(c.postfile),
		cafile:   C.CString(c.cafile),
		clifile:  C.CString(c.clifile),
		keyfile:  C.CString(c.keyfile),
		timeout:  C.int(c.timeout),
		jsonStr:  (*C.char)(cJsonStr), // 使用C分配的内存
		jsonLen:  C.int(len(jsonStr)),
	}

	defer func() {
		C.free(unsafe.Pointer(params.url))
		C.free(unsafe.Pointer(params.cafile))
		C.free(unsafe.Pointer(params.clifile))
		C.free(unsafe.Pointer(params.keyfile))
	}()

	// 调用C函数并获取返回结构体
	cRet := C.perform_request(&params)
	defer func() {
		C.free(unsafe.Pointer(cRet.respBody))
		C.free(unsafe.Pointer(cRet.filename))
	}()

	// 转换为Go结构体
	return CurlResponse{
		respBody: C.GoString(cRet.respBody),
		filename: C.GoString(cRet.filename),
		retcode:  int(cRet.retcode),
	}
}

与上一版本对比,有如下调整:

  • #cgo linux pkg-config: libcurl改为#cgo linux LDFLAGS: -lcurl,对编译环境较友好一些。
  • 将全局变量改为局域变量,防止多线程情况下出现问题。
  • 上版本返回值使用换行符进行解析,现改为返回多个值(go语言本身支持),代码较友好。

测试

与curl请求有关的输出信息如下:

 * About to connect() to 172.18.18.10 port 86 (#4)
 *   Trying 172.18.18.10...
 * Connected to 172.18.18.10 (172.18.18.10) port 86 (#4)
 * Initializing NSS with certpath: sql:/etc/pki/nssdb
 *   CAfile: ../../../cert/all.pem
   CApath: none
 * SSL connection using ECDHE-RSA-AES256-GCM-SHA384
 * Server certificate:
 *        subject: CN=172.18.18.10
 *        start date: 2023-02-16 08:19:00 GMT
 *        expire date: 2033-02-16 08:19:00 GMT
 > POST /mypost/foobar HTTP/1.1
 Host: 172.18.18.10:86
 Content-Length: 799
 Expect: 100-continue
 Content-Type: multipart/form-data; boundary=----------------------------258acabf1379

 < HTTP/1.1 100 Continue
 < HTTP/1.1 200 OK
 < Server: nginx/1.16.1
 < Date: Sun, 14 May 2025 18:20:48 GMT
 < Content-Type: application/json
 < Content-Length: 1083
 < Connection: keep-alive
 < Content-Disposition: form-data;filename=bar.json
 <
 * Connection #4 to host 172.18.18.10 left intact

小结

上述代码目前只在测试环境测试,后续择机在生产环境中使用。就测试结果看,应该是没有大问题的。