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