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