服務器端 SOCKET 編程


使用 Socket 的程序在使用 Socket 之前必須調用 WSAStartup() 函數,

此函數在應用程序中用來初始化 Windows Socket DLL,

只有此函數調用成功后,應用程序才可以再調用 Windows Sockets DLL 中的其他 API 函數,

否則后面的任何函數都將調用失敗。

 

WSAStartup() 函數原型:

int WSAStartup(

    WORD wVersionRequested, // 應用程序欲使用的 Windows Socket 版本號。 
// 其高位字節是次版本號,低位字字節是髙版本號。可以使用宏 MAKEWORD(X,Y)來設置其版本號,x 表示高位字節,y 表示低位字節。
LPWSADATA lpWSAData // 指向 WSADATA 的指針。用來存儲 Winsock 動態庫信息。 );

函數調用后返回一個 int 型的值,通過檢查這個值來確定初始化是否成功。該函數執行成功后返回 0。

 

在程序中調用該函數的形式如下:

WSAStartup( MAKEWORD(2,2), (LPWSADATA) &WSAData)

其中 MAKEWORD(2,2) 表示程序使用的是 WinSocket 2 版本,WSAData 用來存儲系統傳回的關於 WinSocket 的結構。

 

建立Socket:

初始化 WinSock 的動態連接庫后,需要在服務器端建立一個用來監聽的 Socket 句柄,為此可以調用 Socket() 函數,

用來建立這個監聽的 Socket 句柄,並定義此 Socket 所使用的通信協議。

 

Socket 的原型:

SOCKET socket(

     int domain,  // 指定應用程序使用的通信協議的協議族,對於 TCP/IP 族,該函數設置為 AF_INET。

     int type,   // 指定要創建的套接字類型。

     int protocol  // 指定應用程序所使用的通信協議。

);

 

在Winsock 2 中,type 支持以下三種類型:

      SOCK_STREAM : 流式套接字。

      SOCK_DGRAM : 數據報套接字。

      SOCK_RAW:原始套接字。

在Winsock 2 中,protocol 支持以下3種類型:

     IPPROTO_UDP : UDP 協議,用於無連接數據報套接字。

     IPPROTO_TCP : TCP 協議,用於流套接字。

     IPPROTO_ICMP : ICMP 協議,用於原始套接字。

 

該函數調用成功則返回 Socket 對象,函數調用失敗則返回 INVALID_SOCKET(調用 WSAGetLastError() 

可得知函數調用失敗的原因,所有 WinSocket 的函數都可以使用這個函數來獲取失敗的原因)

在 Windows 程序中,並不是用內存的物理地址來標志內存塊,文件,任務和動態裝入模塊;

相反,Windows API 給這些項目分配確定的句柄,並將句柄返回給應用程序,然后通過句柄來進行操作。

 

提示:

句柄是 Windows 用來標志被應用程序所建立或使用的對象的唯一整數,Windows 使用各種各樣的句柄標志,

諸如應用程序,窗口,控件,位圖,GDI 對象等。一個Windows 應用程序可以用不同的方法獲得一個特定項的句柄。

通常,Windows 通過應用程序的引出函數將一個句柄作為參數傳遞給應用程序,應用程序一旦獲得了一個確定的句柄,

便可以在 Windows 環境下的任何地方對這個句柄進行操作。

 

綁定端口:

當創建了一個 Socket 以后,套接字的數據結構里有一個默認的 IP 地址和默認的 端口號。

一個服務器端程序必須調用 bind() 函數來為其綁定一個 IP 地址和一個特定的 端口號。

這樣客戶端才知道連接服務器的哪一個 IP 地址的哪一個 端口。

客戶端程序一般不必調用 bind() 函數來為其 Socket 綁定 IP 地址和 端口號。

該函數調用成功返回 0 ,否則返回 SOCKET_ERROR.

 

 bind() 函數原型:

int bind(

    SOCKET s,      // 指定待綁定的 Socket 描述符。

    CONST struct sockaddr FAR *name,  // 指定一個 sockaddr 結構。

    int namelen   // name 結構體的大小。

);

 

這里需要簡單介紹下第二個參數 name,這個參數是一個 socket 結構體類型,socket 結構類型定義如下。

struct sockaddr(

      u_short sa_family;  //指定地址族,對於 TCP/IP 族的套接字,將其設置為 AF_INET.

      char sa_data[14];

);

 

對於 TCP/IP 族的套接字進行綁定時,通常使用另一個地址結構:

struct sockaddr_in(

      short sin_family;    // 設置 TCP/IP 協議族類型 AF_INET。

      u_short sin_port;    // 指明端口號。

      struct in_addr sin_addr;   // 指明 IP 地址,該字段為整數。

                               //一般用 inet_addr()函數把字符串形式的 IP,轉換成 unsigned long 型整數值后再發送給 s_addr。

      char sin_zero[8];

);

 

in_addr是一個結構體,可以用來表示一個32位的IPv4地址。

結構體里面只有一個 s_addr 變量,用來存儲 IPv4 地址。

 

bind() 函數調用示例:

//...   

     struct sockaddr_in name;

     name.sin_family = AF_INET;

     name.sin_port = htonl(INADDR_ANY);

     name.sin_addr.s_addr = htonl(INADDR_ANY);

     int namelen = sizeof(name);

     bind(sSocket, (struct sockaddr *) &name, namelen);

//...

 

htonl函數:

將主機的unsigned long值轉換成網絡字節順序(32位)(一般主機跟網絡上傳輸的字節順序是不通的,分大小端),函數返回一個網絡字節順序的數字。

網絡字節序與主機字節序:

 

主機字節序就是我們平常說的大端和小端模式:不同的CPU有不同的字節序類型,

這些字節序是指整數在內存中保存的順序,這個叫做主機序。引用標准的Big-Endian和Little-Endian的定義如下:

  a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。

  b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。

網絡字節序:4個字節的32 bit值以下面的次序傳輸:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。

這種傳輸次序稱作大端字節序。由於TCP/IP首部中所有的二進制整數在網絡中傳輸時都要求以這種次序,因此它又稱作網絡字節序。

字節序,顧名思義字節的順序,就是大於一個字節類型的數據在內存中的存放順序,一個字節的數據沒有順序的問題了。

 

所以:在將一個地址綁定到socket的時候,請先將主機字節序轉換成為網絡字節序,

而不要假定主機字節序跟網絡字節序一樣使用的是Big-Endian。由於這個問題曾引發過血案!公司項目代碼中由於存在這個問題,

導致了很多莫名其妙的問題,所以請謹記對主機字節序不要做任何假定,務必將其轉化為網絡字節序再賦給socket。

 

記憶這類函數,主要看前面的n和后面的hl。n代表網絡,h代表主機host,l代表long的長度,還有相對應的s代表16位的short。

 

以上代碼中,如果不需要特別指明 IP 地址和 端口號,那么可以設定 IP 地址為 INADDR_ANY, Port 為 0。

Windows Sockets 會自動為其設定適當的 IP 地址及 Port(1024~5000)。

如果想得到 IP 地址和 端口號,可以調用 getsockname() 函數來獲知其被設定的值。

 

監聽端口:

服務器端的 Socket 對象綁定完成后,服務端必須建立一個監聽的隊列,來接收客戶端發來的連接請求。

listen() 函數使服務器端的 Socket 進入監聽狀態,並設定可以建立的最大連接數。

 

listen() 函數原型:

int listen(

     SOCKET S,       // 指定監聽的 Socket 描述符。

     int backlog     // 為一次同時連接的最大數目(不可超過 5)

);

該函數調用成功后返回 0,否則返回 SOCKET_ERROR。服務器端的 Socket 調用完 listen() 函數后,

使其套接字 s 處於監聽狀態,處於監聽狀態的流套接字 s 將維護一個客戶連接請求隊列。最多容納 backlog 個客戶請求。

 

創建服務器端接收客戶端請求:

當客戶端發出連接請求時,服務器端 hwnd 視窗會收到 Winsock stack 送來自定義的一個消息。

為了使服務器端接收客戶端的連接請求,就要使用 accept() 函數。處於監聽狀態的流套接字 s 

從客戶端連接請求隊列中取出排在最前面的一個客戶請求,並且創建一個新的套接字來與客戶端

套接字共同創建連接通道。原來處於監聽狀態的套接字繼續處於監聽狀態,等待客戶端的連接,

這樣服務器端和客戶端才算正式完成通信程序的連接動作。如果創建連接通道成功,

就返回新創建套接字的描述符,以后與客戶端套接字交換數據的是新創建的套接字;

如果失敗就返回 INVALID_SOCKET。

 

accept() 函數原型:

SOCKET accept(

      SOCKET s,         // 處於監聽狀態的流套接字。

      struct sockaddr FAR *addr, // 用來返回新創建的套接字的地址結構。

      int FAR *addrlen    // 指明用來返回新創建的套接字的地址結構的長度。

);

accept() 函數示例:

//...

     struct sockaddr_in addr;

     int addrlen;

     addrlen = sizeof(addr);

     accept(sSocket,(struct sockaddr *)&addr,&addrlen);

//...

 

服務器端響應客戶端連接請求:

當客戶端向服務器端發出連接請求,服務端即可用 accept() 函數實現與客戶端建立連接,為了達到

服務端的 Socket 在恰當的時候與從客戶端發來的連接請求建立連接,服務端需要使用 WSAAsyncSelect() 函數,

讓系統主動來通知服務器端程序有客戶端提出連接請求了。

該函數調用成功返回 0,否則返回 SOCKET_ERROR。

 

WSAAsyncSelect() 函數原型:

int WSAAsySelect(

    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 收到帶外數據時收到消息。

 

該函數在具體應用時,wMsg 應是在應用程序中定義的消息名稱,而消息結構中的 IParam 則為以上各種網絡事件名稱。

 

所以,可以在窗口處理自定義消息函數中使用以下結構代碼來響應 Socket 的不同事件。

switch(lParam)
{
    case FD_READ:
     ......
     break;
    case FD_WRITE:
     ......
     break;
    case FD_ACCEPT:
     ......
     break;
//...
}

 

完成服務端與客戶端 Socket 連接:     

結束服務器端與客戶端的通信連接,可以由服務器端或客戶端的任一端發出請求,只要調用 closesocket() 就可以了。

同樣的,要關閉服務器端監聽狀態的 Socket,也是利用該函數。在調用 closesocket() 函數關閉 Socket 之前,

與程序啟動時調用 WSAStartup() 函數相對應。程序結束前,需調用 WSACleanup() 來通知  Winsock Stack釋放 Socket 所占用的資源。

該函數調用成功后返回 0,失敗返回 SOCKET_ERROR。

 

WSACleanup()函數原型:

int WSACleanup();

該函數在應用程序完成對請求的 Socket 庫的使用后調用,來解除與 Socket 庫的綁定並且釋放 Socket 庫

所占用的資源。該函數一般用在網絡程序結束的地方調用。

 

closesocket() 函數原型:

int closesocket(

    socket s    // 表示關閉的套接字。

);

該函數如果成功執行就返回 0,否則返回 SOCKET_ERROR。

每個進程中都有一個套接字描述表,表中的每個套接字描述符都對應了一個位於操作系統緩存區的套接字數據結構。 

因此,可能有幾個套接字描述符指向同一個套接字數據結構。套接字數據結構中專門有一個字段存放該結構被引用的次數,

即有多少個套接字描述符指向該結構。當調用 closesocket() 函數時,操作系統先檢查套接字數據結構中的該字段的值。

如果值為 1,就表明只有一個套接字描述符指向它,因此操作系統就先把 s 在套接字描述符表中對應的那條表項清除,

並且釋放 s 對應的套接字數據結構;如果該字段值大於 1,那么操作系統僅僅清除 s 在套接字描述符表中的對應表項,

並且把 s 對應的套接字數據結構的引用次數減 1。

 

// 服務端
#include<stdio.h>
#include<winsock.h>
#pragma comment(lib,"ws2_32")
#define MYPORT 1234
#define BACKLOG 10
int main()
{
 SOCKET sockfd, new_fd;
 struct sockaddr_in my_addr;
 struct sockaddr_in their_addr;
 int sin_size = sizeof(struct sockaddr_in);
 int num;
 char Buffer[MAX_PATH];
 WSADATA ws;
 if (WSAStartup(MAKEWORD(2, 2), &ws) != 0)
    {
     exit(0);
    }
 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
    {
     exit(0);
    }
 my_addr.sin_family = AF_INET;
 my_addr.sin_port = htons(MYPORT);
 my_addr.sin_addr.S_un.S_addr = INADDR_ANY;
 if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr)) == -1)
    {
     closesocket(sockfd);
     exit(0);
    }
 if (listen(sockfd, BACKLOG) == SOCKET_ERROR)
    {
     closesocket(sockfd);
     exit(0);
    }
 if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == INVALID_SOCKET)
    {
     closesocket(sockfd);
     exit(0);
    }
 printf("\nRequest Has Been Accept!\n");
 printf("\tClient ip:%s\t", inet_ntoa(their_addr.sin_addr));
 printf("\tClient Port:%d\n", ntohs(their_addr.sin_port));
 num = recv(new_fd, Buffer, MAX_PATH, 0);
 Buffer[num - 1] = '\0';
 printf("Msg:%s\n", Buffer);
 closesocket(sockfd);
 closesocket(new_fd);
 WSACleanup();
 getchar();
 return 0;
}

 

//在建立 socket, accept 並且 判斷時,不要寫成了這樣:
if
(sockfd = socket(AF_INET, SOCK_STREAM, 0) == INVALID_SOCKET) { exit(0); }
if (new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size) == INVALID_SOCKET)
    {
     closesocket(sockfd);
     exit(0);
    }
// 因為這 = 優先級比 == 低,所有這樣寫會建立或返回無效的套接字。
// 用 WSAGetLastError() 會返回 10038 錯誤代碼。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM