2014年2月19日 星期三

C++ Socket 資料整理

看了一些 C++ Socket 的範例、教學,但對某些函式的用法,還是不甚熟悉,所以做個整理。

Server 端執行流程:
link Winsock Library(windows環境下) -> 初始化 Windows Sockets DLL(windows環境下) -> 使用 socket() 建立 socket descriptor -> 設定 Server 位址資訊 -> 設定 listen() -> 等待連線 -> 接受連線 accect() -> 傳送接收資料

Client 端執行流程:
link Winsock Library(windows環境下) -> 初始化 Windows Sockets DLL(windows環境下) -> 使用 socket() 建立 socket descriptor -> 設定 Server 位址資訊 -> 連線到 Server 端 connect() -> 傳送接收資料

測試環境 Visual Studio Express 2012
  1. 在 Windows 使用 Socket 需要 link Winsock Library。[server端] [client端]
    link方式:
    • 專案的「屬性」->「組態屬性」->「連結器」->「輸入」->「其他相依性」加入 wsock32.lib 或  Ws2_32.lib
    • 也可以在程式中,使用以下方式加入
      #pragma comment(lib, "wsock32.lib") 或 #pragma comment(lib, "Ws2_32.lib")
    wsock32.lib 和 Ws2_32.lib 的區別:
    • wsock32.lib 是較舊的 1.1 版本,Ws2_32.lib 是較新的 2.0 版本。
    • wsock32.lib 跟 winsock.h 一起使用,Ws2_32.lib 跟 WinSock2.h 一起使用。
    • winsock.h 和 WinSock2.h 不能同時使用,WinSock2.h 是用來取代 winsock.h,而不是擴展 winsock.h。
  2. 初始化 Windows Sockets DLL。[server端] [client端]
    範例:
    WSAData wsaData;
    WORD version = MAKEWORD(2, 2); // 版本
    int iResult = WSAStartup(MAKEWORD(2,2), &wsaData); // 成功回傳 0
    if (iResult != 0) {
        // 初始化Winsock 失敗
    }
  3. 建立 socket 描述符 (socket descriptor)。[server端] [client端]
    函式:SOCKET socket( int af, int type, int protocol);
    int af:使用何種通訊家族。
    • 例如
      AF_INET:使用 IPv4
      AF_INET6:使用 IPv6
    int type:The type specification for the new socket.
    • 能用的值跟第 1 個參數有關
    • 例如
      SOCK_STREAM:使用 TCP 協議
      SOCK_DGRAM:使用 UDP 協議
    int protocol:The protocol to be used.
    • 能用的值跟前面兩個參數有關
    • 例如
      IPPROTO_TCP:使用 TCP
      IPPROTO_UDP:使用 UDP
    成功回傳 socket descriptor,失敗回傳 INVALID_SOCKET
    範例:
    SOCKET sListen = INVALID_SOCKET;
    sListen= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sListen== INVALID_SOCKET) {
        // 建立失敗
    }
  4. 設定位址資訊的資料 (SOCKADDR_IN)。[server端] [client端]
    結構:使用 IP4 格式結構 struct sockaddr_in (in 表示 internet) 設定 internet 位址資訊。
    範例:
    SOCKADDR_IN addr;
    memset (&addr, 0, sizeof (addr)) ; // 清空,將資料設為 0
    addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 設定 IP,後面解釋 inet_addr()
    // addr.sin_addr.s_addr = INADDR_ANY; // 若設定 INADDR_ANY 表示任何 IP
    addr.sin_family = AF_INET;
    addr.sin_port = htons(1234); // 設定 port,後面解釋 htons()
    • struct sockaddr_in:IP4 格式使用
      struct sockaddr_in6:IP6 格式使用
      struct sockaddr:通用格式
      struct sockaddr_un:UNIX domain 格式
    • htons()
      使用 htons() 是因為網路位元順序 (Network Byte Order, 縮寫NBO) 可能跟電腦主機位元順序(Host Byte Order, 縮寫HBO)不同,所以需要要函式來轉換,以增加移植性。
      htons():"h(host)"  "to"  "n(network)" "s(short)",Host to Network Short,將 short (2byte) 資料順序從 host 轉換至 network。
      htonl():Host to Network Long,將  long(4byte) 資料順序從 host 轉換至 network。
      ntohs():Network to Host Short,將 short (2byte) 資料順序從 network 轉換至 host。
      ntohl():Network to Host Long,將 long(4byte) 資料順序從 network 轉換至 host。
    • inet_addr()
      將 IP 轉換為 unsigned long 格式,轉換後,已是網路位元順序 (Network Byte Order,所以不用再使用 htons()
  5. 綁定 socket 的位址資料 (bind)。[server端]
    函式:int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
    • int sockfd:socket() 函式回傳的 socket descriptor
    • struct sockaddr *my_addr:用來通訊的位址資料(IP、PORT)
    • int addrlen:位址長度 ,sizeof(my_addr)
    • 綁定成功回傳 0,失敗回傳 SOCKET_ERROR
    範例:
    // addr 為 sockaddr_in 結構,強制轉型為 sockaddr 結構
    int r = bind(sListen, (SOCKADDR*)&addr, sizeof(addr));
    assert (r != SOCKET_ERROR);
  6. 監聽連線 (listen)。[server端]
    函式:int listen(SOCKET s,  int backlog)
    • SOCKET s:socket() 函式回傳的 socket descriptor。
    • int backlog:最大可監聽多少連線(佇列、排隊)。設定 SOMAXCONN 表示系統最大值。
    • 成功回傳 0,失敗回傳 SOCKET_ERROR。
    • connection-oriented ( SOCK_STREAM ) 的 server 程式端程式才使用。
    範例:
    // addr 為 sockaddr_in 結構,強制轉型為 sockaddr 結構
    int r = listen(sListen, SOMAXCONN);
    assert (r != SOCKET_ERROR);
  7. 處理連線 (accept)。[server端]
    函式:SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen)
    • SOCKET s:socket() 函式回傳的 socket descriptor。
    • struct sockaddr *addr:結果參數,預先配置 SOCKADDR 結構的指標,用來存放客戶端位址,不存放可設置為 NULL。
    • int *addrlen:結果參數,第二個參數的大小,不存放也可以設為 NULL。
    • 成功回傳 socket descriptor,失敗回傳 INVALID_SOCKET,可用 WSAGetLastError() 取得 error code。
      所以成功時,會產生一個新的 socket descriptor,而原本的 socket descriptor 持續監聽原來的端口。
    • connection-oriented ( SOCK_STREAM ) 的 server 程式端程式才使用。
    範例:
    SOCKET sConnect;
    struct sockaddr_in clientAddr; // client 端位址資訊
    int clientAddrLen = sizeof(clientAddr);
    sConnect = accept(sListen, (SOCKADDR*)&clientAddr, &clientAddrLen);
    // sConnect = accept(sListen, NULL, NULL);
    if (sConnect != INVALID_SOCKET)
    {
        // 有 client 端成功連線過來
        printf("server: got connection from %s", inet_ntoa(clientAddr.sin_addr)); 
    }
  8. 連線到 socket Server。[client端]
    函式:int connect(SOCKET s, const struct sockaddr *name, int namelen)
    • SOCKET s:socket() 函式回傳的 socket descriptor。
    • const struct sockaddr *name:Server 端的位址資料。
    • int namelen:第二個參數的大小。
    • 成功回傳 0,失敗回傳 SOCKET_ERROR,可用 WSAGetLastError() 取得 error code。
    範例:
    int r = connect(sConnect, (SOCKADDR*)&addr, sizeof(addr));
    if(r != SOCKET_ERROR){
        // 連線成功
    }
  9. 傳送訊息。[server端] [client端]
    函式 1:int send(SOCKET s, const char *buf, int len, int flags)
    函式 2:int sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen)
    • send() 和 sendto() 的差異在於,send() 只能用在 connection-oriented ( SOCK_STREAM ) 的連線,所以 sendto() 比 send() 多最後兩個參數,用來指定目的地的位址資訊。
    • SOCKET s:socket() 函式回傳的 socket descriptor。
    • const char *buf:訊息的指標。
    • int len:訊息的長度。
    • int flags:The flags parameter can be used to influence the behavior of the function beyond the options specified for the associated socket。(一般設 0)
    MSG_DONTROUTE:不將訊息送給 gateway,而直接送給 host。(Specifies that the data should not be subject to routing. A Windows Sockets service provider can choose to ignore this flag.)
    MSG_OOB:Sends OOB data (stream-style socket such as SOCK_STREAM only)。
    • const struct sockaddr *to:目的地位址資訊。
    • int tolen:目的地位址資訊的大小。
    • 成功回傳傳送的資料長度,失敗回傳 SOCKET_ERROR,可用 WSAGetLastError() 取得 error code。
    範例:
    char *sendbuf = "sending data test";
    send(sConnect, sendbuf, (int)strlen(sendbuf), 0);
  10. 接收訊息。[server端] [client端]
    函式 1:int recv(SOCKET s, char *buf, int len, int flags)
    函式 2:int recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen)
    • recv() 和 recvfrom() 的差異在於,recv() 只能用在 connection-oriented ( SOCK_STREAM ) 的連線,所以 recvfrom() 比 recv() 多最後兩個參數,用來指定接收來源的位址資訊。
    • SOCKET s:socket() 函式回傳的 socket descriptor。
    • const char *buf:訊息的指標。
    • int len:訊息的長度。
    • int flags:The flags parameter can be used to influence the behavior of the function invocation beyond the options specified for the associated socket.。(一般設 0)
    MSG_PEEK:Peeks at the incoming data。只看訊息內容,但不將訊息從 queue 移除。
    MSG_OOB:Processes Out Of Band (OOB) data。
    • const struct sockaddr *to:目的地位址資訊。
    • int tolen:目的地位址資訊的大小。
    • 成功回傳接收的資料長度,連線被關閉回傳 0。失敗回傳 SOCKET_ERROR,可用 WSAGetLastError() 取得 error code。
    範例:
    char message[200];
    ZeroMemory(message, 200);
    recv(sConnect, message, sizeof(message), 0);
  11. 設定 socket option 選項。[server端] [client端]
    函式 :int setsockopt(SOCKET s, int level, int optname, const char *optval, int optlen)
    用來設定建立的  socket 一些特性,例如是否強制關閉等。
    使用範例可參考:setsockopt 设置socket - 驱动开发 - 博客频道 - CSDN.NET
  12. 關閉 socket。[server端] [client端]
    函式 :int closesocket(SOCKET s)


Scoket Server 範例:
執行後會等待 client 端連線,有連線進來時,則傳送 "sending data test" 訊息給 client 端。
#pragma comment(lib, "Ws2_32.lib")

#include <WinSock2.h>
#include <iostream>

using namespace std;

void main()
{
    int r;
    WSAData wsaData;
    WORD DLLVSERION;
    DLLVSERION = MAKEWORD(2,1);//Winsocket-DLL 版本

    //用 WSAStartup 開始 Winsocket-DLL
    r = WSAStartup(DLLVSERION, &wsaData);

    //宣告 socket 位址資訊(不同的通訊,有不同的位址資訊,所以會有不同的資料結構存放這些位址資訊)
    SOCKADDR_IN addr;
    int addrlen = sizeof(addr);

    //建立 socket
    SOCKET sListen; //listening for an incoming connection
    SOCKET sConnect; //operating if a connection was found

    //AF_INET:表示建立的 socket 屬於 internet family
    //SOCK_STREAM:表示建立的 socket 是 connection-oriented socket 
    sConnect = socket(AF_INET, SOCK_STREAM, NULL);

    //設定位址資訊的資料
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_family = AF_INET;
    addr.sin_port = htons(1234);

    //設定 Listen
    sListen = socket(AF_INET, SOCK_STREAM, NULL);
    bind(sListen, (SOCKADDR*)&addr, sizeof(addr));
    listen(sListen, SOMAXCONN);//SOMAXCONN: listening without any limit

    //等待連線
    SOCKADDR_IN clinetAddr;
    while(true)
    {
        cout << "waiting..." << endl;

        if(sConnect = accept(sListen, (SOCKADDR*)&clinetAddr, &addrlen))
        {
            cout << "a connection was found" << endl;
            printf("server: got connection from %s\n", inet_ntoa(addr.sin_addr));

            //傳送訊息給 client 端
            char *sendbuf = "sending data test";
            send(sConnect, sendbuf, (int)strlen(sendbuf), 0);
            
        }
    }

    //getchar();
}

Socket Client 範例:
執行後,按 "Y" 連線到 Server 端,並接收 Server 端傳過來的訊息。
#pragma comment(lib, "Ws2_32.lib")

#include <WinSock2.h>
#include <iostream>
#include <string>

using namespace std;

void main()
{
    string confirm;
    char message[200];

    //開始 Winsock-DLL
    int r;
    WSAData wsaData;
    WORD DLLVersion;
    DLLVersion = MAKEWORD(2,1);
    r = WSAStartup(DLLVersion, &wsaData);

    //宣告給 socket 使用的 sockadder_in 結構
    SOCKADDR_IN addr;

    int addlen = sizeof(addr);

    //設定 socket
    SOCKET sConnect;

    //AF_INET: internet-family
    //SOCKET_STREAM: connection-oriented socket
    sConnect = socket(AF_INET, SOCK_STREAM, NULL);

    //設定 addr 資料
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_family = AF_INET;
    addr.sin_port = htons(1234);

    cout << "connect to server?[Y] or [N]" << endl;
    cin >> confirm;

    if(confirm == "N")
    {
        exit(1);
    }else{
        if(confirm == "Y")
        {
            connect(sConnect, (SOCKADDR*)&addr, sizeof(addr));

            //接收 server 端的訊息
            ZeroMemory(message, 200);
            r = recv(sConnect, message, sizeof(message), 0);
            cout << message << endl;

            //設定 closesocket 時,不經過 TIME-WAIT 過程,直接關閉socket
            //BOOL bDontLinger = FALSE;
            //setsockopt(sConnect,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
            
            //若之後不再使用,可用 closesocket 關閉連線
            closesocket(sConnect);
            
            getchar();
            getchar();
        }
    }

}


其他:
執行範例時,可另開一個指令視窗,執行以下指令,觀察每1秒鐘 1234 port 的監聽、連線狀態
netstat -na -p TCP 1 | find ":1234"

參考:
WinSock.h & WinSock2.h which to use?
Socket中winsock.h和winsock2.h的不同
http://en.wikipedia.org/wiki/Winsock
Hook MSN Messenger之socket通訊的鳥事

MSDN WSAStartup function
MSDN socket function
【網络編程基礎筆記】struct sockaddr和struct sockaddr_in的區別和用法
整理:Linux網络編程之sockaddr與sockaddr_in,sockaddr_un結構體詳細講解
SOCKADDR 結構
htonl() htons()及inet_ntoa() inet_addr()的用法
【轉】struct sockaddr與struct sockaddr_in的區別和聯繫
MSDN sockaddr_in
MSDN inet_addr function
MSDN bind function
MSDN listen function
MSDN accept function
網絡編程socket之accept函數
MSDN connect function
MSDN send function
MSDN sendto function
MSDN recv function
MSDN recvfrom function
MSDN setsockopt function
MSDN closesocket function
Beej網絡socket編程指南
SOCKET筆記~
Socket 程式設計
MSDN Complete Winsock Server Code
MSDN Complete Winsock Client Code
C++ Server & Client Tutorial pt.1
C++ Server & Client Tutorial pt.2
C++ Chat Application
輕描淡寫的低調: TCP TIME_WAIT的釋義
傳輸控制協定 - 維基百科,自由的百科全書



4 則留言:

  1. 你好,我是一個C++的新手,看了你的文章受益良多,謝謝你的分享。
    但我想請問,我是使用visual studio2013,在使用你的範例程式時,addr.s_addr那一行會噴錯,所以我改成使用inet_Pton。
    但改完之後沒有噴任何錯誤,就是連線不上...
    我有去查port的使用狀況,port1234是未被使用的。
    想請問是哪個環節出錯了,謝謝!

    回覆刪除
    回覆
    1. 您好,我手邊沒有Visual Studio 2013,所以下載最新的Visual Studio Community 2017安裝測試。
      1.
      addr.sin_addr.s_addr = inet_addr("127.0.0.1");
      改寫成
      inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);


      2.
      printf("server: got connection from %s\n", inet_ntoa(addr.sin_addr));
      改寫成
      char ip4[INET_ADDRSTRLEN];
      while (true)
      {
      ....
      inet_ntop(AF_INET, &addr.sin_addr, ip4, INET_ADDRSTRLEN);
      printf("server: got connection from %s\n", ip4);
      ....
      }

      以上修改後可正常執行,若您可正常編譯,建議可檢查是否系統防火牆擋住了。

      刪除
  2. 你好,以下有幾個問題想要問:
    1.請問如果我要在兩台電腦上分別執行server及client端,IP部分該如何設置?
    2.請問有辦法直接將client.exe檔移植到另一台電腦上執行嗎?
    3.若是要跨平台使用的話,需要做些什麼樣的更動嗎?

    回覆刪除
    回覆
    1. 您好:
      1.
      client程式中的IP,表示要連接到哪一台Server。
      server程式中的IP,表示Server自己所有IP中,所要監聽的那一個IP。

      所以,
      若兩者在同一個區網,可直接設定Server在區網的IP。
      若client在外網,client要連的IP變成是Server的對外IP,但此時還須確定外網IP設定,有將使用到的port對應到server。

      2.
      電腦環境一樣,client.exe就可執行

      3.
      我沒實作過跨平台,所以有什麼要注意的細節,不敢給您肯定的答案。
      建議您可參考
      http://www.programmer-club.com.tw/showsametitlen/vc/28140.html
      裡面有對幾種不同平台進行相對應的處理。

      刪除