go中使用curl实现https请求的示例代码
之前曾经在一个 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(¶ms) 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
小结
上述代码目前只在测试环境测试,后续择机在生产环境中使用。就测试结果看,应该是没有大问题的。
到此这篇关于go中使用curl实现https请求的示例代码的文章就介绍到这了,更多相关go实现https请求内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
最新评论