最佳实践-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进程里面。
