ssl的消息讀寫以及和tcp語義的異同

   SSL實現必須讀取整條記錄,哪怕select返回了一個字節可讀,那麼ssl也要讀取整個記錄,這種基於紀錄的讀寫方式就是爲了正確的加密個解密。因此如果用select模型的話可能會出現一些莫名其妙的問題,事實上也正是ssl消息需要加密解密從而需要整個消息整個消息讀寫才使得ssl協議的行爲和tcp的有了少有的不一致。

     1、tcp的特點是流式傳輸,流式的特點就是沒有消息邊界,一個連接就是一個流,需要應用程序自己去劃分自己的數據,舉個例子就是一端寫入x個字節,對端可能讀出y個字節,具體多少要看網絡狀況和窗口情況,tcp在這一點上是相當複雜的,應用程序的發送只是簡單的將數據放入tcp的發送緩衝區,而接收只是簡單的從接收緩衝區中取回數據。

    2、反觀udp就不是這樣子,udp是基於數據報的,就是說不能分段,一端寫入多少另一端就讀出多少,當然也可能永遠收不到,也可能亂序等等。

     3、現在看看ssl,它看起來好像是結合了tcp和udp的特點,它是有連接的,必須可靠傳輸並且按照順序收發,但是卻不是流式的,每次調用SSL_read必須讀入一個ssl紀錄(一個ssl紀錄有一個固定大小的頭部[5字節],該頭部指示了消息類型,ssl版本號以及消息長度),首先需要讀出一個ssl消息頭部,接下來就要在該頭部的消息長度字段的指導下進行消息體的讀取,而且必須讀取完整個完整消息之後才能返回成功,否則均返回失敗,並且什麼都不做,ssl讀操作中,帶有頭的消息是read的最小單位。

ssl3_read_bytes是openssl中SSL_read最終要調用的函數,它內部調用了ssl3_get_record:

static int ssl3_get_record(SSL *s)
{
...
    rr= &(s->s3->rrec);
    sess=s->session;
...

again:
    if ((s->rstate != SSL_ST_READ_BODY) ||
    (s->packet_length < SSL3_RT_HEADER_LENGTH)) {
        n=ssl3_read_n(s, SSL3_RT_HEADER_LENGTH, s->s3->rbuf.len, 0);
        if (n <= 0) return(n);
        s->rstate=SSL_ST_READ_BODY;
        p=s->packet;
        rr->type= *(p++);   //得到消息頭中的消息類型
        ssl_major= *(p++);  //得到消息頭中的主版本號
        ssl_minor= *(p++);  //得到消息頭中的次版本號
        version=(ssl_major<<8)|ssl_minor; //組合成版本號
        n2s(p,rr->length);  //得到消息的長度
...
    }
    if (rr->length > s->packet_length-SSL3_RT_HEADER_LENGTH) {
        i=rr->length;
        n=ssl3_read_n(s,i,i,1); //按照消息長度讀取消息
        if (n <= 0) return(n);
    }
    s->rstate=SSL_ST_READ_HEADER;
...

}
 

在ssl3_read_n的主要邏輯很簡單:
while (newb < n) {
    clear_sys_error();
    s->rwstate=SSL_READING;
    i=BIO_read(s->rbio, &(s->s3->rbuf.buf[off+newb]), max-newb);
    if (i <= 0) {    //只要沒有讀到數據,那麼就返回
        s->s3->rbuf.left = newb;
        return(i);
    }
    newb+=i;
}
 

int ssl3_pending(const SSL *s)
{
    if (s->rstate == SSL_ST_READ_BODY)
        return 0;
    return (s->s3->rrec.type == SSL3_RT_APPLICATION_DATA) ? s->s3->rrec.length : 0;
}
 

     通過SSL_pending可以判斷是否有消息數據還在緩衝區或者還沒有到緩衝區,它實際上返回的就是消息的長度,因此如果使用select調用的話,很有可能select檢測到的可讀情況僅僅只有tcp送來的很少的數據量,遠遠不夠ssl需要的數據量,那麼只要SSL_pending返回非0,那麼就需要循環調用SSL_read繼續讀取,否則你會認爲這是一個莫名其妙的錯誤,明明select返回了,爲何SSL_read卻讀不到數據,注意,在ssl讀緩衝區被完全的消息填滿前,SSL_read是不會返回任何數據的。同樣的,SSL_write也是一樣的道理,總之在openssl的實現中,一個ssl擁有一個SSL3_BUFFER類型的結構體(v3):

typedef struct ssl3_buffer_st {
    unsigned char *buf;     /* at least SSL3_RT_MAX_PACKET_SIZE bytes,
    size_t len;             /* buffer size */
    int offset;             /* where to 'copy from' */
    int left;               /* how many bytes left */
} SSL3_BUFFER;

     可以看到在ssl_st結構體中有ssl3_state_st類型的字段,ssl3_state_st中有SSL3_BUFFER類型的rbuf和wbuf,它們並不是鏈表,而是隻有一個緩衝區,並且在ssl_write中並沒有看到有線程保護的措施,因此每一個ssl連接存在且僅存在一對SSL3_BUFFER,也就是說每次只能由一個線程操作一個讀緩衝或者一個寫緩衝,這就迎合了openssl文檔中的一個FAQ:Is OpenSSL thread-safe? Yes (with limitations: an SSL connection may not concurrently be used by multiple threads).這就是不能在多個線程操作同一個ssl指針的原因,當初這個問題可害得我加了好幾個週末的班啊。特別要注意的是,如果用select模型來寫基於ssl的程序,一定要弄清楚ssl和tcp語義的不同,也正是這種不同點使得將傳統套接字程序移植成ssl套接字程序並不是我一年前認爲的那麼簡單。

發佈了15 篇原創文章 · 獲贊 6 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章