1
0

Windows Socket 2の基礎

Last updated at Posted at 2024-08-18

アーキテクチャ

WS2_32.DLLを通じてWindows Sockets 2 APIを活用し、プロトコルスタックと通信する。

Windows ソケット 2のアーキテクチャ

winsockの使い方

基本的にはwinsock2.hws2tcpip.hをインクルードして利用する。また、#pragma comment(lib, "Ws2_32.lib")も宣言してスタティックリンクを行う。

ws2tcpip.hは接続先を指定する際に利用するInetPton関数を利用するために必要なヘッダーファイルである。

Ws2tcpip.h ヘッダー ファイルには、TCP/IP 用の WinSock 2 Protocol-Specific Annex ドキュメントで導入された定義が含まれています。これには、IP アドレスの取得に使用される新しい関数と構造体が含まれています。

また、Windows.hはロードする必要はありません。

Winsock2.h ヘッダー ファイルには内部的に Windows.h ヘッダー ファイルのコア要素が含まれているため、Winsock アプリケーションでは通常、Windows.h ヘッダー ファイルの#include行はありません。 Windows.h ヘッダー ファイルに#include行が必要な場合は、その前に #define WIN32_LEAN_AND_MEAN マクロを付ける必要があります。

#include <winsock2.h>
#include <ws2tcpip.h>

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

サーバー モデル

一般的なモデルは以下の通り

  1. Winsock を初期化します。
  2. ソケットを作成します。
  3. ソケットをバインドします。
  4. クライアントのソケットでリッスンします。
  5. クライアントからの接続を受け入れます。
  6. データの受信と送信。
  7. 切断します。

Winsockの初期化

Windowsソケットの実装に関する情報が含まれるWSADATA構造体を準備したのち、WSAStartup関数を用いてプロセスがWinsock DLLを使用できるようにする。

WSAStartup関数の戻り値はint型であり、成功した場合は0を、失敗した場合はエラーコードを返す。

WSADATA wsaData;

int iResult;

// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
    printf("WSAStartup failed: %d\n", iResult);
    return 1;
}

ソケットの作成

  1. 構造体を準備し名前解決を行う

    • ZeroMemory関数を実行してaddrinfo構造体のインスタンス化により確保された領域をすべて0で埋める
      • これを実行しないとgetaddrinfo関数はエラーを起こしてしまうため絶対に実施すること
    • getaddrinfo関数の第1引数には名前解決したいホスト名を、第2引数にはサービス名もしくはポート番号を格納する
    struct addrinfo *result = NULL, hints;
    
    ZeroMemory(&hints, sizeof (hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;
    
    // Resolve the local address and port to be used by the server
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed: %d\n", iResult);
        WSACleanup();
        return 1;
    }
    
  2. リッスン用ソケットオブジェクトの作成
    ソケット作成と統合してもよい

    SOCKET ListenSocket = INVALID_SOCKET;
    
  3. ソケットの作成

    // Create a SOCKET for the server to listen for client connections
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    

サーバーのソケットの作成

ソケットのバインド

サーバーの場合は作成したソケットをネットワークアドレス(IPアドレスとポート)にバインドする必要がある。また、バインド後はgetaddrinfo関数によって返された情報は不要になるためfreeaddrinfo関数でメモリ領域を解放する。

// Setup the TCP listening socket
iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
    printf("bind failed with error: %d\n", WSAGetLastError());
    freeaddrinfo(result);
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

ソケットでのリッスン

  • Listen関数を用いてソケットをリッスン状態にする。
  • Listen関数の第2引数は保留中の接続のキューの最大長を示している。SOMAXCONNを指定すると最大の妥当な値に設定してくれる。
if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
    printf( "Listen failed with error: %ld\n", WSAGetLastError() );
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

listen関数

接続の受け入れ

通常はパフォーマンス向上のためマルチスレッドで行う。また、複数の接続を受け付けるためにはループを用いて接続要求をチェックする。

  1. クライアントからの接続受け入れ用ソケットの作成(手順2に統合してもよい)
    SOCKET ClientSocket;
    
  2. 接続を受信したら許可する(シングルスレッド)
    ClientSocket = INVALID_SOCKET;
    
    // Accept a client socket
    ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    

サーバーでのデータの受信と送信

  • recv関数でデータを受信しデータを第2引数で指定したchar型のバッファーへ格納する。
    • エラーが発生していなければ戻り値は受信したバイト数を返す
    • 接続が正常に閉じられている場合は戻り値は0となる
  • send関数で第2引数で指定したchar型バッファーに格納されているデータを送信する。

下記コードは受信したデータをそのまま送り返すものである

// Since the maximum value of MTU is 1500 bytes
#define DEFAULT_BUFLEN 1500

char recvbuf[DEFAULT_BUFLEN];
int iResult, iSendResult;
int recvbuflen = DEFAULT_BUFLEN;

// Receive until the peer shuts down the connection
do {

    iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", iResult);

        // Echo the buffer back to the sender
        iSendResult = send(ClientSocket, recvbuf, iResult, 0);
        if (iSendResult == SOCKET_ERROR) {
            printf("send failed: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }
        printf("Bytes sent: %d\n", iSendResult);
    } else if (iResult == 0)
        printf("Connection closing...\n");
    else {
        printf("recv failed: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }

} while (iResult > 0);

また、バッファをリセットしないと受信データが前回より少ない場合は前回のデータが残ってしまうため毎回リセットするのが推奨される。

memset(recvbuf, '\0', sizeof(recvbuf));

サーバーの切断

  • shutdown関数を用いてクライアントと接続されているソケットの送信側を終了させる
  • 送信側を終了すると引き続きデータを受信できるようになるため、ソケットの受信側も終了させる
// shutdown the send half of the connection since no more data will be sent
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
    printf("shutdown failed: %d\n", WSAGetLastError());
    closesocket(ClientSocket);
    WSACleanup();
    return 1;
}

// cleanup
closesocket(ClientSocket);
WSACleanup();

return 0;

関連記事

クライアントモデル

  1. Winsock を初期化します。
  2. ソケットを作成します。
  3. サーバーに接続します。
  4. データの送受信。
  5. 切断します。

Winsockの初期化

サーバー モデルの「Winsockの初期化」と同じであるため記載省略。詳細はサーバー モデルを参照すること。

ソケットの作成

  1. 構造体を準備し名前解決を行う

    • ZeroMemory関数を実行してaddrinfo構造体のインスタンス化により確保された領域をすべて0で埋める
      • これを実行しないとgetaddrinfo関数はエラーを起こしてしまうため絶対に実施すること
    • getaddrinfo関数の第1引数には名前解決したいホスト名を、第2引数にはサービス名もしくはポート番号を格納する
    struct addrinfo *result = NULL, *ptr = NULL, hints;
    
    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    
    // Resolve the server address and port
    iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed: %d\n", iResult);
        WSACleanup();
        return 1;
    }
    
  2. リッスン用ソケットオブジェクトの作成
    ソケット作成と統合してもよい

    SOCKET ConnectSocket = INVALID_SOCKET;
    
  3. ソケットの作成

    // Attempt to connect to the first address returned by
    // the call to getaddrinfo
    ptr=result;
    
    // Create a SOCKET for connecting to server
    ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
    

ソケットへの接続

  • connect関数を用いて、作成されたソケットとgetaddrinfo関数にてパラメーターが設定されたsockaddr構造体を渡してサーバーと接続する。
  • getaddrinfo関数によって返された情報は不要になるためfreeaddrinfo関数でメモリ領域を解放する。
// Connect to server.
iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
    closesocket(ConnectSocket);
    ConnectSocket = INVALID_SOCKET;
}

// Should really try the next address returned by getaddrinfo
// if the connect call failed
// But for this simple example we just free the resources
// returned by getaddrinfo and print an error message

freeaddrinfo(result);

if (ConnectSocket == INVALID_SOCKET) {
    printf("Unable to connect to server!\n");
    WSACleanup();
    return 1;
}

なお、別のアプリケーションがソケットを経由してやり取りを行う場合はWSAconnectを用いること。
socket関数のみで行おうとする場合、非常に実装が難しく改行の扱いなどもこちら側で実装する必要があるためである。

WSAConnect 関数は、別のソケット アプリケーションへの接続を確立し、接続データを交換し、指定された FLOWSPEC 構造体に基づいて必要なサービス品質を指定します。

クライアントでのデータの送受信

  • recv関数でデータを受信しデータを第2引数で指定したchar型のバッファーへ格納する。
    • エラーが発生していなければ戻り値は受信したバイト数を返す
    • 接続が正常に閉じられている場合は戻り値は0となる
  • send関数で第2引数で指定したchar型バッファーに格納されているデータを送信する。
#define DEFAULT_BUFLEN 512

int recvbuflen = DEFAULT_BUFLEN;

const char *sendbuf = "this is a test";
char recvbuf[DEFAULT_BUFLEN];

int iResult;

// Send an initial buffer
iResult = send(ConnectSocket, sendbuf, (int) strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
    printf("send failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

printf("Bytes Sent: %ld\n", iResult);

// shutdown the connection for sending since no more data will be sent
// the client can still use the ConnectSocket for receiving data
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
    printf("shutdown failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

// Receive data until the server closes the connection
do {
    iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
    if (iResult > 0)
        printf("Bytes received: %d\n", iResult);
    else if (iResult == 0)
        printf("Connection closed\n");
    else
        printf("recv failed: %d\n", WSAGetLastError());
} while (iResult > 0);

また、バッファをリセットしないと受信データが前回より少ない場合は前回のデータが残ってしまうため毎回リセットするのが推奨される。

memset(recvbuf, '\0', sizeof(recvbuf));

クライアントの切断

  • shutdown関数を用いてサーバーと接続されているソケットの送信側を終了させる
  • 送信側を終了後、ソケットの受信側も終了させる
// shutdown the send half of the connection since no more data will be sent
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
    printf("shutdown failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

// cleanup
closesocket(ConnectSocket);
WSACleanup();

return 0;
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0