lua 异步HTTPS并发请求库

原文 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创建连接过程,由阻塞改成非阻塞方式

发表评论

邮箱地址不会被公开。 必填项已用*标注