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