關於 k210 的 micropython 添加 ussl 模塊,實現 https 訪問支持的那些事。

原由

事情已通過去快一週了吧,繼上次修復 maixpy k210 的 esp8285 at 通訊後,忽然遇到澤畔大大問,要不要作 ussl 的支持?python

評估了一下各方的實現,想了一下本身也恰好在作網絡層的優化和處理,何況 micropython 在 stm32 、 esp32 上的也有對應的實現,那就添加實現進去吧,選取了 mbedtls 版本的 ussl 模塊,實現相關文件以下。git

這裏說一下 ussl 的工做機制。github

首先創建在 micropython 的 network 架構下的 socket 模塊,提供了關鍵的 steam->write 和 steam->read 基礎接口,實際上就是繼承一個抽象 steam 對象的接口。json

所以 ussl 提供了 wrap_socket 用來提高 socket 的功能,從而支持 https 的訪問。網絡

咱們看一下 micropython 的實例就知道了。架構

try:
    import usocket as _socket
except:
    import _socket
try:
    import ussl as ssl
except:
    import ssl

def main(use_stream=True):
    s = _socket.socket()

    ai = _socket.getaddrinfo("google.com", 443)
    print("Address infos:", ai)
    addr = ai[0][-1]

    print("Connect address:", addr)
    s.connect(addr)

    s = ssl.wrap_socket(s)
    print(s)

    if use_stream:
        # Both CPython and MicroPython SSLSocket objects support read() and
        # write() methods.
        s.write(b"GET / HTTP/1.0\r\n\r\n")
        print(s.read(4096))
    else:
        # MicroPython SSLSocket objects implement only stream interface, not
        # socket interface
        s.send(b"GET / HTTP/1.0\r\n\r\n")
        print(s.recv(4096))

    s.close()

main()

實現的最終結果以下,不過目前的實測效果距離商業使用,保守來說,還有很大的優化空間,主要在覈心函數和配置方面要改善性能。app

  • esp32 的效果

  • k210 的效果

實現細節

MaixPy k210 採用 components/micropython/CMakeLists.txt 來管理 micropython 的編譯命令。dom

因此在不脫離主流的基礎上,在 micropython-ulab 的配置後面繼續添加以下配置。socket

if(1 OR CONFIG_MICROPY_SSL_MBEDTLS)
        list(APPEND ADD_INCLUDE "${mpy_core_dir}/lib/mbedtls/include")
        set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBEDTLS_CONFIG_FILE='\"${mpy_port_dir}/src/mbedtls/include/mbedtls_config.h\"'")
        # message(${CMAKE_C_FLAGS})
        append_srcs_dir(ADD_SRCS "port/src/mbedtls")
        append_srcs_dir(ADD_SRCS "core/lib/mbedtls/library")
        list(REMOVE_ITEM ADD_SRCS "${mpy_core_dir}/lib/mbedtls/library/net_sockets.c")
    endif()

稍微解釋一下函數

  • 將 mbedtls/include 和 mbedtls/library 添加到環境中,並排除 net_sockets.c 的實現,問題下述。

  • 給 CMAKE_C_FLAGS 添加 -DMBEDTLS_CONFIG_FILE 自定義的 mbedtls 配置文件,從而屏蔽內置的 config.h 。(奇怪的是 add_definitions 不 work ,就直接用 set 了)
#if !defined(MBEDTLS_CONFIG_FILE)
#include "mbedtls/config.h"
#else
#include MBEDTLS_CONFIG_FILE
#endif

因此如今把代碼編譯了進去,就完成了大部分的移植,是否是很簡單?

修改記錄在這裏,可供參考。

固然,事情不會這麼順利的,在沒有進行專門配置的時候,啓動模塊是能夠的,如今開始實踐發起一次 get https 網站的請求,測試 python code 以下:

wCli = MicroWebCli('https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13631786501')
# wCli = MicroWebCli('https://www.baifubao.com/callback?cmd=1059&callback=phone&phone=13631786501')
# wCli = MicroWebCli('https://github.com')
# wCli = MicroWebCli('https://ssl.logink.cn/')
# wCli = MicroWebCli('https://cn.bing.com/?FORM=Z9FD1')
# wCli = MicroWebCli('https://www.sojson.com')
# wCli = MicroWebCli('https://www.baidu.com')

while True:
    try:
        print('GET %s' % wCli.URL)
        wCli.OpenRequest()
        buf  = memoryview(bytearray(1024))
        resp = wCli.GetResponse()
        if resp.IsSuccess() :
          while not resp.IsClosed() :
            x = resp.ReadContentInto(buf)
            if x < len(buf) :
              buf = buf[:x]
            print(bytes(buf))
          print('GET success with "%s" content type' % resp.GetContentType())
        else :
          print('GET return %d code (%s)' % (resp.GetStatusCode(), resp.GetStatusMessage()))
    except Exception as E:
        print(E)
    time.sleep(2)

問題一: -0x0034 錯誤代碼

Gather entropy_len bytes of entropy to seed state

mbedtls_ctr_drbg_seed returned -52 或者-0x0034

錯誤解釋:

MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED        -0x0034  /**< The entropy source failed. */

緣由:

給mbedtls提供的熵源不夠混亂,應該用硬件隨機數發生器。

解決:

mbedtls\config.h

#define MBEDTLS_ENTROPY_HARDWARE_ALT

解決方案能夠參考

https://blog.csdn.net/liaofeifly/article/details/88899655

mbedtls_hardware_poll 實現能夠參考 esp32 、stm32 的,以下是我後來實踐到 k210 的,這個函數只會在發起連接的時候調用。

#include <stdlib.h>
#include <stdio.h>
#include "rng.h"

#if !defined(MBEDTLS_CONFIG_FILE)
#include "mbedtls/config.h"
#else
#include MBEDTLS_CONFIG_FILE
#endif

#include "mphalport.h"

int os_get_random(unsigned char *buf, size_t len)
{
    int i, j;
    unsigned long tmp;
 
    for (i = 0; i < ((len + 3) & ~3) / 4; i++) {
        tmp = rng_get() + systick_current_millis();
 
        for (j = 0; j < 4; j++) {
            if ((i * 4 + j) < len) {
                buf[i * 4 + j] = (uint8_t)(tmp >> (j * 8));
            } else {
                break;
            }
        }
    }
 
    return 0;
}

int mbedtls_hardware_poll( void *data, unsigned char *output, size_t len, size_t *olen )
{
    int res = os_get_random(output, len);
    *olen = len;
    return 0;
}

// int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen) {
//     uint32_t val;
//     int n = 0;
//     *olen = len;
//     while (len--) {
//         if (!n) {
//             val = rng_get();
//             n = 4;
//         }
//         *output++ = val;
//         val >>= 8;
//         --n;
//     }
//     return 0;
// }

問題二: -0x7280 錯誤代碼

錯誤類型是

#define MBEDTLS_ERR_SSL_CONN_EOF -0x7280 /**< The connection indicated an EOF. */

這個問題我看了好久,由於上來就給我當頭一棒,任何 https 的訪問都布星。

那就奇了怪了,而後我開了 debug 開關,開到等級 4 後。

#ifdef MBEDTLS_DEBUG_C
    // Debug level (0-4)
    mbedtls_debug_set_threshold(4);
    #endif

發現 TM 有在流動數據,並且是在工做的,並且看數據發現是正常工做的。

那麼開了 debug 和沒有開 debug 的區別在哪裏呢?

能夠想象的是 debug 確定會對程序執行產生一些細微的差距,最後經過幾回 debug 的後定位到是這個函數的延時保障了程序的執行。

看起來是否是很奇怪?爲何呢?若是在這裏簡單的延時就會產生下述的效果。

也就是請求時好時壞,若是知道是須要延時的話,那麼又該延時多少呢?

此時交給❤名偵探登場❤,真相只有一個!

首先函數來自於 f_send 操做,這個確定是個回調,而後定位它。

void mbedtls_ssl_set_bio( mbedtls_ssl_context *ssl,
        void *p_bio,
        mbedtls_ssl_send_t *f_send,
        mbedtls_ssl_recv_t *f_recv,
        mbedtls_ssl_recv_timeout_t *f_recv_timeout )
{
    ssl->p_bio          = p_bio;
    ssl->f_send         = f_send;
    ssl->f_recv         = f_recv;
    ssl->f_recv_timeout = f_recv_timeout;
}

說明來自上層,那麼繼續,定位到 /home/junhuanchen/MaixPy/components/micropython/core/extmod/modussl_mbedtls.c 。

mbedtls_ssl_set_bio(&o->ssl, &o->sock, _mbedtls_ssl_send, _mbedtls_ssl_recv, NULL);

說明回調的是 _mbedtls_ssl_send 函數,以下。

STATIC int _mbedtls_ssl_send(void *ctx, const byte *buf, size_t len) {
    mp_obj_t sock = *(mp_obj_t*)ctx;

    const mp_stream_p_t *sock_stream = mp_get_stream(sock);
    int err;
    mp_uint_t out_sz = sock_stream->write(sock, buf, len, &err);
    if (out_sz == MP_STREAM_ERROR) {
        if (mp_is_nonblocking_error(err)) {
            return MBEDTLS_ERR_SSL_WANT_WRITE;
        }
        return -err;
    } else {
        return out_sz;
    }
}

通過測試發現,延時不該該上在 sock_stream->write 以前,而是以後,那麼這說明什麼問題呢?

若是要解決問題,是能夠在這裏添加延時解決問題,可是這裏是問題的源頭嗎?顯然不是。

判斷有二

  • esp32 爲何沒有這個問題?
  • 這個模塊的位置在 micropython core 體系下,說明有大量的實例支撐它們的邏輯正確性。

那麼說明問題並非這個地方致使的,在這個 micropython 的體系裏,這時候只能說明一個問題,是網卡的 sock_stream->write 請求過快返回致使的問題。

而我在用的是 ESP8285 的 AT 網卡,這就能夠聯想到 esp_send 的工做機制不必定符合標準 socket 工做時序。

爲何呢?

由於 AT 的請求只須要將數據發送過去便可完成傳輸,但傳輸正確與否請求這端不直接參與,這與其餘芯片的工做到鏈路層發送數據的機制不一樣,因此就會提早返回。

若是個人假設是正確的,那麼我應該把問題定位到這裏 components/micropython/port/src/standard_lib/network/esp8285/modesp8285.c 之中的 esp8285_socket_send 函數。

STATIC mp_uint_t esp8285_socket_send(mod_network_socket_obj_t *socket, const byte *buf, mp_uint_t len, int *_errno) {

	if((mp_obj_type_t*)&mod_network_nic_type_esp8285 != mp_obj_get_type(MP_OBJ_TO_PTR(socket->nic)))
	{
		*_errno = MP_EPIPE;
		return MP_STREAM_ERROR;
	}
	nic_obj_t* self = MP_OBJ_TO_PTR(socket->nic);
    if(socket->peer_closed)
    {
        *_errno = MP_ENOTCONN;
        return MP_STREAM_ERROR;
    }
    Buffer_Clear(&self->esp8285.buffer);//clear receive buffer
    socket->first_read_after_write = true;
	if(0 == esp_send(&self->esp8285,(const char*)buf,len, (uint32_t)(socket->timeout*1000) ) )
	{
		*_errno = MP_EPIPE;
		return MP_STREAM_ERROR;
	}
    // printk("%s len %d\r\n", __func__, len);
    mp_hal_delay_us(len * 50); // maybe 50 us time required to send per byte
    // vTaskDelay(len / portTICK_PERIOD_MS);
    return len;
}

在這裏我作了一個假設性的 timing 測試,假設爲了修復它與其餘芯片同步的工做時序,那麼我應該在發送數據後進行一段時間的延時,但這個延時是多久呢?

我如今通訊在用的波特率都是 921600 ,在這個假設可行的狀況下,我假定每一個字節發送到對端須要的時間爲 100 us 即 mp_hal_delay_us(len * 50); ,以此進行測試。

結論是實測 100 us 恢復正常工做,表示已經修復成功,那麼就結束了嗎?

還沒,咱們應該還要繼續肯定,真正預期的延時應該是多少?

接着二分法到 50 us 也成功。

繼續二分法到 25 us 也成功。

繼續二分法到 10 us 卻不成功。// 虛僞二分

那麼結論也有個大概了,考慮到 921600 的速率過快,我本來思考要麼就 25 us 最優方案。

但後來發現,每次請求的數據並不會應該這個值而有所改善性能,反而可能會存在小几率請求失敗,那既然沒影響,應該保守設置到 50 us 比較合理。

修改記錄在此

此時修改的位置很是合理,一方面不破壞 esp32 spi 那端的網卡,另外一方面也不破壞 micropython core 的代碼,從而比較優雅的解決問題。

最後作個總結

實測 https 的網站都有以下,不過 github.com 的 hostname 常常獲取不到,dns 炸裂

wCli = MicroWebCli('https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13631786501')
wCli = MicroWebCli('https://www.baifubao.com/callback?cmd=1059&callback=phone&phone=13631786501')
wCli = MicroWebCli('https://github.com')
wCli = MicroWebCli('https://ssl.logink.cn/')
wCli = MicroWebCli('https://cn.bing.com/?FORM=Z9FD1')
wCli = MicroWebCli('https://www.sojson.com')
wCli = MicroWebCli('https://www.baidu.com')

另外 HTTPS 的訪問速度沒有想象的快,從創建連接到收發數據就須要差很少 20s,相比 HTTP 的 10 秒一次請求,這中間有很大的優化空間。

簡單判斷跟加密解密的軟實現函數的效率有關,也可能跟目標網站的響應速度以及對應的 TLS 傳輸協議有關,能夠進一步優化 mbedtls 的配置文件。

須要實測真實用戶的 https 的使用環境才能進一步優化。

2020年5月11日 junhuanchen 留

相關文章
相關標籤/搜索