天天看點

winsock程式設計入門

一,           什麼是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),通信流程如下:

                    面向連接配接的通信

winsock程式設計入門

                 無連接配接的通信

winsock程式設計入門

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];

};

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

struct sockaddr_in {
       
           
        short   sin_family;
       
           
        u_short sin_port;
       
           
        struct  in_addr sin_addr;
       
           
        char    sin_zero[8];
       
           
};
       
           
其中sin_family是通信協定族,
        
           
sin_port              指明端口号,
                   
sin_addr              結構中有一個字段s_addr,表示IP位址,該字段是一個整數,           
一般用函數inet_addr把點分字元串形式的IP位址轉化成unsigned long型的整數值。           
如果指定為htonl(INADDR_ANY),那麼無論哪個網段上的客戶機都能與該伺服器通信,           
否則,隻有與指定IP位址處于同一網段上的客戶機能與該伺服器通信。
                   
sin_zero[8]              為填充,使兩個結構大小相同。
                   

有一些細節學要說明:

   在計算機把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(

    SOCKET s,

    u_short hostshort,

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

);

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

u_long ntohl(u_long netlong);

int WSANtohl(

    SOCKET s,

    u_long netlong,

    u_long FAR * lphostlong

);

u_short ntohs(u_short netshort);

int WSANtohs(

    SOCKET s,

    u_short netshort,

    u_short FAR * lphostshort

);

4,實作功能

      

         
          
         

      
(1)       伺服器端: 需要對綁定的端口進行偵聽,函數原型如下
          

             
int listen (
                       
  SOCKET              s              ,                   
  int              backlog                 
);               
Backlog 是客戶連接配接請求隊列的最大數量,而不是客戶機連接配接的數量限制。 處于偵聽的套接字将維護一個客戶連接配接請求隊列。 該函數執行成功傳回0,失敗傳回 SOCKET_ERROR     此外,需要從連接配接請求隊列中取出最前面的一個客戶請求,需要用到accept()函數:
SOCKET accept (
                       
  SOCKET              s              ,                                  
  struct sockaddr FAR*              addr              ,                 
  int FAR*              addrlen                           
);
           
           
該函數建立一個新的套接字來與客戶套接字建立通信,如果連接配接成功,就傳回建立的套接字描述符,           
以後與客戶通信的就是該套接字,而偵聽套接字則繼續接受新的連接配接;如果失敗就傳回                INVALID_SOCKET 
                
第一個參數是偵聽套接字
                 
第二個套接字用來傳回新建立的套接字的位址結構
                 
第三個套接字傳回位址結構的長度
                 
(2)       用戶端: connect函數是客戶機建立與遠端伺服器連接配接而使用的。
int connect (
           
           
  SOCKET              s              ,                                         
  const struct sockaddr FAR*                name              ,                 
  int              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(
                
    SOCKET s, 
                
    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);                      if (nResult == SOCKET_ERROR)                      {                             printf("send failed!");                      }               }         // 我們直接關閉連接配接,               closesocket(sClient);        }           // 并關閉監聽Socket,然後退出應用程式         closesocket(sListen);      // 釋放Windows Socket DLL的相關資源         WSACleanup(); }
客戶機程式如下:
                    
#include <winsock2.h> #include <stdio.h>   #define SERVPORT    5050              // 端口為5150 #define MAXDATASIZE 100 #define SERVIP      "127.0.0.1"      // 伺服器IP位址為"127.0.0.1",注意使用inet_addr将IP位址轉換為網絡格式 #pragma comment(lib,"ws2_32.lib")   void main(int argc, char *argv[]) {    WSADATA              wsaData;    SOCKET               sConnect;    SOCKADDR_IN          serverAddr;          int                                 recvbytes;    char                               buf[MAXDATASIZE];      //初始化Windows Socket 2.2      WSAStartup(MAKEWORD(2,2), &wsaData);       // 建立一個新的Socket來連接配接伺服器       sConnect = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);       // 填寫連接配接位址資訊       serverAddr.sin_family = AF_INET;     serverAddr.sin_port = htons(SERVPORT);        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");                 return;        }        // 接受伺服器的回應消息     recvbytes = recv(sConnect, buf, MAXDATASIZE, 0);        if (recvbytes == SOCKET_ERROR)        {               printf("recv failed!/n");        }        else        {               buf[recvbytes] = '/0';               printf("%s/n",buf);        }       closesocket(sConnect);      // 釋放Windows Socket DLL的相關資源         WSACleanup(); } 四,           結束語 這裡介紹的隻不過是winsock最基礎最基礎的知識,開發網絡程式從來都不是一件容易的事情, 盡管隻需要遵守很少的一些規則: 建立套接字,發起連接配接,接受連接配接, 發送和接受資料。真正的困難在于:讓你的程式可以适應從單單一個連接配接到幾千個連接配接, 即開發一個大容量,具可擴充性的Winsock應用程式。 Winscok程式設計很重要的一部分是IO處理,分别提供了“套接字模式”和“套接字I/O模型”, 可對一個套接字上的I/O行為進行控制。 其中,套接字模式用于決定在随一個套接字調用時,那些Winsock函數的行為, 有阻塞和非阻塞兩種模式。 而另一方面,套接字模型描述了一個應用程式如何對套接字上進行的I/O進行管理及處理, 包括包括Select,WSAAsyncSelect,WSAEventSelect ,IO重疊模型,完成端口等。 在這裡推薦一本好書《windows網絡程式設計技術》,有興趣研究的同志可以去啃一啃。

繼續閱讀