流式套接字程式設計
1、網路資料的傳輸是通過套接字來實現的。套接字有三種類型:流式套接字(SOCK_STREAM),資料報套接字(SOCK_DGRAM)及原始套接字(RAW);
2、流式套接字是面向連線的、提供雙向、有序、無重複且無記錄邊界的資料流服務,適用於處理大量資料,可靠性高,但開銷也大。
3、伺服器端程式設計步驟:
(1)呼叫WSAStartup()初始化:此函式在應用程式中初始化Windows Sockets DLL,只有此函式呼叫成功後,應用程式才可以再呼叫其他的API函式。
呼叫形式如下:
int WSAStartup(
WORD wVersionRequested,// 所使用的WinSocket版本
LPWSADATA lpWSAData// 存數系統返回的WinSocket資訊
);
(2)建立socket
初始化WinSock的動態連結庫後,需要在伺服器端建立一個Socket,為此可以呼叫socket()函式來建立這個監聽的Socket,並定義此Socket所使用的通訊協議。
SOCKET socket(
int af,// 目前只提供PF_INET(AF_INET)
int type,// Socket的型別(SOCK_STREAM、SOCK_DGRAM)
int protocol,// 通訊協議(如果使用者不指定,則設為0)
);
呼叫成功後返回Socket物件,失敗則返回INVALID_SOCKET(呼叫WSAGetLastError()可得知原因,所有的函式都可以使用這個函式返回來獲取失敗的原因)。
(3)繫結埠
接下來要為伺服器端定義的監聽Socket指定一個地址以及埠,這樣客戶端才能夠知道要連線到何處,為此需要呼叫bind()函式,該函式呼叫成功返回0,否則返回SOCKET_ERROR。
int bind(
SOCKET s,// Socket物件名
const struct sockaddr FAR *name,// Socket的地址值,即所在及其的IP地址
int namelen// name的長度
);
如果使用者不在意地址或埠的值,那麼可以設定地址為INADDR_ANY,及Port為0,Windows Socket會自動將其設定為適當的地址以及Port(1024到5000之間的值)。此後,可以呼叫getsockname()函式來獲知其被設定的值。
(4)監聽
當伺服器端的Socket物件繫結完成之後,必須建立一個監聽的佇列來接收客戶端的連線請求。listen()函式使伺服器的Socket進入監聽狀態,並設定可以建立的最大連線數,該函式呼叫成功返回0,否則返回SOCKET_ERROR。
int listen(
SOCKET s,// 需要建立監聽的Socket
int backlog,// 最大連線個數
);
伺服器端的Socket呼叫完listen()之後,如果此時客戶端呼叫connect()函式提出連線申請的話,伺服器端必須再呼叫accept()函式,這樣服務端和客戶端才算正式完成通訊程式的連線動作。
為了知道什麼時候客戶端提出連線要求,從而伺服器端的Socket在恰當的時候呼叫accept()函式完成連線的建立,我們就要使用WSAAsyncSelect()函式,讓系統主動通知我們有客戶端提出連線請求了,該函式呼叫成功返回0,呼叫失敗返回SOCKET_ERROR。
int WSAAsyncSelect(
SOCKET s,// Socket物件
HWND hwnd,// 接收訊息的視窗控制代碼
unsigned int wMsg,// 傳給視窗的訊息
long lEvent// 被註冊的網路事件
);
被註冊的網路事件lEvent就是應用程式向視窗傳送訊息的網路事件,該值為下列值的組合:
FD_READ:希望在套接字s收到資料時收到訊息。
FD_WRITE:希望在套接字s上可以傳送資料時收到訊息。
FD_ACCEPT:希望在套接字s上收到連線請求時收到訊息。
FD_CONNECT:希望在套接字s上連線成功是收到訊息。
FD_CLOSE:希望在套接字s上連線關閉時收到訊息。
FD_OOB:希望在套接字s上收到OOB資料時收到訊息。
具體應用時,wMsg是在應用程式中定義的訊息名稱,而訊息結構體中lParam則為以上各種網路事件的名稱。所以,可以在視窗處理自定義訊息的函式中使用以下結構來響應Socket的不同事件:
switch (lParam) {
case FD_READ:
...
break;
case FD_WRITE:
...
break;
...
}
(5)伺服器端接受客戶端的連線請求
當Client提出連線請求時,Server端的hwnd視窗會收到Winsock Stack送來的我們自己定義的一個訊息,這時我們可以分析lParam,然後呼叫相關的函式來處理此事件。為了使伺服器端接受客戶端的連線請求,就要使用accept()函式,該函式新建一個Socket與客戶端的Socket相通,原先監聽的Socket繼續進入監聽狀態,等待其他客戶端的連線要求,該函式嗲用成功返回一個新產生的Socket物件,否則返回INVALID_SOCKET。
SOCKET accept(
SOCKET s,// Socket的識別碼
struct sockaddr FAR *addr,// 存放連線的客戶端地址
int FAR *addrlen// 地址長度
);
(6)結束Socket連線
結束服務端和客戶端的連線是很簡單的,這一過程可以由伺服器或客戶機的任一端啟動,只要呼叫closesocket()就可以了,而要關閉服務端監聽狀態的Socket,同樣也是利用此函式。
另外,與程式啟動時呼叫WSAStartup()函式相對應,程式結束前需要呼叫WSACleanup()來通知Winsock Stack釋放Socket所佔用的資源。這兩個函式都是呼叫成功返回0,否則返回SOCKET_ERROR。
int closesocket(
SOCKET s;// Socket的識別碼
);
(7)最後呼叫WSACleanup
int WSACleanup(void);
4、客戶端程式設計步驟:
(1)建立客戶端Socket
客戶端應用程式首先也是呼叫WSAStartup()函式來與Winsock的動態連結庫建立關係,然後同樣呼叫socket()來建立一個TCP或UDP Socket(相同協定的Socket才能想通,TCP對TCP,UDP對UDP)。與服務端Socket不同的是,客戶端的Socket可以呼叫bind(0函式,有自己來指定IP地址以及Port號碼,但是也可以不呼叫bind()函式,而由Winsock來自動設定IP地址以及埠號。
(2)提出連線請求
客戶端的Socket使用connect()函式來提出與服務端的Socket建立連線的申請,函式呼叫成功返回0,否則返回SOCKET_ERROR。
int connect(
SOCKET s,// 服務端Socket的識別碼
const struct sockaddr FAR *name,// Socket想要連線的對方地址
int namelen// 地址長度
);
5、資料的傳送
基於TCP/IP連線協議(流式套接字)的服務是設計客戶機/伺服器應用程式時的主流標準,但有些服務是可以通過無連線協議(資料報套接字)提供的。一般情況下TCP Socket的資料傳送和接收是呼叫send()和recv()兩個函式來達成的,而UDP Socket則是使用sendto()以及recvfrom()這兩個函式,這兩個函式呼叫成功返回傳送或接收資料的長度,否則返回SOCKET_ERROR。
int send(
SOCKET s,// Socket的識別碼
const char FAR *buf,// 存放要傳送的資料的快取區
int len, // buf的長度
int flags// 此函式被呼叫的方式
);
對於Datagram Socket而言,若是Datagram的大小超出限制,則將不會送出任何資料,並會傳回錯誤值。對於Stream Socket而言,在Blocking模式下,若是傳送系統內的儲存空間不夠存放這些要傳送的資料,send()將會被block住,直到資料送完為止;如果該Socket被設定為Non-Blocking模式,那麼output buffer空間有多少,就送出多少資料,並不會被block住。
flags的值可設為0或MSG_DONTROUTE及MSG_OOB的組合。
int recv(
SOCKET s,// Socket的識別碼
char FAR *buf,// 存放接收到的資料的快取區
int len,// buf的長度
int flags// 此函式被呼叫的方式
);