前幾篇的文章發表後,有網友留言說沒有涉及到阻塞的問題嗎?在 socket 的編程當中,這確實是個很重要的問題。結合目前咱們文章的內容進度,咱們來看看爲何說阻塞概念很重要。
接着上篇的內容,當咱們發送了 ehlo 命令以後就要接收服務器的返回了。這個地方是一個很容易出錯的位置,通常的網絡命令都是發送一條命令接收一條回覆,這很容易讓初學好者覺得每一個命令都是一行內容,進而在代碼中進行了錯誤的處理。而實際上不管是命令仍是對命令的應答都是有多行的狀況,若是對 socket 機制不瞭解,那就會說:那就讀取完全部的行唄。但在實際狀況中「讀取全部的行」是不可能完成的任務,由於咱們前面已經說過了 socket 其實是字節流,並無一個行結束或者一個數據包結束了的概念(固然底層實現會有 ip 包)。因此在網絡編程中有一個重要的事情,那就是怎樣定義一個數據包算是結束了?這是每一個通信協議都要解決的問題(我我的認爲是每一個協議中最爲重要的內容),在每一個通信協議中作法都不一樣,並且方法那是五花八門,用如今的話來講成是腦洞大開都不爲過。我印象最深的是前幾個月寫的一個公司的專有 http 包轉發服務器時意外發現的一個 http 包的結束表示方法,很慚愧地說,我接觸 http 協議不少年了,甚至寫過好幾個真正能用的 http 服務器實現,殊不知道這個方法 ... 這也不能怪我,加上這個方法我都數不清 http 到底有多少種表示一個包結束的方式了(是 http 中的 Transfer-Encoding chunked,之後有機會再給你們詳細介紹)。
回到 smtp 協議上來,前面的文章中其實咱們已經提到過 ehlo 命令的響應是怎樣處理的。它的迴應相似於這樣:
java
250-Eemail server 250 AUTH LOGIN
在 rfc 文檔中就有說明,讀取到有 250 並且沒跟的 "-" 符號時就能夠了。若是咱們沒有正確處理一直讀取下去,那麼就會觸發 socket 中一個著名的問題:阻塞。就是程序整個不動彈了,除了把它的進程殺死之外沒有別的任何辦法。能夠用如下 java 代碼模擬(基於上一篇的代碼):linux
//發送一個命令 //SendLine("EHLO"); //163 這樣是不行的,必定要有 domain //SendLine("EHLO" + " " + domain); //domain 要求其實來自 HELO 命令//HELO <SP> <domain> <CRLF> //收取一行 line = RecvLine(); System.out.println("recv:" + line); //收取一行 line = RecvLine(); System.out.println("recv:" + line);
這裏咱們設想,先嚐試讀取 100 行數據,當沒有行內容的狀況下就提早跳出,想是服務器的響應內容讀取完了。這個思想是沒問題的,惋惜現實下是行不通的。緣由就是 socket 的讀取函數默認狀況下會一直等待,一直到有數據爲止,若是一直沒有數據呢?那就一直在等,整個程序就中止響應了,除非對方主動把鏈接給斷開了,或者是網絡斷線了。這就是爲何安卓程序如今不容許在主線程中直接調用 socket 的最主要緣由:由於不少初學者處理很差這個問題,經常會讓程序卡死,那乾脆就強制不讓他們放在主線程了。
要解決這個問題,java 中只須要在鏈接後多加一個函數調用:
git
socket = new Socket(host, port); socket.setSoTimeout(10000);//設置超時,單位爲毫秒
以原始 socket 方式處理的話,傳統上則有好幾種作法:程序員
1.是設置 socket 的超時; 2.接收前使用 select 函數判斷是否能夠收發數據; 3.使用非阻塞的 socket; 4.使用線程。
其中第一種方法最簡單,鏈接後簡單的調用一下相關函數就一了百了(上面的 java 代碼就是如此),不過有些簡化版本的 socket 環境不必定支持;而 select 函數則最傳統,能夠在決大多數環境下使用;前兩種都要配合線程使用纔好,而非阻塞 socket 的方式則徹底不會阻塞主線程,不過編程的複雜度會直線上升級,不適合初學者。因此咱們這裏簡單地使用 select 函數來完成超時判斷,實現代碼以下:github
//是否可讀取,時間//超時返回,單位爲秒 int SelectRead_Timeout(SOCKET so, int sec) { fd_set fd_read; //fd_read:TFDSet; struct timeval timeout; // : TTimeVal; int Result = 0; FD_ZERO( &fd_read ); FD_SET(so, &fd_read ); //個數受限於 FD_SETSIZE //timeout.tv_sec = 0; //秒 timeout.tv_sec = sec; //秒 //linux 第一個參數必定要賦值 if (_select( so+1, &fd_read, NULL, NULL, &timeout ) > 0) //至少有1個等待Accept的connection Result = 1; return Result; }//
這裏要注意的是 windows 的寫法和 linux 的寫法是小有差別,你們必定要當心。
順便介紹一下其餘幾種方法的實現吧。
前面 java 代碼的超時本質就是用 setsockopt 來實現的,對於 C 語言來講相似於這樣:
面試
//設置發送超時 setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO, (char *)&timeout,sizeof(struct timeval)); //設置接收超時 setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO, (char *)&timeout,sizeof(struct timeval));
其 jdk 實現代碼爲:編程
/** * Enable/disable SO_TIMEOUT with the specified timeout, in * milliseconds. With this option set to a non-zero timeout, * a read() call on the InputStream associated with this Socket * will block for only this amount of time. If the timeout expires, * a <B>java.net.SocketTimeoutException</B> is raised, though the * Socket is still valid. The option <B>must</B> be enabled * prior to entering the blocking operation to have effect. The * timeout must be > 0. * A timeout of zero is interpreted as an infinite timeout. * @param timeout the specified timeout, in milliseconds. * @exception SocketException if there is an error * in the underlying protocol, such as a TCP error. * @since JDK 1.1 * @see #getSoTimeout() */ public synchronized void setSoTimeout(int timeout) throws SocketException { if (isClosed()) throw new SocketException("Socket is closed"); if (timeout < 0) throw new IllegalArgumentException("timeout can't be negative"); getImpl().setOption(SocketOptions.SO_TIMEOUT, new Integer(timeout)); }
多線程的相關文章汗牛充棟,咱們就不重複了。
而非阻塞的 socket 方法則相似於這樣:windows
ioctlsocket(so, FIONBIO, &arg);
我又不得不說,我很慚愧非阻塞的 socket 概念我是工做好幾年之後才據說的。準確的說是畢業不久後就知道了,不過一直覺得只是 windows 下的一種擴展,由於 windows 對 socket 的擴展不少因此也並無多在乎。後來到了一家公司面試,說他們主要用非阻塞的 socket 時才知道還能實用...... 在之後的工做當中漸漸的發現,有些工做環境下沒有非阻塞 socket 還真很差實現。因此如今非阻塞的 socket 基本上也是各個平臺都支持了的。不過非阻塞的實現難度基本上是直接上升,咱們這裏暫時就不給出示例了。這種方法的特色是 socket 被設置爲非阻塞後,全部的接收和發送都會當即返回,不論是否成功。
根據以上思想修改後的 C 語言代碼多了1個函數:服務器
//讀取多行結果 lstring * RecvMCmd(SOCKET so, struct MemPool * pool, lstring ** _buf) { int i = 0; int index = 0; int count = 0; lstring * rs; char c4 = '\0'; //判斷第4個字符 lstring * mline = NewString("", pool); for (i=0; i<50; i++) { rs = RecvLine(so, pool, _buf); //只收取一行 mline->Append(mline, rs); LString_AppendConst(mline, "\r\n"); //printf("\r\nRecvMCmd:%s\r\n", rs->str); if (rs->len<4) break; //長度要足夠 c4 = rs->str[4-1]; //第4個字符 //if ('\x20' == c4) break; //"\xhh" 任意字符 二位十六進制//其實如今的轉義符已經擴展得至關複雜,不建議用這個表示空格 if (' ' == c4) break; //第4個字符是空格就表示讀取完了//也能夠判斷 "250[空格]" }// return mline; }//
另外 RecvLine 函數中也多了幾行內容:網絡
canread = SelectRead_Timeout(so, 3);//是否可讀取,時間//超時返回,單位爲秒 if (0 == canread) break;
具體代碼有點多,仿照才慣例,請你們到如下 github 地址下載吧:
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_9
另外,雖然這個系列的文章說的是郵件發送和收取,不過其中涉及到的知識都會應用於其餘的網絡通信協議,瞭解了郵件相關的,象什麼 ftp、http 等協議其實也基本上貫通了。其實我我的也是打算將本身所瞭解的網絡編程相關的知識都放到這系列的文章中來,由於象郵件涉及到的 base6四、mime 編碼這樣的內容其實都是在其餘協議中普遍使用的。你們看完這系列的文章後寫個 http 程序也徹底不是問題。因此請你們多多關注吧!
有了前面這幾篇的文章和代碼,你們其實已經能夠用程序寫出完整的郵件發送代碼了.這和真實的郵件客戶聞風而動發送過程也差很少了(還差的主要是兩點: base64 編碼和 mime 過程,咱們會在後面的文章詳細說明).
--------------------------------------------------
版權聲明:
本系列文章已受權百家號 "clq的程序員學前班" . 文章編排上略有差別. 百家號目前對文章中的代碼轉換得很厲害,所以推薦你們在博客園這邊查看原始的代碼.