讓咱們回到 smtp/pop3 等網絡命令上來. 前面的文章已經說過了大多數的網絡命令都是基於網絡命令行的,咱們就先來研究一行命令自己.
讀取一行命令,在前面的 java 語言示例中實現很簡單:java
String s = br.readLine();
也就是說 java 中直接實現了讀取一行的功能. 這個實現其實也沒初學者想象的那麼簡單,甚至是網絡編程中一個很易錯的地方.換到直接操做 socket 的大多數環境中來,咱們仍然以 C 語言爲示例.咱們之前面代碼中的 RecvBuf() 來讀取一行,這在真實的環境中是不正確的.若是你們測試過咱們以前的示例必定會發現一個奇怪的現象:怎麼有時候會一次收到好幾行的內容呢?
這就涉及到一個很重要的概念:字節流. 在網絡中傳輸的數據並非咱們發送多少對方就能接收到多少,更不是咱們發送一次就對應對方的一次接收,而是咱們發送的內容做爲一個總體的字節流在網絡中傳輸,若是咱們發送了兩次後對方纔開始接收,那麼對方就會一次性收到兩次發出的命令.字節流的狀況下還有一個更嚴重的問題:發送一次命令,對方有可能會花不少次接收調用後才能收取完! 不少程序員是不懂得這一點的,甚至不少公司的代碼里長期存在這種缺陷的代碼! 這種現象在如今這種網絡環境超級好的中國國內是比較難重現的,在咱們之前的撥號環境中就很常見,估計這也是如今的程序員不瞭解這種狀況的緣由之一吧. 既然比較難給出一個實例讓你們去測試這種狀況,那麼讓咱們從原理上來解釋吧.
咱們先來看看 C 語言示例中的 RecvBuf() 函數實現.代碼以下:
c++
lstring * RecvBuf(SOCKET so, struct MemPool * pool) { char buf[1024+1]; int r = 0; lstring * s = NewString("", pool); memset(&buf, 0, sizeof(buf)); r = _recv(so, buf, sizeof(buf)-1, 0); //留下一個 #0 結尾 if (r > 0) { LString_AppendCString(s, buf, r); } if (r == 0) //通常都是斷開了 { MessageBox(0, "recv error.[socket close]", "", 0); return s; } return s; }//
拋開字符串內存池的相關代碼,實際上主要調用的是系統的 recv() socket 函數,而這個函數的原型爲
git
int recv(SOCKET s, char *buf, int len, int flags);
其中 flags 通常是 0 能夠不理會,而 s 就是網絡鏈接也能夠不用理會. buf 是接收到的數據要存放的緩衝區, len 則是緩衝區的大小,接收到的數據是不會大於 len 的(C語言的程序員都知道,那樣就內存溢出了). 好了,讓咱們來模擬一下一個命令要接收兩次的狀況吧: 咱們發送一個 4K 長的命令的話,咱們 RecvBuf 一下, 眼尖的程序員必定看出來了,緩衝區只有 1024 啊. 很顯然要接收屢次才行嘛! 有的讀者立刻就會說了,你這不對嘛,誰讓你緩衝區這麼小的,來個 4K 的緩衝區! 這裏有幾個問題:你怎麼知道 4K 就必定能接收完一個命令? 若是我是 1M 長的命令行呢? 那就開 1M 的 ... 若是我發送了一個 G 的文件過來呢? 先不考慮系統能不能開出這麼大的緩衝區,先想想 1G 長的文件或者命令要在網絡中傳送多久,那麼這個 recv 函數是要等這一個 G 的數據都傳輸完了才返回嗎? 很顯然不是,要不咱們就不會看到下載文件時的進度條了. 因此 recv 是在有數據到達時就返回了的,而無論收取到了多少,若是返回時收取了兩個命令的數據那就會象咱們示例中的那樣顯示出兩個命令的內容.若是隻收取到半個命令,就會出現過去撥號環境下的那種要屢次收取才能獲得一行命令的狀況. 實際上數據在互聯網中傳輸要通過不少設備,設備給每一個鏈接的緩衝都是有限的,不可能期望對方發送的內容咱們一次性就能收取. 因此在全部正規的 socket 的封裝中都是要先用 recv 把數據先收取到程序中的一個緩衝區,通常來講會把每一個鏈接設計成一個類,而後開一個 buf ,系統有空閒時就不停的調用 recv 直到 buf 收滿爲止,而後當程序要 readLine 一行時就在緩衝區取出一行的內容給它. 這就是 java 或者相似環境的 readLine 函數的實現原理和緣由.
可是假如咱們在 C 語言的測試中也這樣作的話就比較困難,一是 C 語言沒有類,二是 C 語言的內存管理不那麼好作,要把這兩個問題都解決好了再進行測試的話就太繁瑣了,並且代碼量太多的話也很差重用和維護. 因此咱們能夠先作一個簡單的實現:每次讀取一個字節,拼到字符串中,讀取到行結束符時返回就能夠了.這個代碼很容易實現,但在實際的環境中效率是比較低的,能夠在測試完成後實現一個更好一點的版本:每次讀取一個緩衝區的內容,函數返回當前的命令和餘下的內容,下次收取時再將餘下的內容與新收取的內容合併就能夠了.
根據以上思想就能夠獲得這樣收取一行的函數:程序員
//收取一行,可再優化 lstring * RecvLine(SOCKET so, struct MemPool * pool, lstring ** _buf) { int i = 0; int index = -1; lstring * r = NULL; lstring * s = NULL; lstring * buf = *_buf; for (i=0;i<10;i++) //安全起見,不用 while ,用 for 必定次數就能夠了 { //index = pos("\n", buf); index = pos(NewString("\r\n", pool), buf); if (index>-1) break; s = RecvBuf(so, pool); buf->Append(buf, s); } if (index <0 ) return NewString("", pool); r = substring(buf, 0, index); buf = substring(buf, index + 2, Length(buf)); *_buf = buf; return r; }
由於這個示例中我已經實現了一個簡單的字符串內存池,因此按第二種先讀取到緩衝區的方法實現了這個讀取一行的命令.若是你們在 c++ 環境裏能夠本身用 std::string 來實現第一種一次讀取一個字節的實現方法(對於客戶端接收命令來講,其實效率也沒那麼差,由於通常客戶端就幾個鏈接在工做嘛).
對比咱們以前的結果,能夠看到兩者的區別.如圖:github
有經驗而且眼尖的讀者必定看到了 RecvLine 裏的讀取循環是 for 了必定的次數而不是用的 while 到成功後再跳出,這是由於長期的服務端開發我發現由於開發週期緊或者開發人員經驗不足或者考慮不周等狀況,在 while 裏出現問題的話很容易形成死循環致使服務器不響應.因此我習慣在須要循環或者遞歸的地方設置必定的次數,若是超出這些次數就認爲是出錯了強制跳出.若是你們的服務器也不太穩定能夠考慮也加入這種機制,仍是頗有效的.
你們可能對我讀取一行的方法可能有疑慮,那咱們來看看現有的語言是怎樣實現的吧,java 的源碼不是太方便看,我仍是先用下 delphi 的吧:
編程
function TIdTCPConnection.ReadLn(ATerminator: string = LF; const ATimeout: Integer = IdTimeoutDefault; AMaxLineLength: Integer = -1): string; var LInputBufferSize: Integer; LSize: Integer; LTermPos: Integer; begin if AMaxLineLength = -1 then begin AMaxLineLength := MaxLineLength; end; // User may pass '' if they need to pass arguments beyond the first. if Length(ATerminator) = 0 then begin ATerminator := LF; end; FReadLnSplit := False; FReadLnTimedOut := False; LTermPos := 0; LSize := 0; repeat LInputBufferSize := InputBuffer.Size; if LInputBufferSize > 0 then begin LTermPos := MemoryPos(ATerminator, PChar(InputBuffer.Memory) + LSize, LInputBufferSize - LSize); if LTermPos > 0 then begin LTermPos := LTermPos + LSize; end; LSize := LInputBufferSize; end;//if if (LTermPos - 1 > AMaxLineLength) and (AMaxLineLength <> 0) then begin if MaxLineAction = maException then begin raise EIdReadLnMaxLineLengthExceeded.Create(RSReadLnMaxLineLengthExceeded); end else begin FReadLnSplit := True; Result := InputBuffer.Extract(AMaxLineLength); Exit; end; // ReadFromStack blocks - do not call unless we need to end else if LTermPos = 0 then begin if (LSize > AMaxLineLength) and (AMaxLineLength <> 0) then begin if MaxLineAction = maException then begin raise EIdReadLnMaxLineLengthExceeded.Create(RSReadLnMaxLineLengthExceeded); end else begin FReadLnSplit := True; Result := InputBuffer.Extract(AMaxLineLength); Exit; end; end; // ReadLn needs to call this as data may exist in the buffer, but no EOL yet disconnected CheckForDisconnect(True, True); // Can only return 0 if error or timeout FReadLnTimedOut := ReadFromStack(True, ATimeout, ATimeout = IdTimeoutDefault) = 0; if ReadLnTimedout then begin Result := ''; Exit; end; end; until LTermPos > 0; // Extract actual data Result := InputBuffer.Extract(LTermPos + Length(ATerminator) - 1); // Strip terminators LTermPos := Length(Result) - Length(ATerminator); if (ATerminator = LF) and (LTermPos > 0) and (Result[LTermPos] = CR) then begin SetLength(Result, LTermPos - 1); end else begin SetLength(Result, LTermPos); end; end;//ReadLn
這是 delphi 中著名的 indy 組件的實現,雖然代碼比較長,你們對 delphi 語法可能也不熟,不過仍是能夠比較清楚的看到它也是先保存到一個緩衝區的.
改進後的完整代碼以下(相關的依賴文件見文章末尾處):
windows
#include <stdio.h> #include <windows.h> #include <time.h> #include <winsock.h> #include "lstring.c" #include "socketplus.c" #include "lstring_functions.c" //vc 下要有可能要加 lib //#pragma comment (lib,"*.lib") //#pragma comment (lib,"libwsock32.a") //#pragma comment (lib,"libwsock32.a") //SOCKET gSo = 0; SOCKET gSo = -1; //收取一行,可再優化 lstring * RecvLine(SOCKET so, struct MemPool * pool, lstring ** _buf) { int i = 0; int index = -1; lstring * r = NULL; lstring * s = NULL; lstring * buf = *_buf; for (i=0;i<10;i++) //安全起見,不用 while ,用 for 必定次數就能夠了 { //index = pos("\n", buf); index = pos(NewString("\r\n", pool), buf); if (index>-1) break; s = RecvBuf(so, pool); buf->Append(buf, s); } if (index <0 ) return NewString("", pool); r = substring(buf, 0, index); buf = substring(buf, index + 2, Length(buf)); *_buf = buf; return r; } void main() { int r; mempool mem, * m; lstring * s; lstring * rs; lstring * buf; //-------------------------------------------------- mem = makemem(); m = &mem; //內存池,重要 buf = NewString("", m); //-------------------------------------------------- //直接裝載各個 dll 函數 LoadFunctions_Socket(); InitWinSocket(); //初始化 socket, windows 下必定要有 gSo = CreateTcpClient(); r = ConnectHost(gSo, "newbt.net", 25); if (r == 1) printf("鏈接成功!\r\n"); s = NewString("EHLO\r\n", m); SendBuf(gSo, s->str, s->len); printf(s->str); s->Append(s, s); printf(s->str); s->AppendConst(s, "中文\r\n"); printf(s->str); //-------------------------------------------------- rs = RecvLine(gSo, m, &buf); //只收取一行 printf("\r\nRecvLine:"); printf(rs->str); printf("\r\n"); rs = RecvLine(gSo, m, &buf); //只收取一行 printf("\r\nRecvLine:"); printf(rs->str); printf("\r\n"); rs = RecvLine(gSo, m, &buf); //只收取一行 printf("\r\nRecvLine:"); printf(rs->str); printf("\r\n"); //-------------------------------------------------- // rs = RecvBuf(gSo, m); //注意這個並不僅是收取一行 // // printf("\r\nRecvBuf:\r\n"); // printf(rs->str); // // rs = RecvBuf(gSo, m); //注意這個並不僅是收取一行 // printf("\r\nRecvBuf:\r\n"); // printf(rs->str); //-------------------------------------------------- Pool_Free(&mem); //釋放內存池 printf("gMallocCount:%d \r\n", gMallocCount); //看看有沒有內存泄漏//簡單的檢測而已 //-------------------------------------------------- getch(); //getch().不過在VC中好象要用getch(),必須在頭文件中加上<conio.h> }
不知不覺這篇內容又佔了很大的篇幅,這也是沒辦法,由於感受確實有這麼多要講的,生怕哪一個地方沒說清楚又上你們走了彎路.若是你們看着以爲囉嗦,那就請多見諒吧!
--------------------------------------------------
本想上傳依賴的相關文件到 github,到本身的帳號裏一看原來那個字符串的類已經傳過了.因此補充了 socketplus.c 就行了. 你們能夠到如下網址下載:
https://github.com/clqsrc/c_lib_lstring
也能夠到以前同系列的文章中去複製,不過二者內容略有差別. 用 github 上的較好,由於之後有可能更新.安全
本系列文章已受權百家號 "clq的程序員學前班" . 文章編排上略有差別.服務器