天天看點

QT分析之溫故而知新 WinSocket

網絡程式設計是個很大的課題,這裡隻限Windows Socket程式設計。轉一篇别人的文章,僅供參考。

一,          

什麼是Socket

接觸網絡程式設計當然要了解Socket,Socket(套接字)是一種網絡程式設計接口,Socket提供了很多靈活的函數,幫助程式員寫出高效的網絡應用。Socket分為BSD

UNIX和windows兩個版本。

在win32平台上的Winsock程式設計都要經過下列基本步驟:

定義變量——獲得Winsock版本——加載Winsock庫——初始化——建立套接字——設定套接字——關閉套接字——解除安裝Winsock庫

使用winsock2 API程式設計,必須包含頭檔案winsock2.h

(連結環境WS2_32.LIB),頭檔案winsock.h(WSOCK32.LIB)是為了相容winsock1程式時使用的,另外mswsock.h(MSWSOCK.DLL)是微軟的擴充類,用于開發高性能的winsock程式。

準備好後,你就可以着手建立你的第一個網絡程式了。

二,          

Socket程式設計的基本過程

Socket通信分為面向連接配接的通信(TCP)和面向無連接配接的通信(UDP).

1,Winsock初始化和結束

每一個winsock應用程式必須首先加載相應的winsock dll版本。方法是調用:

int WSAStartup(

WORD wVersionRequested,     庫版本,高位元組副版本,低位元組主版本

LPWSADATA lpWSAData         結構指針,函數自動填充該結構。

);   

函數調用成功傳回0

可以用宏MAKEWORD(x, y)用來指定第一個參數的值

2,建立套接字

套接字是傳輸提供者的一個句柄。

SOCKET socket (

int af,          

int type,        

int protocol      IPPROTO_TCP,

IPPROTO_UDP, 0(如果不想指定)

);

第一個參數指定通信協定的協定族,AF_INET(IPv4)或 AF_INET6(IPv6)(因為Socket是網絡程式設計接口而不是一個協定,它使用流行的網絡協定(TCP/IP,IPX)為應用程式提供的一個程式設計接口。)

第二個參數指定要建立的套接字的類型。SOCK_STREAM(TCP流套接字), SOCK_

DGRAM(UDP 資料包套接字),SOCK_RAW(原始套接字)

第三個參數指定應用程式所指定應用程式所使用的通信協定。

函數成功傳回套接字描述符,失敗傳回INVALID_SOCKET

3,配置套接字

當建立一個套接字後,再進行網絡通信之前,必須先配置Socket。面向連接配接的用戶端Socket通過調用connect函數在Socket資料結構中儲存位址和遠端資訊。無連接配接用戶端,服務端以及面向連接配接Socket的服務端,通過調用bind函數來配置本地資訊。

int bind(

SOCKET                     s,                 建立的套接字

const struct sockaddr FAR* name,       指向位址緩沖區的指針

int                        namelen     位址緩沖區的大小

成功傳回0,失敗傳回SOCKET_ERROR

當建立一個套接字後,套接字資料結構中有一個預設的IP位址和預設的端口号。一個服務程式必須調用bind函數來給其綁定一個IP位址和一個特定的端口号。

第二個參數指定一個sockaddr結構定義如下:

struct sockaddr {

u_short    sa_family;

char       sa_data[14];

};

我們通常使用另外一個等價的位址結構:

de>struct sockaddr_in {de>

de>       

short   sin_family;de>

u_short sin_port;de>

de>   

    struct  in_addr sin_addr;de>

char    sin_zero[8];de>

de>};de>

de>其中sin_family是通信協定族,de>

de>sin_portde>de>指明端口号,de>

de>sin_addrde>de>結構中有一個字段s_addr,表示IP位址,該字段是一個整數,de>

de>一般用函數inet_addr把點分字元串形式的IP位址轉化成unsigned long型的整數值。de>

de>de>de>如果指定為htonl(INADDR_ANY),那麼無論哪個網段上的客戶機都能與該伺服器通信,de>

de>否則,隻有與指定IP位址處于同一網段上的客戶機能與該伺服器通信。de>

de>sin_zero[8]de>de>為填充,使兩個結構大小相同。de>

de> de>

de>有一些細節學要說明:de>  

在計算機把IP位址和端口号指定成多位元組時,這個數是按“主機位元組”(host-byte)順序表示的,不同的處理器對數的表示方法有“大頭”(big-endian——最有意義的位元組到最無意義的位元組)和“小頭”(little-endian)兩種形式。但是如果在網絡上指定IP位址和端口号時,必須按照big-endian

的形式,一般稱之為“網絡位元組”(network-byte)順序。

以下幾個函數将主機位元組順序轉換成網絡位元組順序:

u_long htonl(u_long hostlong);

int WSAHtonl(

SOCKET s,

u_long hostlong,

u_long FAR * lpnetlong               通過指針傳回網絡位元組順序的4個位元組的數

u_short htons(u_short hostshort);

int WSAHtons(

u_short hostshort,

u_short FAR * lpnetshort        通過指針傳回網絡位元組順序的2個位元組的數

以下幾個函數将網絡位元組順序轉換成主機位元組順序:

u_long ntohl(u_long netlong);

int WSANtohl(

u_long netlong,

u_long FAR * lphostlong

u_short ntohs(u_short netshort);

int WSANtohs(

u_short netshort,

u_short FAR * lphostshort

de>

4,實作功能

(1)      

伺服器端:

需要對綁定的端口進行偵聽,函數原型如下

 de>int listen (de>

de> 

SOCKETde>de> sde>de>,   

de>de>de>

de>  intde>de> backlog 

de>);de>de>de>

de>Backlogde>de>是客戶連接配接請求隊列的最大數量,而不是客戶機連接配接的數量限制。de>

de>處于偵聽的套接字将維護一個客戶連接配接請求隊列。de>

de>de>該函數執行成功傳回0,失敗傳回de>SOCKET_ERROR

此外,需要從連接配接請求隊列中取出最前面的一個客戶請求,需要用到accept()函數:

de>SOCKET accept (de>

SOCKETde>de> sde>de>,                  

de>  struct sockaddr FAR*de>de> addrde>de>, 

de>  int FAR*de>de> addrlen           

de>);de>

de>該函數建立一個新的套接字來與客戶套接字建立通信,如果連接配接成功,就傳回建立的套接字描述符,de>

de>以後與客戶通信的就是該套接字,而偵聽套接字則繼續接受新的連接配接;如果失敗就傳回de>INVALID_SOCKET

第一個參數是偵聽套接字

第二個套接字用來傳回新建立的套接字的位址結構

第三個套接字傳回位址結構的長度

(2)      

用戶端:

connect函數是客戶機建立與遠端伺服器連接配接而使用的。

de>int connect (de>

SOCKETde>de> sde>de>,                         

de>  const struct sockaddr FAR*

de>de> namede>de>, 

de>  intde>de> namelen                       

5,資料傳輸

收發資料時網絡程式設計的一切在這裡我們隻讨論同步函數send和recv,

不讨論異步函數WSASend和WSARecv。

資料發送要用到send函數,原型如下:

int send(

SOCKET s,

    const char FAR * buf,

   發送資料緩沖區的位址

int len,                  要發送的位元組數

int flags                 一般為0,

MSG_DONTROUTE, MSG_OOB(外帶資料)

成功傳回發送位元組數,出錯傳回SOCKET_ERROR

注意send函數把資料從buf複制到socket發送緩沖區後就傳回了,

但是這些資料并不時馬上就發送到連接配接的另一端。

在已連接配接的套接字上接受資料,recv函數是最基本的方式。

int recv(

    char FAR* buf,

    int len,

      準備接收的位元組數或buf緩沖區長度

int flags      0, MSG_PEEK, MSG_OOB

其中MSG_PEEK表示将有用的資料複制到所提供的接收端緩沖區,

但是沒有從系統緩沖區中将它删除

成功傳回接收的資料的位元組數量,失敗傳回SOCKET_ERROR,如果接受資料時網絡中斷傳回0

6,關閉Socket

一旦完成任務,記得将套接字關閉以釋放資源:

int closeSocket(SOCKET s)

三,          

Socket程式設計執行個體

講了這麼多其實還是看執行個體最為重要了,下面我提供了最簡單的面向連接配接的用戶端和服務端程式,

當服務端接受用戶端的連接配接後,先是該客戶機地IP位址,并向用戶端發送一個回應消息,

最後關閉該連接配接套接字。這樣的伺服器當然沒什麼實際的用途。

設計一個基本的網絡伺服器有以下幾個步驟:

1、初始化Windows Socket

2、建立一個監聽的Socket

3、設定伺服器位址資訊,并将監聽端口綁定到這個位址上

4、開始監聽

5、接受用戶端連接配接

6、和用戶端通信

7、結束服務并清理Windows Socket和相關資料,或者傳回第4步

#include <winsock2.h>

#include <stdio.h>

#define SERVPORT   

5050

#pragma comment(lib,"ws2_32.lib")

void main(void)

{

WSADATA              wsaData;

SOCKET               sListen;              //

監聽socket

SOCKET               sClient;       

// 連接配接socket

SOCKADDR_IN          serverAddr;              //

本機位址資訊

SOCKADDR_IN          clientAddr;        //

用戶端位址資訊

int                        clientAddrLen;      

// 位址結構的長度

int                  nResult;

// 初始化Windows Socket 2.2

WSAStartup(MAKEWORD(2,2), &wsaData);

// 建立一個新的Socket來響應用戶端的連接配接請求

sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// 填寫伺服器綁定的位址資訊

// 端口為5150

// IP位址為INADDR_ANY,響應每個網絡接口的客戶機活動

// 注意使用htonl将IP位址轉換為網絡格式

serverAddr.sin_family = AF_INET;

serverAddr.sin_port = htons(SERVPORT);   

serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

memset(&(serverAddr.sin_zero), 0, sizeof(serverAddr.sin_zero));      

// 綁定監聽端口

nResult = bind(sListen, (SOCKADDR *)&serverAddr, sizeof(SOCKADDR));

if (nResult == SOCKET_ERROR)

printf("bind failed!\n");

return;

}

// 開始監聽,指定最大接受隊列長度5,不是連接配接數的上限

listen(sListen, 5);

// 接受新的連接配接

while(1)

{

clientAddrLen = sizeof (SOCKADDR);

sClient = accept(sListen, (SOCKADDR *)&clientAddr, &clientAddrLen);

if(sClient == INVALID_SOCKET)

printf("Accept failed!");

else

printf("Accepted client: %s : %d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

// 向用戶端發送資訊

nResult = send(sClient, "Connect success!", 16, 0);

printf("send failed!");

// 我們直接關閉連接配接,

closesocket(sClient);

// 并關閉監聽Socket,然後退出應用程式

closesocket(sListen);

// 釋放Windows Socket DLL的相關資源

WSACleanup();

客戶機程式如下:

5050              // 端口為5150

#define MAXDATASIZE 100

#define SERVIP     

"127.0.0.1"      // 伺服器IP位址為"127.0.0.1",注意使用inet_addr将IP位址轉換為網絡格式

void main(int argc, char *argv[])

SOCKET               sConnect;

SOCKADDR_IN          serverAddr;      

int                                 recvbytes;

char                               buf[MAXDATASIZE];

//初始化Windows Socket 2.2

 // 建立一個新的Socket來連接配接伺服器

sConnect = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// 填寫連接配接位址資訊

serverAddr.sin_addr.s_addr = inet_addr(SERVIP);

memset(&(serverAddr.sin_zero), 0, sizeof(serverAddr.sin_zero));

// 向伺服器發出連接配接請求

if (connect(sConnect, (SOCKADDR *)&serverAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)

printf("connect failed!\n");

// 接受伺服器的回應消息

recvbytes = recv(sConnect, buf, MAXDATASIZE, 0);

if (recvbytes == SOCKET_ERROR)

printf("recv failed!\n");

buf[recvbytes] = '\0';

printf("%s\n",buf);

closesocket(sConnect);

四,          

結束語

這裡介紹的隻不過是winsock最基礎最基礎的知識,開發網絡程式從來都不是一件容易的事情,

盡管隻需要遵守很少的一些規則:

建立套接字,發起連接配接,接受連接配接,

發送和接受資料。真正的困難在于:讓你的程式可以适應從單單一個連接配接到幾千個連接配接,

即開發一個大容量,具可擴充性的Winsock應用程式。

Winscok程式設計很重要的一部分是IO處理,分别提供了“套接字模式”和“套接字I/O模型”,

可對一個套接字上的I/O行為進行控制。

其中,套接字模式用于決定在随一個套接字調用時,那些Winsock函數的行為,

有阻塞和非阻塞兩種模式。

而另一方面,套接字模型描述了一個應用程式如何對套接字上進行的I/O進行管理及處理,

包括包括Select,WSAAsyncSelect,WSAEventSelect

,IO重疊模型,完成端口等。

在這裡推薦一本好書《windows網絡程式設計技術》,有興趣研究的同志可以去啃一啃

繼續閱讀