天天看點

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

SSL實作必須讀取整條記錄,哪怕select傳回了一個位元組可讀,那麼ssl也要讀取整個記錄,這種基于紀錄的讀寫方式就是為了正确的加密個解密。是以如果用select模型的話可能會出現一些莫名其妙的問題,事實上也正是ssl消息需要加密解密進而需要整個消息整個消息讀寫才使得ssl協定的行為和tcp的有了少有的不一緻。

     tcp的特點是流式傳輸,流式的特點就是沒有消息邊界,一個連接配接就是一個流,需要應用程式自己去劃分自己的資料,舉個例子就是一端寫入x位元組,對端可能讀出y位元組,具體多少要看網絡狀況和視窗情況,tcp在這一點上是相當複雜的,應用程式的發送隻是簡單的将資料放入tcp的發送緩沖區,而接收隻是簡單的從接收緩沖區中取回資料,反觀udp就不是這樣子,udp是基于資料報的,就是說不能分段,一端寫入多少另一端就讀出多少,當然也可能永遠收不到,也可能亂序等等。現在看看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); //按照消息長度讀取消息

    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套接字程式并不是我一年前認為的那麼簡單。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271983

繼續閱讀