全國最多中醫師線上諮詢網站-台灣中醫網
發文 回覆 瀏覽次數:7221
推到 Plurk!
推到 Facebook!

[轉貼]簡單的 Winsock 應用程式設計(全)

 
axsoft
版主


發表:681
回覆:1056
積分:969
註冊:2002-03-13

發送簡訊給我
#1 引用回覆 回覆 發表時間:2002-06-13 11:10:56 IP:61.220.xxx.xxx 未訂閱
********************************************************************** Copyright by 林軍鼐 文稿內容不得轉載於任何商業書刊或做任何商業用途 **********************************************************************    簡單的 Winsock 應用程式設計(1)            林 軍 鼐    相信各位讀者現在對於 Winsock 的定義、系統環境,以及一些 Winsock Stack 及 Winsock 應用程式,都有基本的認識了。接下來筆者希望能分幾期為各位讀者 介紹一下簡單的 Winsock 網路應用程式設計。    我們將以 Winsock 1.1 規格所定義的 46 個應用程式介面(API)為基礎,逐 步來建立一對 TCP socket 主從架構(Client / Server)的程式。在這兩個程式中, Server 將使用 Winsock 提供的「非同步」(asynchronous)函式來建立 socket 連 結、關閉、及資料收送等等;而 Client 則採類似傳統 UNIX 的「阻攔式」 (blocking)。由於我們的重點並不在於 MS Windows SDK 的程式設計,所以我 們將使用最簡便的方式來顯示訊息;有關 MS Windows 程式的技巧,請各位讀者 自行研究相關的書籍及文章。    今天我們先要看一下主從架構 TCP socket 的建立連結(connect)及關閉 (close)。(參見圖 1.)    (圖 1. 主從架構的 TCP socket 連接建立與關閉)    以前筆者曾簡單地介紹過主從架構的概念,現在我們再以生活上更淺顯的例 子來說明一下,讀者稍後也較容易能明白筆者的敘述。我們可以假設 Server 就像 是電信局所提供的一些服務,比如「104 查號台」或「112 障礙台」。    (1)電信局先建立好了一個電話總機,這就像是呼叫 socket() 函式開啟了一 個 socket。 (2)接著電信局將這個總機的號碼定為 104,就如同我們呼叫 bind() 函式, 將 Server 的這個 socket 指定(bind)在某一個 port。當然電信局必須讓用戶知道 這個號碼;而我們的 Client 程式同樣也要知道 Server 所用的 port,待會才有辦法 與之連接。 (3)電信局的 104 查號台底下會有一些自動服務的分機,但是它的數量是有 限的,所以有時你會撥不通這個號碼(忙線)。同樣地,我們在建立一個 TCP 的 Server socket 時,也會呼叫 listen() 函式來監聽等待;listen() 的第二個參數即是 waiting queue 的數目,通常數值是由 1 到 5。(事實上這兩者還是有點不一 樣。) (4)用戶知道了電信局的這個 104 查號服務,他就可以利用某個電話來撥號 連接這個服務了。這就是我們 Client 程式開啟一個相同的 TCP socket,然後呼叫 connect() 函式去連接 Server 指定的那個 port。當然了,和電話一樣,如果 waiting queue 滿了、與 Server 間線路不通、或是 Server 沒提供此項服務時,你的連接就 會失敗。 (5)電信局查號台的總機接受了這通查詢的電話後,它會轉到另一個分機做 服務,而總機本身則再回到等待的狀態。Server 的 listening socket 亦是一樣,當 你呼叫了 accept() 函式之後,Server 端的系統會建立一個新的 socket 來對此連接 做服務,而原先的 socket 則再回到監聽等待的狀態。 (6)當你查詢完畢了,你就可以掛上電話,彼此間也就離線了。Client 和 Server 間的 socket 關閉亦是如此;不過這個關閉離線的動作,可由 Client 端或 Server 端任一方先關閉。有些電話查詢系統不也是如此嗎?    接下來,我們就來看主從架構的 TCP socket 是如何利用這些 Winsock 函式來 達成的;並利用資策會資訊技術處的「WinKing」這個 Winsock Stack 中某項功能 來顯示 sockets 狀態的變化。文章中僅列出程式的片段,完整的程式請看附錄的程 式。    【Server 端建立 socket 並進入監聽等待狀態】    首先我們先看 Server 端如何建立一個 TCP socket,並使其進入監聽等待的狀 態。    在圖 1. 上,我們可以看到最先被呼叫到的是 WSAStartup() 函式。說明如下:    WSAStartup():連結應用程式與 Winsock.DLL 的第一個函式。 格  式: int PASCAL FAR WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData ); 參  數:        wVersionRequested       欲使用的 Windows Sockets API 版本         lpWSAData               指向 WSADATA 資料的指標 傳回值:        成功 - 0         失敗 - WSASYSNOTREADY / WSAVERNOTSUPPORTED / WSAEINVAL 說明: 此函式「必須」是應用程式呼叫到 Windows Sockets DLL 函式中的第一 個,也唯有此函式呼叫成功後,才可以再呼叫其他 Windows  Sockets DLL 的函式。 此函式亦讓使用者可以指定要使用的 Windows Sockets API 版本,及獲取設計者的 一些資訊。    程式中我們要用 Winsock 1.1,所以我們在程式中有一段為:    WSAStartup((WORD)((1<<8)|1),(LPWSADATA) &WSAData) 其中 ((WORD)((1<<8)|1) 表示我們要用的是 Winsock 「1.1」版本,而 WSAData 則是用來儲存由系統傳回的一些有關此一 Winsock Stack 的資料。 再來我們呼叫 socket() 函式來開啟 Server 端的 TCP socket。 socket():建立Socket。 格 式: SOCKET PASCAL FAR socket( int af, int type, int protocol ); 參 數: af 目前只提供 PF_INET(AF_INET) type Socket 的型態 (SOCK_STREAM、SOCK_DGRAM) protocol 通訊協定(如果使用者不指定則設為0) 傳回值: 成功 - Socket 的識別碼 失敗 - INVALID_SOCKET(呼叫 WSAGetLastError() 可得知原因) 說明: 此函式用來建立一 Socket,並為此 Socket 建立其所使用的資源。 Socket 的型態可為 Stream Socket 或 Datagram Socket。 我們要建立的是 TCP socket,所以程式中我們的第二個參數為 SOCK_STREAM,我們並將開啟的這個 socket 號碼記在 listen_sd 這個變數。 listen_sd = socket(PF_INET, SOCK_STREAM, 0) 接下來我們要指定一個位址及 port 給 Server 的這個 socket,這樣 Client 才知 道待會要連接哪一個位址的哪個 port;所以我們呼叫 bind() 函式。 bind():指定 Socket 的 Local 位址 (Address)。 格 式: int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name, int namelen ); 參 數: s Socket的識別碼 name Socket的位址值 namelen name的長度 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此一函式是指定 Local 位址及 Port 給某一未定名之 Socket。使用者若不 在意位址或 Port 的值,那麼他可以設定位址為 INADDR_ANY,及 Port 為 0;那麼 Windows Sockets 會自動將其設定適當之位址及 Port (1024 到 5000之間的值),使用 者可以在此 Socket 真正連接完成後,呼叫 getsockname() 來獲知其被設定的值。 bind() 函式要指定位址及 port,這個位址必須是執行這個程式所在機器的 IP 位址,所以如果讀者在設計程式時可以將位址設定為 INADDR_ANY,這樣 Winsock 系統會自動將機器正確的位址填入。如果您要讓程式只能在某台機器上 執行的話,那麼就將位址設定為該台機器的 IP 位址。由於此端是 Server 端,所 以我們一定要指定一個 port 號碼給這個 socket。 讀者必須注意一點,TCP socket 一旦選定了一個位址及 port 後,就無法再呼 叫另一次 bind 來任意更改它的位址或 port。 在程式中我們將 Server 端的 port 指定為 7016,位址則由系統來設定。 struct sockaddr_in sa; sa.sin_family = PF_INET; sa.sin_port = htons(7016); /* port number */ sa.sin_addr.s_addr = INADDR_ANY; /* address */ bind(listen_sd, (struct sockaddr far *)&sa, sizeof(sa)) 我們在指定 port 號碼時會用到 htons() 這個函式,主要是因為各機器的數值讀 取方式不同(PC 與 UNIX 系統即不相同),所以我們利用這個函式來將 host order 的排列方式轉換成 network order 的排列方式;相同地,我們也可以呼叫 ntohs() 這個相對的函式將其還原。(host order 各機器不同,但 network order 都 相同)(htons 是針對 short 數值,對於 long 數值則用 hotnl 及 ntohl) 指定完位址及 port 之後,我們呼叫 listen() 函式,讓這個 socket 進入監聽狀 態。一個 Server 端的 TCP socket 必須在做完了 listen 的呼叫後,才能接受 Client 端的連接。 listen():設定 Socket 為監聽狀態,準備被連接。 格 式: int PASCAL FAR listen( SOCKET s, int backlog ); 參 數: s Socket 的識別碼 backlog 未真正完成連接前(尚未呼叫 accept 前)彼端的連接要求的最大 個數 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 使用者可利用此函式來設定 Socket 進入監聽狀態,並設定最多可有多少 個在未真正完成連接前的彼端的連接要求。(目前最大值限制為 5, 最小值為1) 程式中我們將 backlog 設為 1 。 listen(listen_sd, 1) 呼叫完 listen 後,此時 Client 端如果來連接的話,Client 端的連接動作 (connect)會成功,不過此時 Server 端必須再呼叫 accept() 函式,才算正式完成 Server 端的連接動作。但是我們什麼時候可以知道 Client 端來連接,而適時地呼 叫 accept 呢?在這裡我們就要利用一個很好用的 WSAAsyncSelect 函式,將 Server 端的這個 socket 轉變成 Asynchronous 模式,讓系統主動來通知我們有 Client 要連接了。(圖1. 中並未將此函式繪出) WSAAsyncSelect():要求某一 Socket 有事件 (event) 發生時通知使用者。 格 式: int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent ); 參 數: s Socket 的編號 hWnd 動作完成後,接受訊息的視窗 handle wMsg 傳回視窗的訊息 lEvent 應用程式有興趣的網路事件 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式是讓使用者用來要求 Windows Sockets DLL 在偵測到某一 Socket 有網路事件時送訊息到使用者指定的視窗;網路事件是由參數 lEvent 設定。呼叫此 函式會主動將該 Socket 設定為 Non-blocking 模式。lEvent 的值可為以下之「OR」 組合:(參見 WINSOCK第1.1版88、89頁) FD_READ、FD_WRITE、FD_OOB、 FD_ACCEPT、FD_CONNECT、FD_CLOSE 使用者若是針對某一Socket再次呼叫 此函式時,會取消對該 Socket 原先之設定。若要取消對該Socket 的所有設定,則 lEvent 的值必須設為 0。 (圖2) WSAAsyncSelect 函式參數與應用程式關係 我們在程式中要求 Winsock 系統知道 Client 要來連接時,送一個 ASYNC_EVENT 的訊息到程式中 hwnd 這個視窗;由於我們想知道的只有 accept 事 件,所以我們只設定 FD_ACCEPT。 WSAAsyncSelect(listen_sd, hwnd, ASYNC_EVENT, FD_ACCEPT) (圖 3)demoserv 在 WinKing 系統上建立 socket 並進入監聽狀態 讀者必須注意一點,WSAAsyncSelect 的設定是針對「某一個 socket」;也就是 說,只有當您設定的這個 socket (listen_sd)的那些事件(FD_ACCEPT)發生時, 您才會收到這個訊息(ASYNC_EVENT)。如果您開啟了很多 sockets,而要讓每 個 socket 都變成 asynchronous 模式的話,那麼就必須對「每一個 socket」都呼叫 WSAAsyncSelect 來一一設定。而如果您想將某一個 socket 的 async 事件通知設定取 消的話,那麼同樣也是用 WSAAsyncSelect 這個函式;且第四個參數 lEvent 一定要 設為 0。 WSAAsyncSelect( s, hWnd, 0, 0 ) -- 取消所有 async 事件設定 在這裡筆者還要告訴各位一點,呼叫 WSAAsyncSelect 的同時也將此一 socket 改變成「非阻攔」(non-blocking)模式。但是此時這個 socket 不能很簡單地用 ioctlsocket() 這個函式就將它再變回「阻攔」(blocking)模式。也就是說 WSAAsyncSelect 和 ioctlsocket 所改變的「非阻攔」模式仍是有些不同的。如果您想 將一個「非同步」(asynchronous)模式的 socket 再變回「阻攔」模式的話,必須 先呼叫 WSAAsyncSelect() 將所有的 async 事件取消,再用 ioctlsocket() 將它變回阻 攔模式。 ioctlsocket():控制 Socket 的模式。 格 式: int PASCAL FAR ioctlsocket( SOCKET s, long cmd, u_long FAR *argP ); 參 數: s Socket 的識別碼 cmd 指令名稱 argP 指向 cmd 參數的指標 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式用來獲取或設定 Socket 的運作參數。其所提供的指令有:(參見 WINSOCK 第 1.1 版 35、36 頁) cmd 的值可為: FIONBIO -- 開關 non-blocking 模式 FIONREAD -- 自 Socket 一次可讀取的資料量(目前 in buffer 的資料量) SIOCATMARK -- OOB 資料是否已被讀取完 由於我們 Server 端的 socket 是用非同步模式,且設定了 FD_ACCEPT 事件,所 以當 Client 端和我們連接時,Winsock Stack 會主動通知我們;我們再先來看看 Client 端要如何和 Server 端建立連接? 【Client 端向 Server 端主動建立連接】 Client 首先也是呼叫 WSAStartup() 函式來與 Winsock Stack 建立關係;然後同樣 呼叫 socket() 來建立一個 TCP socket。(讀者此時一定要用 TCP socket 來連接 Server 端的 TCP socket,而不能用 UDP socket 來連接;因為相同協定的 sockets 才 能相通,TCP 對 TCP,UDP 對 UDP) 和 Server 端的 socket 不同的地方是:Client 端的 socket 可以呼叫 bind() 函式, 由自己來指定 IP 位址及 port 號碼;但是也可以不呼叫 bind(),而由 Winsock Stack 來自動設定 IP 位址及 port 號碼(此一動作在呼叫 connect() 函式時會由 Winsock 系 統來完成)。通常我們是不呼叫 bind(),而由系統設定的,稍後可呼叫 getsockname() 函式來檢查系統幫我們設定了什麼 IP 及 port。一般言,系統會自動 幫我們設定的 port 號碼是在 1024 到 5000 之間;而如果讀者要自己用 bind 設定 port 的話,最好是 5000 以上的號碼。 connect():要求連接某一 TCP Socket 到指定的對方。 格 式: int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen ); 參 數: s Socket 的識別碼 name 此 Socket 想要連接的對方位址 namelen name的長度 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫WSAGetLastError()可得知原因) 說明: 此函式用來向對方要求建立連接。若是指定的對方位址為 0 的話,會傳 回錯誤值。當連接建立完成後,使用者即可利用此一 Socket 來做傳送或接收資料之 用了。 我們的例子中, Client 是要連接的是自己機器上 Server 所監聽的 7016 這個 port,所以我們有以下的程式片段。(假設我們機器的 IP 存在 my_host_ip) struct sockaddr_in sa; /* 變數宣告 */ sa.sin_family = PF_INET; /* 設定所要連接的 Server 端資料 */ sa.sin_port = htons(7016); sa.sin_addr.s_addr = htonl(my_host_ip); connect(mysd, (struct sockaddr far *)&sa, sizeof(sa)) /* 建立連接 */ 【Server 端接受 Client 端的連接】 由於我們 Server 端的 socket 是設定為「非同步模式」,且是針對 FD_ACCEPT 這個事件,所以當 Client 來連接時,我們 Server 端的 hwnd 這個視窗會收到 Winsock Stack 送來的一個 ASYNC_EVENT 的訊息。(參見前面 WSAAsyncSelect 的設定) 這時,我們應該先利用 WSAGETSELECTERROR(lParam) 來檢查是否有錯誤; 並由 WSAGETSELECTEVENT(lParam) 得知是什麼事件發生(因為 WSAAsyncSelect 函式可針對同一個 socket 同時設定很多事件,但是只用一個訊息 來代表)(此處當然是 FD_ACCEPT 事件);然後再呼叫相關的函式來處理此一事 件。所以我們呼叫 accept() 函式來建立 Server 端的連接。 accept():接受某一 Socket 的連接要求,以完成 Stream Socket 的連接。 格 式: SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr, int FAR *addrlen ); 參 數: s Socket的識別碼 addr 存放來連接的彼端的位址 addrlen addr的長度 傳回值:成功 - 新的Socket識別碼 失敗 - INVALID_SOCKET (呼叫 WSAGetLastError() 可得知原因) 說明: Server 端之應用程式呼叫此一函式來接受 Client 端要求之 Socket 連接動 作;如果Server 端之 Socket 是為 Blocking 模式,且沒有人要求連接動作,那麼此一 函式會被 Block 住;如果為 Non-Blocking 模式,此函式會馬上回覆錯誤。accept() 函式的答覆值為一新的 Socket,此新建之 Socket 不可再用來接受其它的連接要求; 但是原先監聽之 Socket 仍可接受其他人的連接要求。 TCP socket 的 Server 端在呼叫 accept() 後,會傳回一個新的 socket 號碼;而這 個新的 socket 號碼才是真正與 Client 端相通的 socket。比如說,我們用 socket() 建 立了一個 TCP socket,而此 socket 的號碼(系統給的)為 1,然後我們呼叫的 bind()、listen()、accept() 都是針對此一 socket;當我們在呼叫 accept() 後,傳回值是 另一個 socket 號碼(也是系統給的),比如說 3;那麼真正與 Client 端連接的是號 碼 3 這個 socket,我們收送資料也都是要利用 socket 3,而不是 socket 1;讀者不可 搞錯。 我們在程式中對 accept() 的呼叫如下;我們並可由第二個參數的傳回值,得知 究竟是哪一個 IP 位址及 port 號碼的 Client 與我們 Server 連接。 struct sockaddr_in sa; int sa_len = sizeof(sa); my_sd = accept(listen_sd, (struct sockaddr far *)&sa, &sa_len) 當 Server 端呼叫完 accept() 後,主從架構的 TCP socket 連接才算真正建立完 畢; Server 及 Client 端也就可以分別利用此一 socket 來送資料到對方或收對方送來 的資料了。(有關資料的收送,我們等下一期再談) (圖 4) demoserv 與 democlnt 在 WinKing 上連接成功後狀態 【Server 及 Client 端結束 socket 連接】 最後我們來看一下如何結束 socket 的連接。socket 的關閉很簡單,而且可由 Server 或 Client 的任一端先啟動,只要呼叫 closesocket() 就可以了。而要關閉監聽 狀態的 socket,同樣也是利用此一函式。 closesocket():關閉某一Socket。 格 式: int PASCAL FAR closesocket( SOCKET s ); 參 數: s Socket 的識別碼 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此一函式是用來關閉某一 Socket。 若是使用者原先對要關閉之 Socket 設定 SO_DONTLINGER,則在呼叫此一函式 後,會馬上回覆,但是此一 Sokcet 尚未傳送完畢的資料會繼續送完後才關閉。 若是使用者原先設定此 Socket 為 SO_LINGER,則有兩種情況: (a) Timeout 設為 0 的話,此一 Socket 馬上重新設定 (reset),未傳完或未收到的 資料全部遺失。 (b) Timeout 不為 0 的話,則會將資料送完,或是等到 Timeout 發生後才真正關 閉。 程式結束前,讀者們可千萬別忘了要呼叫 WSACleanup() 來通知 Winsock Stack;如果您不呼叫此一函式,Winsock Stack 中有些資源可能仍會被您佔用而無 法清除釋放喲。 WSACleanup():結束 Windows Sockets DLL 的使用。 格 式: int PASCAL FAR WSACleanup( void ); 參 數: 無 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 應用程式在使用 Windows Sockets DLL 時必須先呼叫 WSAStartup() 來向 Windows Sockets DLL 註冊;當應用程式不再需要使用 Windows Sockets DLL 時,須呼叫此一函式來註銷使用,以便釋放其占用的資 源。 【結語】 這期筆者先介紹主從架構 TCP sockets 的連接及關閉,以後會再陸續介紹如何 收送資料,以及其他 API 的使用。想要進一步了解如何撰寫 Winsock 程式的讀者, 可以好好研究一下筆者 demoserv 及 democlnt 這兩個程式;也許不是寫的很好,但 是希望可以帶給不懂 Winsock 程式設計的人一個起步。 讀者們亦可自行用 anonymous ftp 方式到 SEEDNET 台北主機 tpts1.seed.net.tw (139.175.1.10)的 UPLOAD / WINKING 目錄下,取得筆者與陳建伶小姐所設計的 WinKing 這個 Winsock Stack 的試用版,來跑 demoserv 與 democlnt 這兩個程式及其 他許許多多的 Winsock 應用程式。(正式版本請洽 SEEDNET 服務中心,新版的 WinKing 已含 Windows 撥接及 PPP 程式,適合電話撥接用戶在 Windows 環境下使 用 SEEDNET;WinKing 同樣也提供 Ethernet 環境的使用。) ********************************************************************** Copyright by 林軍鼐 文稿內容不得轉載於任何商業書刊或做任何商業用途 ********************************************************************** 簡單的 Winsock 應用程式設計(2) 林 軍 鼐 在前一期的文章中,筆者為大家介紹了如何在 Winsock 環境下,建立主從 架構(Client/Server)的 TCP socket 的連接建立與關閉;今天筆者將繼續為大家 介紹如何利用 TCP socket 來收送資料,並詳細解說 WSAAsyncSelect 函式中的 FD_READ 及 FD_WRITE 事件(筆者曾發現有相當多人對這兩個事件甚不了 解)。 相信讀者們已經知道 TCP socket 的連接是在 Client 端呼叫 connect 函式成 功,且 Server 端呼叫 accept 函式後,才算完全建立成功;當連接建立成功後, Client 及 Server 也就可以利用這個連接成功的 socket 來傳送資料到對方,或是 收取對方送過來的資料了。 (圖 1. TCP socket 的資料收送) 在介紹資料的收送前,筆者先介紹一下 TCP socket 與 UDP socket 在傳送資 料時的特性: Stream (TCP) Socket 提供「雙向」、「可靠」、「有次序」、「不重覆」之 資料傳送。 Datagram (UDP) Socket 則提供「雙向」之溝通,但沒有「可靠」、「有次 序」、「不重覆」等之保證; 所以使用者可能會收到無次序、重覆之資料,甚至 資料在傳輸過程中也可能會遺漏。 由於 UDP Socket 在傳送資料時,並不保證資料能完整地送達對方,所以我 們常用的一些應用程式(如 telnet、mail、ftp、news...等)都是採用 TCP Socket,以保證資料的正確性。(TCP 及 UDP 封包的傳送協定不在我們討論範 圍,想要瞭解的讀者們,請自行參考相關書籍) TCP 及 UDP Socket 都是雙向的,所以我們是利用同一個 Socket 來做傳送及 收取資料的動作;一般言 TCP Socket 的資料送、收是呼叫 send() 及 recv() 這兩 個函式來達成,而 UDP Socket 則是用 sendto() 及 recvfrom() 這兩個函式。不過 TCP Socket 也可用 sendto() 及 recvfrom() 函式,UDP Socket 同樣可用 send() 及 recv() 函式;這一點我們稍後再加以解釋。 現在我們先看一下 send() 及 recv() 的函式說明,並回到我們的前一期程 式。 ◎ send():使用連接式(connected)的 Socket 傳送資料。 格 式: int PASCAL FAR send( SOCKET s, const char FAR *buf, int len, int flags ); 參 數: s Socket 的識別碼 buf 存放要傳送的資料的暫存區 len buf 的長度 flags 此函式被呼叫的方式 傳回值: 成功 - 送出的資料長度 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式適用於連接式的 Datagram 或 Stream Socket 來傳送資料。 對 Datagram Socket 言,若是 datagram 的大小超過限制,則將不會送出任何資料,並 會傳回錯誤值。對 Stream Socket 言,Blocking 模式下,若是傳送 (transport) 系統 內之儲存空間(output buffer)不夠存放這些要傳送的資料,send() 將會被 block 住,直到資料送完為止;如果該 Socket 被設定為 Non-Blocking 模式,那麼將視目 前的 output buffer 空間有多少,就送出多少資料,並不會被 block 住。使用者亦須 注意 send()函式執行完成,並不表示資料已經成功地送抵對方了,而是已經放到 系統的 output buffer 中,等待被送出。 flags 的值可設為 0 或 MSG_DONTROUTE 及 MSG_OOB 的組合。(參見 WINSOCK第1.1版48頁) ◎ recv():自 Socket 接收資料。 格 式: int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags ); 參 數: s Socket 的識別碼 buf 存放接收到的資料的暫存區 len buf 的長度 flags 此函式被呼叫的方式 傳回值: 成功 - 接收到的資料長度 (若對方 Socket 已關閉,則為 0) 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式用來自連接式的 Datagram Socket 或 Stream Socket 接收資料。 對 Stream Socket 言,我們可以接收到目前 input buffer 內有效的資料,但其數量 不超過 len 的大小。若是此 Socket 設定 SO_OOBINLINE,且有 out-of-band 的資 料未被讀取,那麼只有 out-of-band 的資料被取出。對 Datagram Socket 言,只取 出第一個 datagram;若是該 datagram 大 於使用者提供的儲存空間,那麼只有該空 間大小的資料被取出,多餘的資料將遺失,且回覆錯誤的訊息。另外如果 Socket 為 Blocking 模式,且目前 input buffer 內沒有任何資料,則 recv() 將 block 到有任 何資料到達為止;如果為 Non-Blocking 模式,且 input buffer 無任何資料,則會馬 上回覆錯誤。參數 flags 的值可為 0 或 MSG_PEEK、MSG_OOB 的組合; MSG_PEEK 代表將資料拷貝到使用者提供的 buffer,但是資料並不從系統的 input buffer 中移走;0 則表示拷貝並移走。(參考 WINSOCK 第1.1版41 頁) 【Server 端的資料收送及關閉 Socket】 在前一期中,我們說建立的是一個 Asynchronous 模式的 Server;程式中, 我們曾對 listen_sd 這個 Socket 呼叫 WSAAsyncSelect() 函式,並設定 FD_ACCEPT 事件,所以當 Client 與我們連接時,系統會傳給我們一個 ASYNC_EVENT 訊息(請參見前一期文章內容);我們在收到訊息並判斷是 FD_ACCEPT 事件,於是呼叫 accept() 來建立連接。 my_sd = accept(listen_sd, (struct sockaddr far *)&sa, &sa_len) 我們在呼叫完 accept() 函式,成功地建立了 Server 端與 Client 端的連接後, 此時便可利用新建的 Socket(my_sd)來收送資料了。由於我們同樣希望用 Asynchronous 的方式,因此要再利用 WSAAsyncSelect() 函式來幫新建的 Socket 設定一些事件,以便事件發生時 Winsock Stack 能主動通知我們。由於我 們的 Server 是被動的接受 Client 的要求,然後再做答覆,所以我們設定 FD_READ 事件;我們也希望 Winsock Stack 在知道 Client 關閉 Socket 時,能主 動通知我們,所以同時也設定 FD_CLOSE 事件。(讀者須注意,我們設定事件 的 Socket 號碼是呼叫 accept 後傳回的新 Socket 號碼,而不是原先監聽狀態的 Socket 號碼) WSAAsyncSelect(my_sd, hwnd, ASYNC_EVENT, FD_READ|FD_CLOSE) 在這裡,我們同樣是利用 hwnd 這個視窗及 ASYNC_EVENT 這個訊息;在 前文中,筆者曾告訴各位,在收到 ASYNC_EVENT 訊息時,我們可以利用 WSAGETSELECTEVENT(lParam) 來判斷究竟是哪一事件(FD_READ 或 FD_CLOSE)發生了;所以並不會混淆。那我們到底在什麼時候會收到 FD_READ 或 FD_CLOSE 事件的訊息呢? 【FD_READ 事件】 我們會收到 FD_READ 事件通知我們去讀取資料的情況有 : (1)呼叫 WSAAsyncSelect 函式來對此 Socket 設定 FD_READ 事件時, input buffer 中已有資料。 (2)原先系統的 input buffer 是空的,當系統再收到資料時,會通知我們。 (3)使用者呼叫 recv 或 recvfrom 函式,從 input buffer 讀取資料,但是並 沒有一次將資料讀光,此時會再驅動一個 FD_READ 事件,表示仍有資料在 input buffer 中。 讀者必須注意:如果我們收到 FD_READ 事件通知的訊息,但是我們故意 不呼叫 recv 或 recvfrom 來讀取資料的話,爾後系統又收到資料時,並不會再次 通知我們,一定要等我們呼叫了 recv 或 recvfrom 後,才有可能再收到 FD_READ 的事件通知。 【FD_CLOSE 事件】 當系統知道對方已經將 Socket 關閉了的情況下(收到 FIN 通知,並和對方 做關閉動作的 hand-shaking),我們會收到 FD_CLOSE 的事件通知,以便我 們也能將這個相對的 Socket 關閉。FD_CLOSE 事件只會發生於 TCP Socket,因 為它是 connection-oriented;對於 connectionless 的 UDP Socket,即使設了 FD_CLOSE,也不會有作用的。 程式中,當 Client 端送一個要求(request)來時,系統會以 ASYNC_EVENT 訊息通知我們的 hwnd 視窗;我們在利用 WSAGETSELECTEVENT(lParam) 及 WSAGETSELECTERROR(lParam) 知道是 FD_READ 事件及檢查無誤後,便呼叫 recv() 函式來收取 Client 端送來的資料。 recv(wParam, &data, sizeof(data), 0) 筆者在前一期文章中也曾提到說,FD_XXXX 事件發生,收到訊息時,視 窗 handle 被呼叫時的參數 wParam 代表的就是事件發生的 Socket 號碼,所以此 處 wParam 的值也就是前面提到的 my_sd 這個 Socket 號碼。recv() 的第四個參 數設為 0,表示我們要將資料從系統的 input buffer 中讀取並移走。 收到要求後,我們要答覆 Client 端,也就是要送資料給 Client;這時我們就 要利用 send() 這個函式了。 我們先將資料放到 data 這個資料暫存區,然後呼叫 send() 將它送出,我們 利用的也是 wParam (my_sd) 這個同樣的 Socket 來做傳送的動作,因為它是雙向 的。 send(wParam, &data, strlen(data), 0) Server 與 Client 收送資料一段時間後(資料全部收送完畢),如果 Client 端 先呼叫 closesocket() 將它那端的 Socket 關閉,那麼系統在知道後,會通知我們 一個 FD_CLOSE 事件的訊息,此時我們也可以呼叫 closesocket() 將我們這端的 Socket 關閉了;當然我們也可以呼叫 closesocket() 先主動關閉我們這端的 Socket。 【Client 端的資料收送及關閉 Socket】 我們例子的 Client 是採 Blocking 模式,所以在呼叫 connect() 函式與 Server 連接時,可能會等一下子才成功;connect() 函式返回後,且無錯誤發生的話, Client 與 Server 端的 TCP socket 連接就算成功了。這時,我們便可利用這個連 接成功的 Socket 來送收資料了。由於我們並沒有要設定為 Asynchronous 模式, 所以也不用呼叫 WSAAsyncSelect() 來設定事件。 Client 端通常是會先主動發出要求到 Server 端,因此我們呼叫 send() 來傳送 此一資料。我們的資料量很小,所以並不會被 send() 函式 Block 住;不過如果 您要送的資料量很大,那麼可能會等一段時間才會自 send() 函式返回;也就是 說必須等資料都放到系統的 output buffer 後才會返回;這是因為我們 Client 的 Socket 是阻攔模式。如果我們用的是非阻攔模式的 Socket,那麼 send() 函式會 視系統的 output buffer 的空間有多少,只拷貝那麼多的資料到 output buffer,然 後就返回,並告知使用者送出了多少資料,並不須等所有資料都放到 output buffer 才返回。 我們將要求放在 data 資料暫存區,然後呼叫 send() 將要求送出。資料送出 後,我們呼叫 recv() 來等待 Server 端的答覆。 send(mysd, data, strlen(data), 0) recv(mysd, &data, sizeof(data), 0) 由於我們 Client 端是 Blocking 模式,所以 recv() 會一直 Block 住,直到下 列的情況之一發生,才會返回。 (1)Server 端送來資料。(此時 return 值是讀取的資料長度) (2)Server 端將相對的 Socket 關閉了。(此時的 return 值會是 0) (3)Client 端自己呼叫 WSACancelBlockingCall() 來取消 recv() 的呼叫。 (此時 return 值是 SOCKET_ERROR 錯誤,錯誤碼 10004 WSAEINTR) 同樣地,資料全部送收完畢後,我們也呼叫 closesocket() 來將 Socket 關 閉。 ◎ WSACancelBlockingCall():取消目前正在進行中的 blocking 動作。 格 式: int PASCAL FAR WSACancelBlockingCall( void ); 參 數: 無 傳回值: 成功 - 0 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式用來取消該應用程式正在進行中的 blocking 動作。通常的 使用時機有:(a) Blocking 動作正在進行中,該應用程式又收到某一訊息 (Mouse、Keyboard、Timer 等),則可在處理該訊息的段落中呼叫此函式。(b) Blocking 動作正在進行中,而 Windows Sockets 又呼叫回應用程式的 「blocking hook」函式時,在該函式內可呼叫此函式來取消 blocking 動作。 使用者必須注意,在某一 Winsock blocking 函式動作進行時,除了 WSAIsBlocking() 及 WSACancelBlockingCall() 外,不可以再呼叫其它任何 Windows Sockets DLL 提供的函式,否則會產生錯誤。另外若取消的 blocking 動作不是 accept() 或 select() 的話,那麼該 Socket 可能會處於未定 狀態,使用者最好是呼叫 closesocket() 來關閉該 Socket,而不該再對它做任 何動作。 (圖 2.)demoserv 與 democlnt 在資策會 WinKing 上收送資料的畫面 (圖 3.)demoserv 與 democlnt 在資策會 WinKing 上關閉 Socket 後的畫面 介紹完了 TCP Socket 的資料收送,筆者接著為讀者介紹 sendto() 及 recvfrom() 這兩個函式,以及許多人可能很容易搞錯的 FD_WRITE 事件。 【sendto 及 recvfrom 函式】 一般言,TCP Socket 使用的是 send() 及 recv() 這兩個函式;而 UDP Socket 用的是 sendto() 及 recvfrom() 函式。這是因為 TCP 是 Connection-oriented,必須 做完 Socket 真正的連接程序後,才可以開始收送資料,此時系統已經知道了連 接的對方,所以我們不用再指定資料要送到哪裡。而 UDP 是 Connectionless, 收送資料的雙方並沒有建立真正的連接,所以我們要利用 sendto() 及 recvfrom() 來指定收資料的對方及獲知是誰送資料給我們。 TCP Socket 也可以用 sendto() 及 recvfrom() 來送收資料,只是此時這兩個 函式的最後兩個參數沒有作用,會被系統所忽略。而 UDP Socket 如果呼叫了 connect() 函式來指定對方的位址(這個 connect 並不會真的和對方做連接的動 作,而是告知我們本身的系統說我們只想收、送何方的資料),那麼也可以利 用 send() 及 recv() 來送收資料。 ◎ sendto():將資料送到使用者指定的目的地。 格 式: int PASCAL FAR sendto( SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen ); 參 數: s Socket 的識別碼 buf 存放要傳送的資料的暫存區 len buf 的長度 flags 此函式被呼叫的方式 to 資料要送達的位址 tolen to 的大小 傳回值: 成功 - 送出的資料長度 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式適用於 Datagram 或 Stream Socket 來傳送資料到指定的 位址。 對 Datagram Socket 言,若是 datagram 的大小超過限制,則將不會 送出任何資料,並會傳回錯誤值。對 Stream Socket 言,其作用與 send() 相 同;參數 to 及 tolen 的值將被系統所忽略。 若是傳送 (transport) 系統內之儲 存空間不夠存放這些要傳送的資料,sendto() 將會被 block 住,直到資料都被 送出;除非該 Socket 被設定為 non-blocking 模式。使用者亦須注意 sendto() 函式執行完成,並不表示資料已經成功地送抵對方了,而可能仍在系統的 output buffer 中。 flags 的值可設為 0、MSG_DONTROUTE 及 MSG_OOB 的組合。 (參見 WINSOCK第1.1版51頁) ◎ recvfrom():讀取資料,並儲存資料來源的位址。 格 式: int PASCAL FAR recvfrom( SOCKET s, char FAR *buf, int len, int flags, struct socketaddr FAR *from, int FAR *fromlen ); 參 數: s Socket 的識別碼 buf 存放接收到的資料的暫存區 len buf 的長度 flags 此函式被呼叫的方式 from 資料來源的位址 fromlen from 的大小 傳回值: 成功 - 接收到的資料長度 (若對方 Socket 已關閉,則為 0) 失敗 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因) 說明: 此函式用來讀取資料並記錄資料來源的位址。對 Datagram Socket (UDP)言,一次讀取一個 Datagram;對 Stream Socket (TCP)言,其作用與 recv() 相同,參數 from 及 fromlen 的值會被系統忽略。如果 Socket 為 Blocking 模 式,且目前 input buffer 內沒有任何資料,則 recvftom() 將 block 到有任何資料到 達為止;如果為 Non-Blocking 模式,且 input buffer 無任何資料,則會馬上回覆錯 誤。 【FD_WRITE 事件】 筆者在前面介紹過 FD_READ 事件的發生時機,現在繼續介紹 FD_WRITE 這個較易使人混淆的事件,因為真的有相當多的人對此一事件的發生不明瞭。 由字面上看,FD_WRITE 應該是要求系統通知我們某個 Socket 現在是否可 以呼叫 send() 或 sendto() 來傳送資料?答案可以說「是」,但是它和 FD_READ 卻又有不同的地方。 在前面我們知道呼叫一次 recv() 後,如果 input buffer 中尚有資料未被取出 的話,系統會再通知我們一次 FD_READ。那麼如果我們呼叫一次 send() 後, 系統的 output buffer 仍有空間可寫入的話,它是否會再通知我們一個 FD_WRITE,叫我們繼續傳送資料呢?這個答案就是「否定」的了!系統並不 會再通知我們了。 系統會通知我們 FD_WRITE 事件的訊息,只有下列幾種情況: (1)呼叫 WSAAsyncSelect() 來設定 FD_WRITE 事件時,Socket 已經可以 傳送資料(TCP scoket 已經和對方連接成功了,或 UDP socket 已建立完成), 且目前 output buffer 仍有空間可寫入資料。 (2)呼叫 WSAAsyncSelect() 來設定 FD_WRITE 事件時,Socket 尚不能傳 送資料,不過一旦 Socket 與對方連接成功,馬上就會收到 FD_WRITE 的通 知。 (3)呼叫 send() 或 sendto() 傳送資料時,系統告知錯誤,且錯誤碼為 10035 WSAEWOULDBLOCK (呼叫 WSAGetLastError() 得知這項錯誤),這 時表示 output buffer 已經滿了,無法再寫入任何資料(此時即令呼叫再多次的 send() 也都一定失敗);一旦系統將部份資料成功送抵對方,空出 output buffer 後,便會送一個 FD_WRITE 給使用者,告知可繼續傳送資料了。換句話說,讀 者在呼叫 send() 傳送資料時,只要不是返回錯誤 10035 的話,便可一直繼續呼 叫 send() 來傳送資料;一旦 send() 回返錯誤 10035,那麼便不要再呼叫 send() 傳送資料,而須等收到 FD_WRITE 後,再繼續傳送資料。 【結語】 在這一期的文章中,筆者介紹了各位有關 TCP Socket 的資料收、送方式及 FD_READ、FD_WRITE 等事件的發生時機;讀者們綜合前一期的文章,應該 已經可以建立出一對主從架構的程式,並利用 TCP Socket 來傳送資料了。 下一期,筆者將繼續介紹有關如何獲取網路資訊的函式,如 gethostname()、getsockname()、getpeername(),以及同步與非同步的網路資料庫 擷取函式 getXbyY()、WSAAsyncGetXByY()。 本文中所提到的 WinKing 試用版可自 SEEDNET 台北主機 tpts1.seed.net.tw (139.175.1.10)的 UPLOAD/WINKING 目錄中取得,檔名為 wkdemo.exe; WinKing 提供 Ethernet 及 PPP 連線功能,適用於一般 Ethernet 網路,亦可用來 以電話、數據機連上 SEEDNET 的 PPP 伺服主機;範例 demoserv、democlnt, 以及一些筆者所寫的 Winsock 程式(含原始程式碼)則存放在 UPLOAD/WINKING/JNLIN 目錄下;有興趣的讀者可自行用 anonymous ftp 方式 取得。 ************************************************************** Copyright by: 林軍鼐 本文稿非經本人同意,不得任意刊載於任何書報雜誌或做為商業用途 ************************************************************** 簡單的 Winsock 應用程式設計(3) 林 軍 鼐 在前兩期的文章中,筆者介紹@
天外來客
初階會員


發表:22
回覆:199
積分:44
註冊:2001-11-27

發送簡訊給我
#2 引用回覆 回覆 發表時間:2002-06-13 22:39:26 IP:61.16.xxx.xxx 未訂閱
前不久為了Localport 的問題找的人仰馬翻, 沒想到今天就看到這些好文章. 對我們來說, 真的是有很大的幫助. 謝謝
ablueway
一般會員


發表:0
回覆:2
積分:0
註冊:2005-01-02

發送簡訊給我
#3 引用回覆 回覆 發表時間:2005-01-02 22:51:26 IP:218.168.xxx.xxx 未訂閱
-.-
系統時間:2024-04-19 21:11:42
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!