??? 讓我們回到 smtp/pop3 等網絡命令上來. 前面的文章已經說過了大多數的網絡命令都是基于網絡命令行的,我們就先來研究一行命令本身. ??? 讀取一行命令,在前面的 java 語言示例中實現很簡單:
String s = br.readLine();
??? 也就是說 java 中直接實現了讀取一行的功能. 這個實現其實也沒初學者想象的那么簡單,甚至是網絡編程中一個很易錯的地方.換到直接操作 socket 的大多數環境中來,我們仍然以 C 語言為示例.我們以前面代碼中的 RecvBuf() 來讀取一行,這在真實的環境中是不正確的.如果大家測試過我們之前的示例一定會發現一個奇怪的現象:怎么有時候會一次收到好幾行的內容呢? ??? 這就涉及到一個很重要的概念:字節流 . 在網絡中傳輸的數據并不是我們發送多少對方就能接收到多少 ,更不是我們發送一次就對應對方的一次接收,而是我們發送的內容作為一個整體的字節流在網絡中傳輸,如果我們發送了兩次后對方才開始接收,那么對方就會一次性收到兩次發出的命令.字節流的情況下還有一個更嚴重的問題:發送一次命令,對方有可能會花很多次接收調用后才能收取完! 很多程序員是不懂得這一點的,甚至很多公司的代碼里長期存在這種缺陷的代碼! 這種現象在現在這種網絡環境超級好的中國國內是比較難重現的,在我們以前的撥號環境中就很常見,估計這也是現在的程序員不了解這種情況的原因之一吧. 既然比較難給出一個實例讓大家去測試這種情況,那么讓我們從原理上來解釋吧. 我們先來看看 C 語言示例中的 RecvBuf() 函數實現.代碼如下:
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 函數,而這個函數的原型為
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 來實現第一種一次讀取一個字節的實現方法(對于客戶端接收命令來說,其實效率也沒那么差,因為一般客戶端就幾個連接在工作嘛). 對比我們之前的結果,可以看到二者的區別.如圖:
有經驗并且眼尖的讀者一定看到了 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 語法可能也不熟,不過還是可以比較清楚的看到它也是先保存到一個緩沖區的. 改進后的完整代碼如下(相關的依賴文件見文章末尾處):
#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的程序員學前班" . 文章編排上略有差異.
?
總結
以上是生活随笔 為你收集整理的一步一步从原理跟我学邮件收取及发送 7.读取一行命令的实现 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。