原文 2017-03-21 19:19:54 发表于 CSDN,这里对以前写的文章做下收录。
项目使用skynet框架,这个框架主要用lua写逻辑,但缺乏对HTTPS支持,所以我利用一点时间写了lua模块,支持异步HTTPS请求,文章这里讲述HTTPS相关知识,如何接入openssl请求HTTPS数据,同时也分享了lua模块给大家参考。(注:新版本已支持)
HTTPS说明
HTTPS可以理解成 HTTP协议的安全版,协议还是HTTP协议,只是对传输过程的数据进行了加密处理,保证数据传输的安全。(默认端口是443)
HTTPS验证数据的过程如下:
HTTPS支持
skynet只封装了HTTP的接口,没有对HTTPS做支持,所以要外接lua 库使用。
skynet支持HTTPS请求有两种方法:
优点 | 缺点 | |
---|---|---|
CURL | 支持http/https/ftp等,接入较简单 | 并发支持差 |
OpenSSL | 更底层,效率较高 | 接入复杂 |
其实,CURL是利用OpenSSL实现的,有网友封装了非阻塞版本的lua CURL库,可用于skynet处理HTTPS请求。链接猛击这里。
前面提到了CURL的缺点,CURL本身可做并发请求(libcurl multi),但做法却是将所有URL请求合到一起处理,需等全部URL数据处理完毕才返回数据。假设其中一个URL出现超时,那一起的其他URL都会受影响。
所以,文章推荐使用OpenSSL,这里先介绍C/C++如何处理HTTPS请求,然后再封装一个lua库,给大家演示下 skynet 如何请求HTTPS数据。
C/C++处理HTTPS请求
这里以一个例子,说明C/C++如何处理HTTPS请求。
#include <stdio.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <resolv.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "openssl/ssl.h" #include "openssl/err.h" #define MAXBUF 1024 int main(int argc, char **argv) { int sockfd, len; char send_data[1024]; struct sockaddr_in dest; char buffer[MAXBUF + 1]; SSL_CTX *ctx; SSL *ssl; char host_addr[] = "www.jd.com"; char ip[] = "112.91.125.129"; int port = "443"; /* SSL 库初始化 */ SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); ctx = SSL_CTX_new(SSLv23_client_method()); if (ctx == NULL) { ERR_print_errors_fp(stdout); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket Create Fail!"); exit(errno); } /* 建立 TCP 连接 */ bzero(&dest, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(atoi(port)); if (inet_aton(ip, (struct in_addr *) &dest.sin_addr.s_addr) == 0) { perror("Socket Init Fail!"); exit(errno); } printf("Socket Created\n"); if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0) { perror("Socket Connect Fail!"); exit(errno); } printf("Socket Connected\n"); /* 绑定 Socket 与 SSL */ ssl = SSL_new(ctx); SSL_set_fd(ssl, sockfd); /* 建立 SSL 连接 */ if (SSL_connect(ssl) == -1) ERR_print_errors_fp(stderr); else printf("SSL Connected with %s encryption\n", SSL_get_cipher(ssl)); sprintf(send_data, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: Close\r\n\r\n", host_addr); /* SSL 发数据 */ len = SSL_write(ssl, send_data, strlen(send_data)); if (len < 0) printf("SSL Send failure! errno = %d, err_msg = %s\n", errno, strerror(errno)); printf("SSL Send Done !\n"); bzero(buffer, MAXBUF + 1); int nbytes; /* SSL 收数据 */ while ((nbytes = SSL_read(ssl, buffer, MAXBUF)) > 0) { /* 打印收到的数据 */ printf("SSL Read %d\n", nbytes); } printf("SSL Read Done !\n"); /* 关闭连接 */ SSL_shutdown(ssl); SSL_free(ssl); close(sockfd); SSL_CTX_free(ctx); return 0; }
原理很简单,底层还是走了socket,只是TCP连接建立后,把连接交给SSL,让SSL收发数据。
Lua处理HTTPS请求
这里提供lua版本处理HTTPS请求,除了skynet,其他lua项目也可以使用。
以下是lua C模块的代码,保存为lua_httpsc.c,有删节,代码托管在 Github lua_httpsc
#include <stdlib.h> #include <string.h> #include <lua.h> #include <lauxlib.h> #include <time.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include "openssl/ssl.h" #include "openssl/err.h" #include <poll.h> static int openssl_init = !!NULL; typedef struct { int is_init; int ssl_init; int is_async; int snd_tmo; int rcv_tmo; SSL_CTX *ctx; } cutil_conf_t; enum cutil_conn_st { CONNECT_INIT = 1, CONNECT_SSL = 2, CONNECT_DONE = 3 }; typedef struct { int fd; SSL* ssl; int in_async; int header; enum cutil_conn_st status; } cutil_fd_t; static cutil_conf_t* fetch_config(lua_State *L) { cutil_conf_t* cfg; cfg = lua_touserdata(L, lua_upvalueindex(1)); if (!cfg) { luaL_error(L, "unable to fetch cfg"); return NULL; } if (!cfg->is_init) { luaL_error(L, "not inited"); return NULL; } if (!cfg->ssl_init) { if (!openssl_init) { openssl_init = !NULL; SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); } cfg->ssl_init = !NULL; } if (!cfg->ctx) { cfg->ctx = SSL_CTX_new(SSLv23_client_method()); if (!cfg->ctx) { char buf[256]; unsigned long err = ERR_get_error(); ERR_error_string_n(err, buf, sizeof(buf)); luaL_error(L, "unable to new ssl_ctx %s", buf); return NULL; } } return cfg; } static int _gc_fd(lua_State *L) { cutil_fd_t* fd_t = lua_touserdata(L, 1); SSL* ssl = fd_t->ssl; if (ssl) { fd_t->ssl = NULL; if (!SSL_in_init(ssl)) SSL_shutdown(ssl); SSL_free(ssl); } int fd = fd_t->fd; if (fd != ERROR_FD) { fd_t->fd = ERROR_FD; close(fd); } return 0; } static int _connect_ssl(lua_State *L, cutil_fd_t* fd_t) { SSL* ssl = fd_t->ssl; int ret = SSL_connect(ssl); if (ret == 1) { if (fd_t->in_async) { SSL_set_mode(ssl, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); } return 0; } int err = errno; int sslerr = SSL_get_error(ssl, ret); ERR_clear_error(); if (sslerr != SSL_ERROR_WANT_WRITE && sslerr != SSL_ERROR_WANT_READ ) { luaL_error(L, "connect error: %s (%d), ssl_error: %d", strerror(err), err, sslerr); return -1; } return 1; } static int lconnect(lua_State *L) { cutil_conf_t* cfg = fetch_config(L); if (!cfg) return 0; const char * addr = luaL_checkstring(L, 1); int port = luaL_checkinteger(L, 2); cutil_fd_t* fd_t = lua_newuserdata(L, sizeof(cutil_fd_t)); if (!fd_t) { luaL_error(L, "create fd %s %d failed", addr, port); return 0; } fd_t->fd = ERROR_FD; fd_t->ssl = NULL; fd_t->status = CONNECT_INIT; fd_t->in_async = cfg->is_async; fd_t->header = 0; if (luaL_newmetatable(L, "https_socket")) { lua_pushcfunction(L, _gc_fd); lua_setfield(L, -2, "__gc"); } lua_setmetatable(L, -2); int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in my_addr; fd_t->fd = fd; bzero(&my_addr, sizeof(my_addr)); my_addr.sin_addr.s_addr = inet_addr(addr); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(port); int ret; if (fd_t->in_async) { int flag = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flag | O_NONBLOCK); } ret = connect(fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)); if (ret != 0) { if (errno != EINPROGRESS) { luaL_error(L, "connect %s %d failed", addr, port); return 0; } } SSL *ssl = SSL_new(cfg->ctx); if (!ssl) { luaL_error(L, "ssl_new error, errno = %d", errno); return 0; } fd_t->ssl = ssl; fd_t->status = CONNECT_SSL; SSL_set_fd(ssl, fd); ret = _connect_ssl(L, fd_t); if (!fd_t->in_async) { if (ret != 0) { luaL_error(L, "ssl_connect fail"); return 0; } fd_t->status = CONNECT_DONE; } return 1; } static int lcheck_connect(lua_State *L) { cutil_conf_t* cfg = fetch_config(L); if (!cfg) return 0; cutil_fd_t* fd_t = (cutil_fd_t* ) lua_touserdata(L, 1); if (!fd_t) { luaL_error(L, "fd error"); return 0; } if (fd_t->status == CONNECT_SSL) { int ret = _connect_ssl(L, fd_t); if (ret != 0) return 0; fd_t->status = CONNECT_DONE; } if (fd_t->status == CONNECT_DONE) { lua_pushboolean(L, 1); return 1; } luaL_error(L, "connect error"); return 0; } static int lsend(lua_State *L) { cutil_conf_t* cfg = fetch_config(L); if (!cfg) return 0; cutil_fd_t* fd_t = (cutil_fd_t* ) lua_touserdata(L, 1); if ( fd_t == NULL ) { luaL_error(L, "fd error"); return 0; } if (fd_t->status != CONNECT_DONE) { luaL_error(L, "fd status error"); return 0; } size_t sz = 0; const char * msg = luaL_checklstring(L, 2, &sz); if (sz <= 0) { lua_pushinteger(L, 0); return 1; } fd_t->header = 0; SSL* ssl = fd_t->ssl; int r = SSL_write(ssl, msg, (int)sz); if (r > 0) { lua_pushinteger(L, r); return 1; } if (errno == EAGAIN || errno == EINTR) { lua_pushinteger(L, 0); return 1; } int err = errno; int sslerr = SSL_get_error(ssl, r); ERR_clear_error(); luaL_error(L, "send error: %s (%d), ssl_error : %d", strerror(err), err, sslerr); return 0; } static int lrecv(lua_State *L) { cutil_conf_t* cfg = fetch_config(L); if (!cfg) return 0; cutil_fd_t* fd_t = (cutil_fd_t* ) lua_touserdata(L, 1); if (!fd_t) { luaL_error(L, "fd error"); return 0; } if (fd_t->status != CONNECT_DONE) { luaL_error(L, "fd status error"); return 0; } char buffer[BUF_SZ]; int size = BUF_SZ * MAX_RETRY; if (lua_gettop(L) > 1 && lua_isnumber(L, 2)) { int _size = lua_tointeger(L, 2); if (_size > 0) size = _size; } luaL_Buffer b; luaL_buffinit(L, &b); int bset = 0; int sz; SSL* ssl = fd_t->ssl; for (;;) { sz = size < BUF_SZ ? size : BUF_SZ; if (fd_t->header >= 0) { if (sz + fd_t->header > HEADER_LMT) { sz = HEADER_LMT - fd_t->header; } } int r = SSL_read(ssl, buffer, sz); if (r <= 0) { int sslerr = SSL_get_error(ssl, r); ERR_clear_error(); if (sslerr == SSL_ERROR_WANT_READ || sslerr == SSL_ERROR_WANT_WRITE) { break; } luaL_error(L, "recv error: %d", sslerr); return 0; } if (r > sz) { luaL_error(L, "recv overflow: %d", r); return 0; } bset = 1; luaL_addlstring(&b, (const char*)buffer, r); size -= r; if (fd_t->header >= 0) { fd_t->header += r; if (fd_t->header >= HEADER_LMT){ fd_t->header = -1; break; } } if (size <= 0) break; } if (bset) { luaL_pushresult(&b); return 1; } return 0; } /* GC, clean up the ctx */ static int _gc(lua_State *L) { cutil_conf_t* cfg = lua_touserdata(L, 1); SSL_CTX *ctx; if (cfg && (ctx = cfg->ctx)) { cfg->ctx = NULL; SSL_CTX_free(ctx); } return 0; } static void _create_config(lua_State *L) { cutil_conf_t *cfg; cfg = lua_newuserdata(L, sizeof(*cfg)); cfg->is_init = !!NULL; cfg->ssl_init = !!NULL; cfg->ctx = NULL; cfg->is_async = !NULL; cfg->snd_tmo = TIMEOUT; cfg->rcv_tmo = TIMEOUT; /* Create GC to clean up ctx */ lua_newtable(L); lua_pushcfunction(L, _gc); lua_setfield(L, -2, "__gc"); lua_setmetatable(L, -2); cfg->is_init = !NULL; } int luaopen_httpsc(lua_State *L) { static const luaL_Reg funcs[] = { { "connect", lconnect }, { "check_connect", lcheck_connect }, { "recv", lrecv }, { "send", lsend }, {NULL, NULL} }; lua_newtable(L); _create_config(L); luaL_setfuncs(L, funcs, 1); return 1; }
编译到项目执行后,lua调用的方法如下:
local httpsc = require "httpsc" for k,v in pairs(httpsc ) do print(k,v) end local fd = httpsc.connect("163.177.151.109", 443) print(fd) while true do local ok = httpsc.check_connect(fd) if ok then break end httpsc.usleep(10000) end -- httpsc.usleep(10000) httpsc.send(fd, "GET / HTTP/1.1\r\nAccept: */*\r\nHost: www.baidu.com\r\nConnection: Close\r\n\r\n") httpsc.usleep(1000000) local body = "" while true do local r = httpsc.recv(fd) print(r) if r and #r>0 then body = body .. r else break end end print(body) httpsc.close(fd) print("ok!")
以上只是一个简单的例子,我同时也参考skynet clientsocket写了一个对HTTP协议支持友好的版本,代码托管在Github : https://github.com/chenweiqi/lua_httpsc ,欢迎交流。
OpenSSL动态库的编译
既然基于OpenSSL开发,就需要编译OpenSSL库来使用。
这里提供最近版本几个依赖库的编译方法:
openssl - libssl.a / libcrypto.a
wget https://www.openssl.org/source/openssl-1.0.2k.tar.gz tar -zxf openssl-1.0.2k.tar.gz cd openssl-1.0.2k ./config -fPIC enable-shared make depend make make install find -name "*.a"
zlib - zlib.a
wget http://zlib.net/zlib-1.2.11.tar.xz tar -zxf zlib-1.2.11.tar.xz cd zlib-1.2.11 export CFLAGS=" -fPIC" ./configure make find -name "*.a"
idn - libidn.a
wget http://ftp.gnu.org/gnu/libidn/libidn-1.33.tar.gz tar -zxf libidn-1.33.tar.gz cd libidn-1.33 export CFLAGS=" -fPIC" ./configure make find -name "*.a"
最后语
这个库写完于 2016年,后面基本没改动,一个原因是想法保守,功能稳定后没有改进的动力,同时担心导致线上出问题。现在看到有些同学关注,就做了改进。
主要有几点:
1. 支持多线程调用,原本只能被一个lua vm 加载使用;
2. 优化读操作处理,循环调用 ssl_read 取出数据,减少 lua 态调用次数,同时有利于清空缓冲区,提高吞吐量;
3. 支持阻塞请求,测试过程发现阻塞 socket 使用 ssl_read 时,如果没数据时继续调用会一直阻塞直到超时。解决方案就是,别一次性读完所有数据,先读出 header 后解析 content-length ,再继续读这个长度的数据。
更新说明:
2023/12/16 支持多线程调用,减少lua态调用,提高吞吐量,支持配置阻塞和非阻塞
2017/4/15 优化socket创建连接过程,由阻塞改成非阻塞方式