DELPHI下的SOCK編程
本文是寫給公司新來的程序員的,算是一點培訓的教材。本文不會涉及太多的編程細節,只是簡單講解在DELPHI下進行Winsock編程最好了解的知識。
題外話:我認為學習編程就如同學習外語一樣,最好的方式是你先學會如何去運用它,然后才是了解它的語言特性、語法之類的東西。不過很可惜,我們以前的外語教育使用了相反的過程。軟件編程也是一樣,在很多人的大學階段,你更多的是學習那些理論知識,學習“語法”,這里,我絲毫沒有貶低理論知識重要性的意思。理論知識和實踐是相輔相成的,但一個恰當的學習方式,很多時候可以讓學習者得到事半功倍的效果。例如你學習《數據結構》中排序的概念,我們假設對此概念你學習的非常出色,你腦子中有大量關於不同排序方法優劣的思想,你也自己琢摸出了很多奇妙的構思,但你沒有絲毫編寫代碼的基本功,你所有一切的nice idea都只能停留在你的腦子中,你甚至不知道在實踐運用中,這些idea是否可行,實踐是檢驗真理的唯一標准。倘若你先具備了一些可以用於實踐的基本技能,再去學習那些概念,我想,你可以很好的將”冒泡排序“實現出來,然后在這種實現和理論的結合中不斷的前行。本文會從DELPHI7中的TServerSocket等組件出發,邊用邊學,直到API以及那些所謂的”思想“。
“當你相信並不理解的東西時,你就會受罪了。迷信不是辦法”。--Stevice Wonder
RAD使得程序的編制更方便和快捷,即使沒有經受專業的訓練,依然可以寫出完成一些功能的程序,在我現在的公司內部,有時候我會問公司的新員工,你為什么這樣寫,你的代碼是怎么運行的?“不知道,反正可以運行。”我認為對於准備在Win32開發領域做下去的程序員來說,這是悲哀的。前面已經說了, 本文最終的目的不是簡單的組件使用,而是讓讀者可以了解到Winsock的API以及DELPHI7中TServerSocket等VCL組件的編寫。我始終認為對於一名合格的win32程序員來說,無論你使用什么工具編寫代碼,你都應該深入底層,起碼你應該了解Win API,否則的話,在面對那些組件錯誤的時候,你只能莫名其妙的扣着腦袋,在編程的道路上,最多也只能成為一名熟練的”搬運工“。
閱讀本文,你起碼應該具備對Object Pascal的了解,如果能有一點UML的知識就更好了,因為文中會包含一些這方面的圖形。
WINSOCK的基本概念
在Win32平台下進程網絡編程,對於眾多的基層網絡協議,Winsock是訪問它們的首選接口。而且在每個Win32平台上,Winsock都以不同的形式存在着。
要說明的是,Winsock是網絡編程接口,而不是協議。它從Unix平台的Berkeley(BSD)套接字方案借鑒了許多東西,后者能訪問多種網絡協議。在Win32環境中,Winsock接口最終成為一個真正的“與協議無關”接口,尤其是在Winsock 2發布之后。
Winsock的API是建立在套接字基礎上的。所謂套接字,就是一個指向傳輸提供者的句柄。Win32中,套接字不同於文件描述符,所以它是一個獨立的類型—SOCKET。
第一個程序
在了解那些組件和API之前,我們先來使用一下。下面的程序非常的簡單,只是一個在服務器和客戶機之間建立連接並通訊的程序。
如果你已經使用TServerSocket以及TClientSocket做過客戶端服務器的通訊程序,那么下面的內容你完全可以跳過。
我們使用TServerSocket組件來建立服務器端的程序。
它包含兩個memo組件,用來分別處理接收到的數據和發送的數據,然后再在窗體上方一個TServerSocket組件,ServerType設置為stNonBlocking,Port設置為100,active設置為true,寫幾句簡單的代碼如下:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, StdCtrls, ScktComp;
type
TForm1 = class(TForm)
ServerSocket1: TServerSocket;
Memo1: TMemo;
Button1: TButton;
StatusBar1: TStatusBar;
Memo2: TMemo;
procedure ServerSocket1ClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ServerSocket1ClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
procedure Button1Click(Sender: TObject);
private
...{ Private declarations }
public
...{ Public declarations }
end;
var
Form1: TForm1;
implementation
...{$R *.dfm}
procedure TForm1.ServerSocket1ClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
StatusBar1.SimpleText := 'connect';
end;
procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
StatusBar1.SimpleText := 'disconnect';
end;
procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
begin
Memo1.Lines.Add( Socket.ReceiveText);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if ServerSocket1.Active then
ServerSocket1.Socket.Connections[0].SendText(Memo2.Text);
end;
end.
然后編譯程序。
客戶端實現:
客戶端相對復雜一些,因為我們要有一個用來接收服務器IP地址的edit,以及端口地址的edit。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, StdCtrls, ScktComp;
type
TForm1 = class(TForm)
ClientSocket1: TClientSocket;
edIp: TEdit;
Label1: TLabel;
Label2: TLabel;
edPort: TEdit;
StatusBar1: TStatusBar;
Memo1: TMemo;
btnConnect: TButton;
btnSent: TButton;
btnDisconnect: TButton;
Memo2: TMemo;
procedure ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
procedure btnConnectClick(Sender: TObject);
procedure btnDisconnectClick(Sender: TObject);
procedure btnSentClick(Sender: TObject);
procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
private
...{ Private declarations }
public
...{ Public declarations }
end;
var
Form1: TForm1;
implementation
...{$R *.dfm}
procedure TForm1.ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
begin
StatusBar1.SimpleText := '連接成功';
end;
procedure TForm1.btnConnectClick(Sender: TObject);
begin
if not ClientSocket1.Active then
begin
ClientSocket1.Host := edIp.Text;
ClientSocket1.Port := StrToInt(edPort.Text);
ClientSocket1.Open;
(Sender as TButton).Enabled := false;
btnDisconnect.Enabled := true;
end;
end;
procedure TForm1.btnDisconnectClick(Sender: TObject);
begin
if ClientSocket1.Active then
begin
ClientSocket1.Close;
btnDisconnect.Enabled := false;
btnConnect.Enabled := true;
end;
end;
procedure TForm1.btnSentClick(Sender: TObject);
begin
if ClientSocket1.Active then
ClientSocket1.Socket.SendText(Memo1.Text)
end;
procedure TForm1.ClientSocket1Read(Sender: TObject;
Socket: TCustomWinSocket);
begin
Memo2.Lines.Add(Socket.ReceiveText);
end;
end
. 上面的代碼非常簡單,我甚至不想對它添加任何評述,只所以放上去這個例子,只是想讓那些對sock沒有任何概念的讀者先有一個簡單的了解。是的,我們可以使用Winsock來編程一個網絡會話程序。
去看看源碼
前面說過,本文的目的不是告訴你如何使用DELPHI中這些用於winsock編程的組件,我們的目的是Winsock Api。
那么我們現在就去看看TClientSocket以及TServerSocket的源碼。他們在ScktComp.pas單元中被實現,OK,我們看到了此單元中定義的一些類,其中自然包含TClientSocket以及TServerSocket,看看他們的繼承關系,下面的一幅UML類圖我想可以反映一些信息。
圖中,我划分了一些區域,並對他們上色,這並不是UML的部分,只所以這樣,是為了方便我這里的描繪。
從ScktComp類圖中,我們看到了5塊區域,從藍色的4號區域,我們可以看到TClientSocket以及TServerSocket的繼承關系,是的,你看到了TComponent,也就是說,TClientSocket以及TServerSocket是一個組件,他們的目的是讓我們可以建立典型的客戶機/服務器模式的通訊程序,從設計的概念上說,他們並不負責通訊的具體處理,仔細觀察他們的代碼,你會發現二者有一個共同的地方,就是都有一個T***WinSocket的私有變量,是的,T***WinSocket才是真正完成對WinSock API封裝的地方。
勿在淺沙築高樓。在談論TServerSocket等組件編寫之前,這里先對Winsock中一些基本概念和API函數做一個簡單的說明。
一、定址
要通過Winsock建立通信,必須了解如何利用指定的協議為工作站定址。Winsock 2引入了幾個新的、與協議無關的函數,它們可和任何一個地址家族一起使用;但是大多數情況下,各協議家族都有自己的地址解析機制,要么通過一個函數,要么作為一個投給getsockopt的選項。
因為目前網絡編程中用的最多最普遍的也許就是TCP/IP協議了,所以這里主要介紹此協議下的WinSock編程。
1、IP
網際協議(Internet Protocol, IP)是一種用於互聯網的網絡協議,已經廣為人知。它可廣泛用於大多數計算機操作系統上,也可用於大多數局域網LAN(比如辦公室小型網絡)和廣域網WAN(比如說互聯網)。從它的設計看來, IP是一個無連接的協議,不能保證數據投遞萬
無一失。兩個比它高級的協議(TCP和UDP)用於依賴IP協議的數據通信。
2、TCP
面向連接的通信是通過“傳輸控制協議”(Transmission Control Protocol, TCP)來完成的。TCP提供兩台計算機之間的可靠無錯的數據傳輸。應用程序利用TCP進行通信時,源和目標之間會建立一個虛擬連接。這個連接一旦建立,兩台計算機之間就可以把數據當作一個雙向字
節流進行交換。
3、UDP
無連接通信是通過“用戶數據報協議”(User Datagram Protocol, UDP)來完成的。UDP不保障可靠數據的傳輸,但能夠向若干個目標發送數據,接收發自若干個源的數據。簡單地說,如果一個客戶機向服務器發送數據,這一數據會立即發出,不管服務器是否已准備接收數據。如果服務器收到了客戶機的數據,它不會確認收到與否。數據傳輸方法采用的是數據報。
TCP和UDP兩者都利用IP來進行數據傳輸,一般稱為TCP/IP和UDP/IP。Winsock通過AF_INET地址家族為IP通信定址。
4、定址
IP中,計算機都分配有一個IP地址,用一個32位數來表示,正式的稱呼是“IPv4地址”。客戶機需要通過TCP或UDP和服務器通信時,必須指定服務器的IP地址和服務端口號。另外,服務器打算監聽接入客戶機請求時,也必須指定一個IP地址和一個端口號。Winsock中,應用通過SOCKADDR_IN結構來指定I P地址和服務端口信息,該結構的在DELPHI中的聲明如下:
sockaddr_in = record
case Integer of
0: (sin_family: u_short;
sin_port: u_short;
sin_addr: TInAddr;
sin_zero: array[0..7] of Char);
1: (sa_family: u_short;
sa_data: array[0..13] of Char)
end;
TSockAddrIn = sockaddr_in;
在DELPHI中,sockaddr_in結構被聲明為了一個變體記錄(關於變體記錄可以參看我其他的文章)。sin_family: 字段必須設為AF_INET,以告知Winsock我們此時正在使用I P地址家族。
准備使用哪個TCP或UDP通信端口來標識服務器服務這一問題,則由sin_port字段定義。在選擇端口時,應用必須特別小心,因為有些可用端口號是為“已知的”(即固定的)服務保留的(比如說文件傳輸協議和超文本傳輸協議,即FTP和HTTP)。“已知的協議”,即固定協議,采用的端口由“互聯網編號分配認證(IANA)”控制和分配,RFC 1700中說明編號。從本質上說,端口號分為下面這三類:“已知”端口、已注冊端口、動態和(或)私用端口。
■ 0~1023由IANA控制,是為固定服務保留的。
■ 1024 ~ 49151是IANA列出來的、已注冊的端口,供普通用戶的普通用戶進程或程序使用。
■ 49152 ~ 65535是動態和(或)私用端口。
普通用戶應用應該選擇1024 ~ 49151之間的已注冊端口,從而避免端口號已被另一個應用或系統服務所用。此外, 49152 ~ 65535之間的端口可自由使用,因為IANA這些端口上沒有注冊服務。在使用bind API函數時,如果一個應用和主機上的另一個應用采用的端口號綁定在一起,系統就會返回Winsock錯誤WSAEADDRINUSE。sockaddr_in 結構的sin_addr字段用於把一個IP地址保存為一個4字節的數,它是無符號長整數類型。根據這個字段的不同用法,還可表示一個本地或遠程IP地址。IP地址一般是用“互聯網標准點分表示法”(像a.b.c.d一樣)指定的,每個字母代表一個字節數,從左到右分配一個4字節的無符號長整數。最后一個字段sin_ zero ,只充當填充項的職責,以使sockaddr_in 結構和SOCKADDR結構的長度一樣。一個有用的、名為inet_addr的支持函數,可把一個點式IP地址轉換成一個32位的無符號長整數。它的定義如下:
unsigned long inet_addr(
const char FAR *cp
);
cp字段是一個空中止字符串,它認可點式表示法的IP地址。注意,這個函數把IP地址當作一個按網絡字節順序排列的32位無符號長整數返回.
1. 特殊地址
對於特定情況下的套接字行為,有兩個特殊IP地址可對它們產生影響。特殊地址INADDR_ANY允許服務器應用監聽主機計算機上面每個網絡接口上的客戶機活動。一般情況下,在該地址綁定套接字和本地接口時,網絡應用才利用這個地址來監聽連接。如果你有一個多址系統,這個地址就允許一個獨立應用接受發自多個接口的回應。
特殊地址INADDR_BROADCAST用於在一個IP網絡中發送廣播UDP數據報。要使用這個特殊地址,需要應用設置套接字選項SO_BROADCAST。
2. 字節排序
針對“大頭”(big-endian)和“小頭”(little-endian)形式的編號,不同的計算機處理器的表示方法有所不同,這由各自的設計決定。比如, Intel 86處理器上,用“小頭”形式來表示多字節編號:字節的排序是從最無意義的字節到最有意義的字節。在計算機中把IP地址和
端口號指定成多字節數時,這個數就按“主機字節”(host-byte)順序來表示。但是,如果在網絡上指定I P地址和端口號,“互聯網聯網標准”指定多字節值必須用“大頭”形式來表示(從最有意義的字節到最無意義的字節),一般稱之為“網絡字節”(network-byte)順序。有一系列的函數可用於多字節數的轉換,把它們從主機字節順序轉換成網絡字節順序,反之亦然。下面四個API函數便將一個數從主機字節順序轉換成網絡字節順序:
HTONL,htons,WSAHtons,WSAHtonl
下面這四個是前面四個函數的反向函數:它們把網絡字節順序轉換成主機字節順序:ntohl,WSANtohl,ntohs,WSANtohs
Winsock的初始化
每個Winsock應用都必須加載Winsock DLL的相應版本。如果調用Winsock之前,沒有加載Winsock庫,這個函數就會返回一個SOCKET_ERROR,錯誤信息是WSANOTINITIALISED。
加載Winsock庫是通過調用WSAStartup函數實現的。這個函數在DELPHI中的WinSock單元被定義如下:
function WSAStartup(wVersionRequired: word; var WSData: TWSAData): Integer; stdcall;
ScktComp中這樣使用了此函數
procedure Startup;
var
ErrorCode: Integer;
begin
ErrorCode := WSAStartup($0101, WSAData);
if ErrorCode <> 0 then
raise ESocketError.CreateResFmt(@sWindowsSocketError,
[SysErrorMessage(ErrorCode), ErrorCode, 'WSAStartup']);
end;
錯誤檢查和控制
對編寫成功的Winsock應用程序而言,錯誤檢查和控制是至關重要的。事實上,對Winsock函數來說,返回錯誤是非常常見的。但是,多數情況下,這些錯誤都是無關緊要的,通信仍可在套接字上進行。盡管其返回的值並非一成不變,但不成功的Winsock調用返回的最常見的值是SOCKET_ERROR。在詳細介紹各個API調用時,我們打算指出和各個錯誤對應的返回值。實際上,SOCKET_ERROR常量是- 1。
如果調用一個Winsock函數,錯誤情況發生了,就可用WSAGetLastError函數來獲得一段代碼,這段代碼明確地表明發生的狀況。該函數的定義如下:function WSAGetLastError: Integer; stdcall;
發生錯誤之后調用這個函數,就會返回所發生的特定錯誤的完整代碼。
針對TCP/IP的WinSock編程
因為TCP協議是一個面向連接的協議,它存在一個概念上的“服務器”端和“客戶端”,在編碼時,要區分對待。
1、服務器端的編程
“服務器”在某種概念上我們可以理解為一個進程,它需要等待任意數量的客戶機連接,以便為它們的請求提供服務。對服務器監聽的連接來說,它必須在一個已知的名字上。在TCP/IP中,這個名字就是本地接口的I P地址,加上一個端口編號。每種協議都有一套不同的定址方案,所以有一種不同的命名方法。在Winsock中,第一步是將指定協議的套接字綁定到它已知的名字上。這個過程是通過API調用bind來完成的。下一步是將套接字置為監聽模式。這時,用API函數listen來完成的。最后,若一個客戶機試圖建立連接,服務器必須通過accept或WSAAccept調用來接受連接。
1.socket
function socket(af, Struct, protocol: Integer): TSocket; stdcall;
在加載Winsock DLL的相應版本之后,你要做的第一件事就是建立一個套接字了。在1.1版本中通過使用socket這個API來實現。第一個參數是你要使用的協議家族,第二個參數為套接字類型,最后一個參數指名你要使用的具體協議。下面的代碼創建了一個使用IP協議家族中的TCP協議創建的流模式的套接字。
skc := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
2. bind
一旦為某種特定協議創建了套接字,就必須將套接字綁定到一個已知地址。bind函數可將指定的套接字同一個已知地址綁定到一起。該函數聲明如下;
function bind(s: TSocket; var addr: TSockAddr; namelen: Integer): Integer; stdcall;
其中第一個參數s代表我們希望在上面等待客戶連接的那個套接字第二個參數addr,針對自己打算使用的那個協議,必須把該參數填充一個地址緩沖區,第三個參數是要傳遞的、由協議決定的地址的長度。例如這樣一段代碼
var
ErrorCode : integer;
SockAdd_In : TSockAddrIn;
...
begin
...
SockAdd_In.sin_family := PF_INET;
SockAdd_In.sin_port := htons(FPort);
SockAdd_In.sin_addr.S_addr := htonl(INADDR_ANY);
ErrorCode := bind(FSock,SockAdd_In,sizeof(SockAdd_In));
一旦出錯, bind就會返回SOCKET_ERROR。對bind 來說,最常見的錯誤是WSAEADDRINUSE。如使用的是TCP/IP,那么WSAEADDRINUSE就表示另一個進程已經同本地IP接口和端口號綁定到了一起,或者那個IP接口和端口號處於TIME_WAIT狀態。假如你針對一個套接字調用bind,但那個套接字已經綁定,便會返回WSAEFFAULT錯誤。
3. listen
我們接下來要做的是將套接字置入監聽模式。bind函數的作用只是將一個套接字和一個指定的地址關聯在一起。指示一個套接字等候進入連接的API函數則是listen,其定義如下:
function listen(s: TSocket; backlog: Integer): Integer; stdcall;
第一個參數同樣是限定套接字。backlog參數指定了正在等待連接的最大隊列長度。這個參數非常重要,因為完全可能同時出現幾個服務器連接請求。例如,假定backlog參數為2。如果三個客戶機同時發出請求,那么頭兩個會被放在一個“待決”(等待處理)隊列中,以便應用程序依次為它們提供服務。而第三個連接會造成一個WSAECONNREFUSED錯誤。注意,一旦服務器接受了一個連接,那個連接請求就會從隊列中刪去,以便別人可繼續發出請求。backlog參數其實本身就存在着限制,這個限制是由基層的協議提供者決定的。如果出現非法值,那么會用與之最接近的一個合法值來取代。除此以外,對於如何知道實際的backlog值,其實並不存在一種標准手段。與listen對應的錯誤是非常直觀的。到目前為止,最常見的錯誤是WSAEINVAL。該錯誤通常意味着,你忘記在listen之前調用bind。否則,與bind調用相反,使用listen時可能收到WSAEADDRINUSE。這個錯誤通常是在進行bind調用時發生的。
4. accept
現在,我們已做好了接受客戶連接的准備。這是通過accept或WSAAccept函數來完成的。
accept格式如下:
function accept(s: TSocket; addr: PSockAddr; addrlen: PInteger): TSocket; stdcall;
其中,參數s是一個限定套接字,它處在監聽模式。第二個參數應該是一個有效的SOCKADDR_IN結構的地址,而addrlen應該是SOCKADDR_IN結構的長度。對於屬於另一種協議的套接字,應當用與那種協議對應的SOCKADDR結構來替換SOCKADDR_IN。通過對accpet函數的調用,可為待決連接隊列中的第一個連接請求提供服務。accept函數返回后,addr結構中會包含發出連接請求的那個客戶機的I P地址信息,而addrlen參數則指出結構的長度。此外,accept會返回一個新的套接字描述符,它對應於已經接受的那個客戶機連接。對於該客戶機后續的所有操作,都應使用這個新套接字。至於原來那個監聽套接字,它仍然用於接受其他客戶機連接,而且仍處於監聽模式。
2、客戶機API函數
客戶機要簡單得多,建立成功連接所需的步驟也要少得多。客戶機只需三步操作:
1) 用socket創建一個套接字。
2) 解析服務器名(以基層協議為准)。
3) 用connect初始化一個連接。
connect函數
關於創建套接字和解析服務器名的方法,前面已有簡單敘述,這里介紹最后一步連接的API函數。我們先來看看該函數的Winsock 1版本,其定義如下:
function connect(s: TSocket; var name: TSockAddr; namelen: Integer): Integer; stdcall;
該函數的參數是相當清楚的: s是即將在其上面建立連接的那個有效TCP套接字; name是針對TCP(說明連接的服務器)的套接字地址結構(SOCKADDR_IN);namelen則是名字參數的長度。
3、數據傳輸
收發數據是網絡編程的主題。要在已建立連接的套接字上接收數據,在Winsock 1版本中,可用這個A P I函數:
int send (
SOCKET s,
const char FAR * buf,
int len,
int flags
);
delphi中聲明如下:
function send(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
SOCKET參數是已建立連接的套接字,將在這個套接字上發送數據。第二個參數buf,則是字符緩沖區,區內包含即將發送的數據。第三個參數len,指定即將發送的緩沖區內的字符數。最后,flags可為0、MSG_DONTROUTE或MSG_OOB。另外, flags還可以是對那些標志進行按位“或運算”的一個結果。MSG_DONTROUTE標志要求傳送層不要將它發出的包路由出去。由基層的傳送決定是否實現這一請求(例如,若傳送協議不支持該選項,這一請求就會被忽略)。MSG_OOB標志預示數據應該被帶外發送。對返回數據而言,send返回發送的字節數;若發生錯誤,就返回SOCKET_ERROR。常見的錯誤是WSAECONNABORTED,這一錯誤一般發生在虛擬回路由於超時或協議有錯而中斷的時候。發生這種情況時,應該關閉這個套接字,因為它不能再用了。遠程主機上的應用通過執行強行關閉或意外中斷操作重新設置虛擬虛路時,或遠程主機重新啟動時,發生的則是WSAECONNRESET錯誤。再次提醒大家注意,發生這一錯誤時,應該關閉這個套接字。最后一個常見錯誤是WSAETIMEOUT,它發生在連接由於網絡故障或遠程連接系統異常死機而引起的連接中斷時。
同樣地,在已建立了連接的套接字上接收數據也有個函數:
int recv (
SOCKET s,
char FAR* buf,
int len,
int flags
);
delphi中聲明如下:
function recv(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
從API的原型中,我們可以看到,所有關系到收發數據的緩沖都屬於簡單的char類型。也就是說,這些函數沒有“Unicode”版本。所有收發函數返回的錯誤代碼都是SOCKET_ERROR。一旦返回錯誤,系統就會調用WSAGetLastError獲得詳細的錯誤信息。最常見的錯誤是WSAECONNABORED和WSAECONNRESET。兩者均涉及到即將關閉連接這一問題—要么通過超時,要么通過通信方關閉連接。另一個常見錯誤是WSAEWOULDBLOCK,一般出現在套接字處於非暫停模式或異步狀態時。這個錯誤主要意味着指定函數暫不能完成。
4、流協議
由於大多面向連接的協議同時也是流式傳輸協議,所以,在此提一下流式協議。對於流套接字上收發數據所用的函數,需要明白的是:它們不能保證對請求的數據量進行讀取或寫入。比如說,一個2048字節的字符緩沖,准備用send函數來發送它。對send函數而言,可能會返回已發出的少於2048的字節。是因為對每個收發數據的套接字來說,系統都為它們分配了相當充足的緩沖區空間。在發送數據時,內部緩沖區會將數據一直保留到應該將它發到線上為止。幾種常見的情況都可導致這一情形的發生。比方說,大量數據的傳輸可以令緩沖區快速填滿。同時,對TCP/IP來說,還有一個窗口大小的問題。接收端會對窗口大小進行調節,以指出它可以接收多少數據。如果有大量數據涌入接收端,接收端就會將窗口大小設為0,為待發數據做好准備。對發送端來說,這樣會強令它在收到一個新的大於0的窗口大小之前,不得再發數據。在使用send調用時,緩沖區可能只能容納1024個字節,這時,便有必要再提取剩下的1024個字節。
5、中斷連接
一旦完成任務,就必須關掉連接,釋放關聯到那個套接字句柄的所有資源。要真正地釋放與一個開着的套接字句柄關聯的資源,執行closesocket調用即可。但要明白這一點,closesocket可能會帶來負面影響(和如何調用它有關),即可能會導致數據的丟失。鑒於此,應該在調用closesocket函數之前,利用shutdown函數從容中斷連接。接下來,我們來談談這兩個A P I函數。
1. shutdown
為了保證通信方能夠收到應用發出的所有數據,對一個編得好的應用來說,應該通知接收端“不再發送數據”。同樣,通信方也應該如此。這就是所謂的“從容關閉”方法,並由shutdown函數來執行。shutdown的定義如下:
int shutdown (
SOCKET s,
int how
);
how參數可以是下面的任何一個值: SD_RECEIVE、SD_SEND或SD_BOTH。如果是SD_RECEIVE,就表示不允許再調用接收函數。這對底部的協議層沒有影響。另外,對TCP套接字來說,不管數據在等候接收,還是數據接連到達,都要重設連接。盡管如此, UDP套接字上,仍然接受並排列接入的數據。如果選擇SE_SEND,表示不允許再調用發送函數。對TCP套接字來說,這樣會在所有數據發出,並得到接收端確認之后,生成一個FIN包。最后,如果指定SD_BOTH,則表示取消連接兩端的收發操作。
2. closesocket
closesocket函數用於關閉套接字,它的定義如下:
int closesocket (
SOCKET s
);
如果沒有對該套接字的其他引用,所有與其描述符關聯的資源都會被釋放。其中包括丟棄所有等侯處理的數據。對這個進程中任何一個線程來說,它們執行的待決異步調用都在未投遞任何通知消息的情況下被刪除。待決的重疊操作也被刪除。與該重疊操作關聯的任何事件,完成例程或完成端口能執行,但最后會失敗,出現WSA_OPERATION_ABORTED錯誤。還有一點會對closesocket的行為產生影響:套接字選項SO_LINGER是否已經設置。LINGER是“拖延”的意思。SO_LINGER用於控制在未發送的數據排隊等候於套接字上的時候,一旦執行了closesocket命令,那么該采取什么樣的行動。
應用WinSock建立客戶機/服務器程序的活動圖
一個典型的客戶機/服務器模式的會話程序的順序圖
I/O控制指令
一系列套接字I/O控制函數用於在套接字之上,控制I/O的行為,同時獲取與那個套接字上進行的I/O操作有關的信息。其中,第一個函數是ioctlsocket,起源於Winsock 1規范,聲明如下:
int ioctlsocket (
SOCKET s,
long cmd,
u_long FAR* argp
);
其中,參數s指定的是要在上面采取I/O操作的套接字描述符,而cmd是一個預定義的標志,用於打算執行的I/O控制命令。最后一個參數argp對應的是一個指針,指向與命令密切相關的一個變量。描述好每個命令之后,再給出要求變量的類型。
標准I/O控制命令
1. FIONBIO
該命令可在套接字s上允許或禁止“非鎖定”(Nonblocking)模式。默認情況下,所有套接字在創建好后,都會自動進入“鎖定”套接字。若隨FIONBIO這個I/O控制命令來調用ioctlsocket,那么應設置argp,令其傳遞指向一個“無符號”(無正負號)長整數的指針;若打算啟用非鎖定模式,應將那個長整數的值設為一個非零值。而若設為0值,意味着套接字進入鎖定模式。
調用WSAAsyncSelect或WSAEventSelect函數的時候,會將套接字自動設為非鎖定模式。調用了其中任何一個函數之后,再有任何將套接字設回鎖定模式的企圖,都會以失敗告終,並返回WSAEINVAL 錯誤。要想將套接字改回鎖定模式,應用程序首先必須禁止WSAAsyncSelect。具體的做法是調用WSAAsyncSelect,同時令其lEvent參數等於0。或者調用WSAEventSelect,令lNetworkEvents參數等於0,從而禁止WSAEventSelect
2. FIONREAD
該命令用於決定可從套接字上自動讀入的數據量。對ioctlsocket 來說,argp值會返回一個無符號的整數,其中包含了打算讀入的字節數。若套接字s是一個“面向數據流”的套接字(類型為SOCK_STREAM),那么FIONREAD會返回在單獨一次接收調用中,可返回的數據總量。要注意的是,若使用這種或其他形式的消息預先“窺視”機制,並一定保證能夠返回正確的數據量。若在一個數據報套接字(類型為SOCK_DGRAM)上使用I/O控制命令,返回值就是在套接字上排隊的第一條消息的大小。
3. SIOCAT M A R K
若一個套接字配置成接收帶外(OOB)數據,而且已設置成以內嵌方式讀取這種OOB數據(通過設置SO_OOBINLINE套接字選項),那么本I/O控制命令就會返回一個布爾值,指出接下來是否准備接收OOB數據。如答案是肯定的,則返回TRUE;否則,便返回FALSE,而且下一次接收操作會返回OOB數據之前的所有或部分數據。對ioctlsocket來說,argp會返回一個指向布爾變量的指針。
套接字模式
Windows套接字在兩種模式下執行I/O操作:鎖定和非鎖定(阻塞和非阻塞)。
在鎖定模式下,在I/O操作完成前,執行操作的Winsock函數(比如send和recv)會一直等候下去,不會立即返回程序(將控制權交還給程序)。而在非鎖定模式下, Winsock函數無論如何都會立即返回。
對於處在鎖定模式的套接字,我們必須多加留意,因為在一個鎖定套接字上調用任何一個Winsock API函數,都會產生相同的后果—耗費或長或短的時間“等待”。大多數Winsock應用都是遵照一種“生產者-消費者”模型來編制的。在這種模型中,應用程序需要讀取(或寫入)指定數量的字節,然后以它為基礎執行一些計算。這種方式下的使用,一定要注意到阻塞作用產生的副作用,例如,我們編寫了了一個“服務器端”的進程,創建一個套接字,然后在主線程中用一個循環接受客戶端發起的連接請求,我們用到了ACCEPT函數,那么在阻塞模式下,當沒有客戶端請求發送時,調用accept函數的線程(這里是主線程)將一直阻塞下去,不會返回,這也就意味着你其他的並發操作無法執行,例如你的程序帶有GUI界面,那么你將無法操作窗口上的其他按鈕。
為了解決上述問題,我們注意到阻塞的作用是針對調用它的線程,也就是說,如果我們在主線程中創建一個輔助線程來進行輪循操作,那么雖然此輔助線程可能被阻塞,但不會影響到主線程的工作。
對鎖定套接字來說,它的一個缺點在於:應用程序很難同時通過多個建好連接的套接字通信。使用前述的辦法,我們可對應用程序進行修改,令其為連好的每個套接字都分配一個讀線程,以及一個數據處理線程。盡管這仍然會增大一些開銷,但的確是一種可行的方案。唯一的缺點便是擴展性極差,以后想同時處理大量套接字時,恐怕難以下手。
非鎖定模式
除了鎖定模式,我們還可考慮采用非鎖定模式的套接字。盡管這種套接字在使用上存在着些許難度,但只要排除了這項困難,它在功能上還是非常強大的。除具備鎖定套接字已有的各項優點之外,還進行了少許擴充,功能更強。將一個套接字置為非鎖定模式之后, Winsock API調用會立即返回。大多數情況下,這些調用都會“失敗”,並返回一個WSAEWOULDBLOCK錯誤。什么意思呢?它意味着請求的操作在調用期間沒有時間完成。舉個例子來說,假如在系統的輸入緩沖區中,尚不存在“待決”的數據,那么recv(接收數據)調用就會返回WSAEWOULDBLOCK錯誤。通常,我們需要重復調用同一個函數,直至獲得一個成功返回代碼。
由於非鎖定調用會頻繁返回WSAEWOULDBLOCK錯誤,所以在任何時候,都應仔細檢查所有返回代碼,並作好“失敗”的准備。許多程序員易犯的一個錯誤便是連續不停地調用一個函數,直到它返回成功的消息為止。
鎖定和非鎖定套接字模式都存在着優點和缺點。其中,從概念的角度說,鎖定套接字更易使用。但在應付建立連接的多個套接字時,或在數據的收發量不均,時間不定時,卻顯得極難管理。而另一方面,假如需要編寫更多的代碼,以便在每個Winsock調用中,對收到一個WSAEWOULDBLOCK錯誤的可能性加以應付,那么非鎖定套接字便顯得有些難於操作。在這些情況下,可考慮使用“套接字I / O模型”,它有助於應用程序通過一種異步方式,同時對一個或多個套接字上進行的通信加以管理。
一個例子:
為了闡述鎖定模式和非鎖定模式的區別,可以用下面這個例子來演示:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,winsock, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
ckbxB: TCheckBox;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
...{ Private declarations }
public
...{ Public declarations }
end;
TSockReadThread= class(TThread)
private
FSocket : TSocket;
FBuf : array[0..255] of Char;
FMemo : TMemo;
procedure GetResult;
protected
procedure Execute;override;
public
constructor Create(pSocket : TSocket;mm : TMemo);
end;
var
Form1: TForm1;
WSAData : TWSAData;
implementation
...{$R *.dfm}
procedure StartUp;
var
ErrorCode : integer;
begin
//加載winSock dll
ErrorCode := WSAStartup($0101, WSAData);
if ErrorCode <> 0 then
begin
ShowMessage('加載失敗');
exit;
end;
end;
//創建一個服務器
procedure TForm1.Button1Click(Sender: TObject);
var
ErrorCode,AddSize : integer;
SockAdd_In,Add: TSockAddrIn;
tm : Longint;
WSAData : TWSAData;
FSock,AcceptSock : TSocket;
begin
//創建一個使用TCP協議的套接字
FSock := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if FSock = SOCKET_ERROR then
begin
showmessage(Format('%s;ErrorCode:%d',['套接字創建失敗',WSAGetLastError]) );
Exit;
end;
//根據一個TCheckBox控件的選擇情況來決定使用鎖定模式還是非鎖定模式
if ckbxB.Checked then
tm := 1 //非鎖定模式
else tm := 0; //鎖定模式
ioctlsocket(FSock,FIONBIO,tm);
SockAdd_In.sin_family := PF_INET;
SockAdd_In.sin_port := htons(5151);
SockAdd_In.sin_addr.S_addr := htonl(INADDR_ANY);
//綁定
ErrorCode := bind(FSock,SockAdd_In,sizeof(SockAdd_In));
if ErrorCode = SOCKET_ERROR then
begin
showmessage(Format('%s;ErrorCode:%d',['綁定失敗:',WSAGetLastError]) );
Exit;
end;
//置為監聽模式
listen(FSock,5);
//用一個循環來反復判斷是否有客戶端請求,如果存在請求就創建一個用來接受數據的讀取線程
while true do
begin
AddSize := sizeof(Add);
AcceptSock := accept(FSock,@Add,@AddSize);
if AcceptSock <> INVALID_SOCKET then
TSockReadThread.Create(AcceptSock,Memo1);
Application.ProcessMessages;
end;
end;
...{ TSockReadThread }
constructor TSockReadThread.Create(pSocket: TSocket; mm: TMemo);
begin
FMemo := mm;
FSocket := pSocket;
inherited Create(false);
end;
procedure TSockReadThread.Execute;
var
ret : integer;
FdSet : TFDSet;
TimeVal : TTimeVal;
begin
inherited;
FreeOnTerminate := True;
while not terminated do
begin
...{ FD_ZERO(FdSet);
FD_SET(FSocket,FdSet);
TimeVal.tv_sec := 0;
TimeVal.tv_usec := 500;
if (select(0,@fdSet,nil,nil,@TimeVal) > 0) and
not terminated then
begin }
ret := recv(FSocket,fbuf,256,0);
if ret > 0 then Synchronize(GetResult)
else Break;
// end;
end;
end;
procedure TSockReadThread.GetResult;
begin
FMemo.Lines.Add(FBuf);
end;
//創建客戶端,並發送數據
procedure TForm1.Button2Click(Sender: TObject);
var
ErrorCode : integer;
buf : array[0..10] of Char;
SockAdd_Inc : TSockAddrIn;
SkC : TSocket;
begin
skc := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if skc = SOCKET_ERROR then
begin
showmessage('創建失敗');
Exit;
end;
SockAdd_Inc.sin_family := PF_INET;
SockAdd_Inc.sin_port := htons(5151);
SockAdd_Inc.sin_addr.S_addr := inet_addr(pchar('127.0.0.1'));
//連接
connect(skc,SockAdd_Inc,sizeof(SockAdd_Inc));
//發送數據
buf :='wudi_1982';
send(SkC,buf,10*sizeof(char),0);
//斷開連接
shutdown(skc,SD_SEND);
closesocket(skc);
end;
initialization
StartUp;
finalization
WSACleanup;
end.
在上面的例子中,首先通過點擊一個按鈕創建一個服務器,如果選擇的是阻塞模式,你可以發現程序就想“死”了一樣,這是阻塞作用產生的效果,因為上面例子調用accept函數的地方是在主線程中,而此時沒有客戶端發起連接,因此accept將無法返回,主線程被阻塞。這種情況下,你根本無法點擊那個用來創建客戶端並發送數據的按鈕。然后再此執行程序,使用非阻塞模式,你會看到程序執行成功,創建客戶端按鈕可以執行。如果有興趣,最好在兩種模式下使用單步執行,來看以下效果,主要是關產accept函數執行的情況。當然,你還可以把用來接受客戶端請求的那段代碼封裝到一個線程中去做,例如上面例子的讀取線程。
如果你仔細關產上面代碼,你可以看到在讀取線程中,有一段被我注釋掉的程序,它的主體是select,它是干什么的呢?下面我們就進入Winsock i/o模式。
套接字I/O模型
共有五種類型的套接字I/O模型,可讓Winsock應用程序對I/O進行管理,它們包括: select(選擇)、WSAAsyncSelect(異步選擇)、WSAEventSelect(事件選擇)、overlapped(重疊)以及completion port(完成端口)。因為本文的出發點是DELPHI中的TServerSocket控件,基於此控件的實現,在這里,我打算向大家解釋主要解釋select以及WSAAsyncSelectI/O模型。
select模型
select(選擇)模型是Winsock中最常見的I/O模型。之所以稱其為“ select模型”,是由於它的“中心思想”便是利用select函數,實現對I/O的管理!最初設計該模型時,主要面向的是某些使用Unix操作系統的計算機,它們采用的是Berkeley套接字方案。select模型已集成到Winsock 1.1中,它使那些想避免在套接字調用過程中被無辜“鎖定”的應用程序,采取一種有序的方式,同時進行對多個套接字的管理。由於Winsock 1.1向后兼容於Berkeley套接字實施方案,所以假如有一個Berkeley套接字應用使用了select函數,那么從理論角度講,毋需對其進行任何修改,便可正常運行。
利用select函數,我們判斷套接字上是否存在數據,或者能否向一個套接字寫入數據。之所以要設計這個函數,唯一的目的便是防止應用程序在套接字處於鎖定模式中時,在一次I / O綁定調用(如send或recv)過程中,被迫進入“鎖定”狀態;同時防止在套接字處於非鎖定模式中時,產生WSAEWOULDBLOCK錯誤。除非滿足事先用參數規定的條件,否則select函數會在進行I/O操作時鎖定。select的函數原型如下:
int select (
int nfds,
fd_set FAR * readfds,
fd_set FAR * writefds,
fd_set FAR * exceptfds,
const struct timeval FAR * timeout
);
其中,第一個參數nfds會被忽略。之所以仍然要提供這個參數,只是為了保持與早期的Berkeley套接字應用程序的兼容。大家可注意到三個fd_set參數:一個用於檢查可讀性(readfds),一個用於檢查可寫性(writefds),另一個用於例外數據(exceptfds)。從根本上說,fd_set數據類型代表着一系列特定套接字的集合。其中, readfds集合包括符合下述任何一個條件的套接字:
■ 有數據可以讀入。
■ 連接已經關閉、重設或中止。
■ 假如已調用了listen,而且一個連接正在建立,那么accept函數調用會成功。
writefds集合包括符合下述任何一個條件的套接字:
■ 有數據可以發出。
■ 如果已完成了對一個非鎖定連接調用的處理,連接就會成功。
最后,exceptfds集合包括符合下述任何一個條件的套接字:
■ 假如已完成了對一個非鎖定連接調用的處理,連接嘗試就會失敗。
■ 有帶外(OOB)數據可供讀取。
例如,假定我們想測試一個套接字是否“可讀”,必須將自己的套接字增添到readfds集合,再等待select函數完成。select完成之后,必須判斷自己的套接字是否仍為readfds集合的一部分。若答案是肯定的,便表明該套接字“可讀”,可立即着手從它上面讀取數據。在三個參數中(readfds、writefds和exceptfds),任何兩個都可以是空值( NULL);但是,至少有一個不能為空值!在任何不為空的集合中,必須包含至少一個套接字句柄;否則, select函數便沒有任何東西可以等待。最后一個參數timeout對應的是一個指針,它指向一個timeval結構,用於
決定select最多等待I/O操作完成多久的時間。如timeout是一個空指針,那么select調用會無限期地“鎖定”或停頓下去,直到至少有一個描述符符合指定的條件后結束。對timeval結構的
定義如下:
timeval = record
tv_sec: Longint;
tv_usec: Longint;
end;
其中,tv_sec字段以秒為單位指定等待時間;tv_usec字段則以毫秒為單位指定等待時間。若將超時值設置為( 0 , 0),表明select會立即返回,允許應用程序對select操作進行“輪詢”。出於對性能方面的考慮,應避免這樣的設置。select成功完成后,會在fd_set結構中,返回剛好有未完成的I / O操作的所有套接字句柄的總量。若超過timeval設定的時間,便會返回0。不管由於什么原因,假如select調用失敗,都會返回SOCKET_ERROR。用select對套接字進行監視之前,在自己的應用程序中,必須將套接字句柄分配給一個集合,設置好一個或全部讀、寫以及例外fd_set結構。將一個套接字分配給任何一個集合后,再來調用select,便可知道一個套接字上是否正在發生上述的I / O活動。
例如上面的例程,在阻塞模式下,我們將while 循環調用accept的那段代碼做如下修改,執行一下,你會發現在阻塞模式下,剛才無法完成的動作現在可以了。
var
FdSet : TFDSet;
TimeVal : TTimeVal;
...
begin
//前面的代碼不便
while true do
begin
FD_ZERO(FdSet);
FD_SET(FSock,FdSet);
TimeVal.tv_sec := 0;
TimeVal.tv_usec := 500;
//使用select函數
if (select(0,@fdSet,nil,nil,@TimeVal) > 0) then
begin
AddSize := sizeof(Add);
AcceptSock := accept(FSock,@Add,@AddSize);
if AcceptSock <> INVALID_SOCKET then
TSockReadThread.Create(AcceptSock,Memo1);
end;
Application.ProcessMessages; end;
end;
WSAAsyncSelect
Winsock提供了一個有用的異步I/O模型。利用這個模型,應用程序可在一個套接字上,接收以Windows消息為基礎的網絡事件通知。具體的做法是在建好一個套接字后,調用WSAAsyncSelect函數。該模型最早出現於Winsock的1.1版本中,用於幫助應用程序開發者面向一些早期的16位Windows平台,適應其“落后”的多任務消息環境。應用程序仍可從這種模型中得到好處,特別是它們用一個標准的Windows例程(常稱為“ winproc”),對窗口消息進行管理的時候。
消息通知
要想使用WSAAsyncSelect模型,程序必須具備一個窗口,然后有消息循環系統,我們通常會自定義一個消息,然后調用WSAAsyncSelect函數將此消息投遞到制定的窗口句柄中。
WSAAsyncSelect函數定義如下:
int WSAAsyncSelect (
SOCKET s,
HWND hWnd,
unsigned int wMsg,
long lEvent
);
其中, s參數指定的是我們感興趣的那個套接字。hWnd參數指定的是一個窗口句柄,它對應於網絡事件發生之后,想要收到通知消息的那個窗口或對話框。wMsg參數指定在發生網絡事件時,打算接收的消息。該消息會投遞到由hWnd窗口句柄指定的那個窗口。最后一個參數是lEvent,它指定的是一個位掩碼,對應於一系列網絡事件的組合,應用程序感興趣的便是這一系列事件。大多數應用程序通常感興趣的網絡事件類型包括: FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT和FD_CLOSE。當然,到底使用FD_ACCEPT,還是使用FD_CONNECT類型,要取決於應用程序的身份到底是一個客戶機呢,還是一個服務器。如應用程序同時對多個網絡事件有興趣,只需對各種類型執行一次簡單的按位O R(或)運算,然后將它們分配給lEvent就可以了。
特別要注意的是,多個事件務必在套接字上一次注冊!另外還要注意的是,一旦在某個套接字上允許了事件通知,那么以后除非明確調用closesocket命令,或者由應用程序針對那個套接字調用了WSAAsyncSelect,從而更改了注冊的網絡事件類型,否則的話,事件通知會永遠有效!若將lEvent參數設為0,效果相當於停止在套接字上進行的所有網絡事件通知。
若應用程序針對一個套接字調用了WSAAsyncSelect ,那么套接字的模式會從“鎖定”自動變成“非鎖定”,我們在前面已提到過這一點。
事件類型 含義
FD_READ 應用程序想要接收有關是否可讀的通知,以便讀入數據
FD_WRITE 應用程序想要接收有關是否可寫的通知,以便寫入數據
FD_OOB 應用程序想接收是否有帶外( O O B)數據抵達的通知
FD_ACCEPT 應用程序想接收與進入連接有關的通知
FD_CONNECT 應用程序想接收與一次連接或者多點join操作完成的通知
FD_CLOSE 應用程序想接收與套接字關閉有關的通知
FD_QOS 應用程序想接收套接字“服務質量”(Q o S)發生更改的通知
FD_GROUP_QOS 應用程序想接收套接字組“服務質量”發生更改的通知(現在沒什么用處,為未來套接字組的使用保留)
FD_ROUTING_INTERFACE_CHANGE 應用程序想接收在指定的方向上,與路由接口發生變化的通知
FD_ADDRESS_LIST_CHANGE應用程序想接收針對套接字的協議家族,本地地址列表發生變化的通知
應用程序在一個套接字上成功調用了WSAAsyncSelect 之后,應用程序會在與hWnd窗口句柄參數對應的窗口例程中,以Windows消息的形式,接收網絡事件通知。
就我們的情況來說,感興趣的是WSAAsyncSelect 調用中定義的消息。wParam參數指定在其上面發生了一個網絡事件的套接字。假若同時為這個窗口例程分配了多個套接字,這個參數的重要性便顯示出來了。在lParam參數中,包含了兩方面重要的信息。其中, lParam的低字(低位字)指定了已經發生的網絡事件,而lParam的高字(高位字)包含了可能出現的任何錯誤代碼。
網絡事件消息抵達一個窗口例程后,應用程序首先應檢查lParam的高字位,以判斷是否在套接字上發生了一個網絡錯誤。若應用程序發現套接字上沒有產生任何錯誤,接着便應調查到底是哪個網絡事件類型,造成了這條Windows消息的觸發—具體的做法便是讀取lParam之低字位的內容。此時可使用另一個特殊的宏:WSAGetSelectEvent(在DELPHI中,它以一個函數的形式存在),用它返回lParam的低字部分。
一個例子:
Unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,winsock;
const
//自定義一個消息
WM_MySockMessage=wm_user + $0101;
type
TForm1 = class(TForm)
Button1: TButton;
ListBox1: TListBox;
mmSRec: TMemo;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
//消息處理過程
procedure SockMessage(var msg : TMessage);message WM_MySockMessage;
public
...{ Public declarations }
end;
var
Form1: TForm1;
WSAData : TWSAData;
implementation
...{$R *.dfm}
procedure StartUp;
var
ErrorCode : integer;
begin
ErrorCode := WSAStartup($0101, WSAData);
if ErrorCode <> 0 then
begin
ShowMessage('加載winsock dll失敗');
exit;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ErrorCode : integer;
SockAdd_In : TSockAddrIn;
FSock : TSocket;
begin
//建立套接字
FSock := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if FSock = SOCKET_ERROR then
begin
showmessage(Format('%s;ErrorCode:%d',['套接字建立失敗',WSAGetLastError]) );
Exit;
end;
SockAdd_In.sin_family := PF_INET;
SockAdd_In.sin_port := htons(5150);
SockAdd_In.sin_addr.S_addr := htonl(INADDR_ANY);
ErrorCode := bind(FSock,SockAdd_In,sizeof(SockAdd_In));
if ErrorCode = SOCKET_ERROR then
begin
showmessage(Format('%s;ErrorCode:%d',['綁定套接字失敗',WSAGetLastError]) );
Exit;
end;
//注意這里
WSAAsyncSelect(FSock,Form1.Handle ,WM_MySockMessage,FD_READ or FD_ACCEPT);
//進入監聽狀態
listen(FSock,5);
end;
//對自定義消息的處理過程
procedure TForm1.SockMessage(var msg: TMessage);
var
AddSize,size,rev : integer;
Acceptsk : TSocket;
Sockin,Add : TSockAddrIn;
buf : array[0..255] of char;
begin
case WSAGetSelectEvent(msg.LParam) of
FD_READ : begin//如果有數據可以讀取,調用recv
rev := recv(msg.WParam,buf,256,0);
if rev > 0 then
begin
Form1.mmSRec.Lines.Add(buf);
end;
end;
FD_ACCEPT : begin
//存在連接請求,調用accept
AddSize := sizeof(Add);
Acceptsk := accept(msg.WParam,@Add,@AddSize);
if Acceptsk <> INVALID_SOCKET then
begin
FillChar(SockIn, SizeOf(SockIn), 0);
size := SizeOf(SockIn);
//將請求連接的客戶端地址添加到一個listbox
getpeername(Acceptsk,SockIn,size);
Form1.ListBox1.Items.Add(inet_ntoa(SockIn.sin_addr));
end;
end;
end;
end;
//客戶端代碼
procedure TForm1.Button2Click(Sender: TObject);
var
ErrorCode : integer;
buf : array[0..10] of Char;
SockAdd_Inc : TSockAddrIn;
SkC : TSocket;
begin
skc := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if skc = SOCKET_ERROR then
begin
showmessage('套接字建立失敗');
Exit;
end;
SockAdd_Inc.sin_family := PF_INET;
SockAdd_Inc.sin_port := htons(5150);
SockAdd_Inc.sin_addr.S_addr := inet_addr(pchar('192.168.1.3'));
//連接
connect(skc,SockAdd_Inc,sizeof(SockAdd_Inc));
//發送數據
buf :='wudi_1982';
send(SkC,buf,10*sizeof(char),0);
shutdown(skc,SD_SEND);
closesocket(skc);
end;
initialization
StartUp;
finalization
WSACleanup;
end.
I/O模型的問題
現在,對於如何挑選最適合自己應用程序的I/O模型,大家心中可能還沒什么數。前面已經提到,每種模型都有自己的優點和缺點。同開發一個簡單的鎖定模式應用相比(運行許多服務線程),其他每種I/O模型都需要更為復雜的編程工作。因此,針對客戶機和服務器應用的開發,我們分別提供了下述建議。
1. 客戶機的開發
若打算開發一個客戶機應用,令其同時管理一個或多個套接字,那么建議采用重疊I/O或WSAEventSelect模型(關於這兩種模型,以后會有涉及),以便在一定程度上提升性能。然而,假如開發的是一個以Windows為基礎的應用程序,要進行窗口消息的管理,那么WSAAsyncSelect模型恐怕是一種最好的選擇,因為WSAAsyncSelect本身便是從Windows消息模型借鑒來的。若采用這種模型,我們的程序一開始便具備了處理消息的能力。
2. 服務器的開發
若開發的是一個服務器應用,要在一個給定的時間,同時控制幾個套接字,建議大家采用重疊I/O模型,這同樣是從性能出發點考慮的。但是,如果預計到自己的服務器在任何給定的時間,都會為大量I/O請求提供服務,便應考慮使用I/O完成端口模型,從而獲得更好的性能。
轉自:http://www.cnblogs.com/linyawen/archive/2010/12/16/1908564.html
/*文章太長了,沒什么耐性看下去。應該說TServerSocket以及TClientSocket是兩個組件,其組件源碼中也是使用這個TSockAddrIn結構
也就是說可以使用TServerSocket以及TClientSocket組件開發網絡程序,也可以使用這個TSockAddrIn結構開發網絡程序。
這個筆記純屬個人理解,目的是為了要明確學習方向,因為網絡上的文章多而且零亂*/