最佳实践-p2p传输https协议
库快科技p2p库是一个业务通用的p2p通信库,只是负责创建连接,至于在连接上业务层使用的协议,不做任何限制,p2p库只是对数据进行透传(relay模式或者p2p模式),所以基于库快科技的p2p通信库,用户可以使用任何协议,包括https、rtmp等。
本例子用于举例说明,如何基于kkp2p使用https协议。众所周知,https连接的建立是一个比较复杂的过程,包括tls握手,证书的校验流程等,实际传输还会对数据自动进行加解密。
本例子分两个程序配合演示,一个作为http server的接入代理(peer client);一个作为服务端(peer server),是http server角色。最后演示时候用curl工具作为http client。测试流程描述如下:
kkp2p库有一个接口为kkp2p_start_proxy,他可以在本地创建一个ip、port的侦听端口,可以当作是和指定peer id进行通信的代理服务端口,即发送到该ip、port的数据都会被自动转发到指定的peer id。我们先看peer client的代码,代码在linux平台编译运行。
#include <errno.h> #include <stdio.h> #include <signal.h> #include <sys/syscall.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> // 到kkuai.com获取最新的库和头文件 #include "kkp2p_sdk.h" // 程序利用USER1信号 char run_flag = 1; void set_exit_flag(int sig) { run_flag = 0; } // 输入6个参数 // 侦听的ip、port、指定通信的peer id、和peer id的建连模式、连接描述 // 连接描述是双方约定好的,服务端可以根据连接描述判断连接所使用的协议 int main(int argc, char** argv) { if (argc < 6) { printf("usage:%s proxy_ip proxy_port peer_id connect_mode connect_desc\n", argv[0]); return -1; } struct sigaction actions; memset(&actions, 0, sizeof(actions)); sigemptyset(&actions.sa_mask); actions.sa_flags = 0; actions.sa_handler = set_exit_flag ; sigaction(SIGUSR1,&actions,NULL); // 初始化p2p服务器的登录ip和端口号信息等 kkp2p_engine_conf_t kkp2p_conf; memset(&kkp2p_conf, 0, sizeof(kkp2p_engine_conf_t)); kkp2p_conf.login_domain = "124.71.217.198"; kkp2p_conf.login_port = 3080; kkp2p_conf.lan_search_port = 3549; kkp2p_conf.max_log_size = 1024*1024*10; kkp2p_conf.log_path = NULL; kkp2p_engine_t* g_engine = kkp2p_engine_init(&kkp2p_conf, 5000); kkp2p_switch_log_level(g_engine, 4); kkp2p_connect_ctx_t ctx; memset(&ctx, 0, sizeof(kkp2p_connect_ctx_t)); strncpy(ctx.peer_id, argv[3], 32); ctx.timeout = 5000; // 创建tcp类型传输通道 ctx.channel_type = KKP2P_TCP_CHANNEL; // 0 为自动模式,1为仅p2p模式,2为仅relay模式 ctx.connect_mode = atoi(argv[4]); // 连接描述,会透传到服务端 // 可以约定参数,比如1为http协议,2为https协议 ctx.connect_desc = atoi(argv[5]); uint32_t proxyId = 0; // 启动和peer id进行通信的本地代理服务 int ret = kkp2p_start_proxy(g_engine, argv[1], atoi(argv[2]), &ctx, &proxyId); if (ret < 0) { printf("create proxy(%s:%d) to peer %s error.\n",argv[1], atoi(argv[2]), argv[3]); return -1; } else { printf("create proxy(%s:%d) to peer %s success.\n",argv[1], atoi(argv[2]), argv[3]); } while(run_flag) { usleep(100); } kkp2p_stop_proxy(g_engine, proxyId); kkp2p_engine_destroy(g_engine); return 0; }
上面程序启动了一个代理服务,凡是发送到侦听的ip,port数据都会被p2p库透传到指定的peer id,凡是收到从指定peer id的返回的数据,都会透传回去。
服务端代码如下,需要额外链接openssl的库,在linux平台下测试验证通过
#include <errno.h> #include <stdio.h> #include <signal.h> #include <sys/syscall.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <openssl/ssl.h> #include <openssl/err.h> // 到kkuai.com获取最新的头文件和库 #include "kkp2p_sdk.h" char run_flag = 1; kkp2p_engine_t* g_engine = NULL; SSL_CTX *ssl_ctx; // 利用USR1信号退出程序 void set_exit_flag(int sig) { run_flag = 0; } int send_data(int fd, char* buff, int len) { int sended = 0 ; while (sended < len) { // 1秒超时 int wl = kkp2p_write(fd, buff + sended, len - sended, 1000); if (wl < 0) { printf("SendData error,fd:%d,ret:%d,len:%d,errno:%d,desc:%s.\n",fd,wl, len, errno, strerror(errno)); return -1; } sended += wl; } return len; } // 处理http协议,返回指定内容,仅作演示用途 void* process_http(void* arg) { kkp2p_channel_t* channel = (kkp2p_channel_t*)arg; char body[64]; int bodyLen = sprintf(body, "%ld\n", time(NULL)); char head[512]; int headLen = sprintf(head,"HTTP/1.1 200 OK\r\n" "Date: Tue, 01 Oct 2022 10:10:10 GMT\r\n" "Server: Apache\r\n" "X-Powered-By: kkp2p http\r\n" "Content-Type: text/xml; charset=utf-8\r\n" "Content-Length:%d\r\n" "Connection: close\r\n" "\r\n",bodyLen); send_data(channel->fd, head, headLen); send_data(channel->fd, body, bodyLen); kkp2p_close_fd(channel->fd); kkp2p_close_channel(g_engine, channel->channel_id); free(channel); return NULL; } // 处理https协议,返回指定内容,仅作演示用途 void* process_https(void* arg) { kkp2p_channel_t* channel = (kkp2p_channel_t*)arg; SSL *ssl; ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, channel->fd); // fd设置为阻塞模式,默认返回的fd是非阻塞模式 int val = fcntl(channel->fd, F_GETFL, 0); fcntl(channel->fd, F_SETFL, val & (~O_NONBLOCK)); // 调用openssl库,进行tls握手 int ret = SSL_accept(ssl); if (ret < 0) { int err_SSL_get_error = SSL_get_error(ssl, ret); printf("SSL_accept error,ret:%d.\n",err_SSL_get_error); printf("%s.\n",ERR_error_string( err_SSL_get_error, NULL)); goto finish; } char body[64]; int bodyLen = sprintf(body, "%ld\n", time(NULL)); char head[512]; int headLen = sprintf(head,"HTTP/1.1 200 OK\r\n" "Date: Tue, 01 Oct 2022 10:10:10 GMT\r\n" "Server: Apache\r\n" "X-Powered-By: kkp2p https\r\n" "Content-Type: text/xml; charset=utf-8\r\n" "Content-Length:%d\r\n" "Connection: close\r\n" "\r\n",bodyLen); // 调用openssl库,写入数据,简单起见不判断len是否发送完成 int len = SSL_write(ssl, head, headLen); if ( len <=0 ) { printf("SSL_write error.\n"); goto finish; } // 调用openssl库,写入数据,简单起见不判断len是否发送完成 len = SSL_write(ssl, body, bodyLen); if ( len <=0 ) { printf("SSL_write error.\n"); goto finish; } finish: SSL_shutdown(ssl); SSL_free(ssl); kkp2p_close_fd(channel->fd); kkp2p_close_channel(g_engine, channel->channel_id); free(channel); return NULL; } // 总共5个参数,包括登录账号、登录密码、私钥文件、证书文件 int main(int argc, char** argv) { if (argc < 5) { printf("usage:%s peer_id peer_key pirvate_key_file cert_file.\n", argv[0]); return -1; } // kill -user1 pid struct sigaction actions; memset(&actions, 0, sizeof(actions)); sigemptyset(&actions.sa_mask); actions.sa_flags = 0; actions.sa_handler = set_exit_flag ; sigaction(SIGUSR1,&actions,NULL); // init open ssl SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); //ssl_ctx = SSL_CTX_new(SSLv23_server_method()); ssl_ctx = SSL_CTX_new(TLS_method()); if (ssl_ctx == NULL) { printf("SSL_CTX_new error.\n"); return -1; } // openssl 加载私钥文件 if (SSL_CTX_use_PrivateKey_file(ssl_ctx, argv[3], SSL_FILETYPE_PEM) <= 0) { printf("SSL_CTX_use_PrivateKey_file error.\n"); return -1; } // openssl 加载证书文件 if (SSL_CTX_use_certificate_file(ssl_ctx, argv[4], SSL_FILETYPE_PEM) <= 0) { printf("SSL_CTX_use_certificate_file error.\n"); return -1; } if (!SSL_CTX_check_private_key(ssl_ctx)) { printf("SSL_CTX_check_private_key error.\n"); return -1; } // 设置p2p服务器的登录端口和域名 kkp2p_engine_conf_t kkp2p_conf; memset(&kkp2p_conf, 0, sizeof(kkp2p_engine_conf_t)); kkp2p_conf.login_domain = "124.71.217.198"; kkp2p_conf.login_port = 3080; kkp2p_conf.lan_search_port = 3549; kkp2p_conf.max_log_size = 1024*1024*10; kkp2p_conf.log_path = NULL; g_engine = kkp2p_engine_init(&kkp2p_conf, 5000); kkp2p_switch_log_level(g_engine, 4); kkp2p_join_lan(g_engine, argv[1]); kkp2p_join_net(g_engine, argv[1], argv[2]); kkp2p_channel_t* channel = (kkp2p_channel_t*)calloc(1, sizeof(kkp2p_channel_t)); while(run_flag) { // 循环侦听是否有连接过来 int ret = kkp2p_accept(g_engine, 1000, channel); if (ret < 0) { // error printf("kkp2p_accept error,exit\n"); free(channel); break; } else if (ret == 0) { // timeout continue; } else { // success pthread_t ThreadId; // mode为1表示是p2p连接,为2表示relay连接 printf("accept new connection,fd:%d, mode is %d,channel id:%u.\n",channel->fd, channel->transmit_mode, channel->channel_id); if (channel->connect_desc == 1) { // 1 是http协议,双方自行约定参数 pthread_create(&ThreadId, NULL, process_http,(void*)channel); } else if (channel->connect_desc == 2) { // 2 是https协议,双方自行约定参数 pthread_create(&ThreadId, NULL, process_https,(void*)channel); } else { printf("unkown protocol,connect desc is :%d.\n",channel->connect_desc); kkp2p_close_fd(channel->fd); kkp2p_close_channel(g_engine, channel->channel_id); } channel = (kkp2p_channel_t*)calloc(1, sizeof(kkp2p_channel_t)); } } SSL_CTX_free(ssl_ctx); kkp2p_engine_destroy(g_engine); return 0; }
最后看测试效果,启动peer client代理
启动peer server服务
可以看到当有连接过来时候,会有日志打印,mode表示传输类型,1就是使用udp协议的p2p模式传输
再看curl命令
因为证书是自己生成的,所以curl加上--insecure命令表示不校验证书,返回内容里面有https字段(代码里面是再https协议流程处理中加的),说明https的数据加解密流程都已经跑通了,且peer client和peer server之间是以p2p方式跑通的。这个例子说明了两个peer之间是可以跑通https协议的。
我们还可以用openssl工具验证,也可以说明https流程可以跑通。
通过上面例子可以看到,使用库快科技的p2p库,通过调用kkp2p_start_proxy很容易的就可以使用自定义的协议,使用起来简单且通用。上面例子为了演示方便使用了curl和openssl命令,实际使用时候,应用层的协议交互和启动代理都可以在一个peer sdk进程里面。