アーキテクチャ
WS2_32.DLL
を通じてWindows Sockets 2 API
を活用し、プロトコルスタックと通信する。
winsockの使い方
基本的にはwinsock2.h
とws2tcpip.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")
サーバー モデル
一般的なモデルは以下の通り
- Winsock を初期化します。
- ソケットを作成します。
- ソケットをバインドします。
- クライアントのソケットでリッスンします。
- クライアントからの接続を受け入れます。
- データの受信と送信。
- 切断します。
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;
}
ソケットの作成
-
構造体を準備し名前解決を行う
-
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; }
-
-
リッスン用ソケットオブジェクトの作成
ソケット作成と統合してもよいSOCKET ListenSocket = INVALID_SOCKET;
-
ソケットの作成
// 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;
}
接続の受け入れ
通常はパフォーマンス向上のためマルチスレッドで行う。また、複数の接続を受け付けるためにはループを用いて接続要求をチェックする。
- クライアントからの接続受け入れ用ソケットの作成(手順2に統合してもよい)
SOCKET ClientSocket;
- 接続を受信したら許可する(シングルスレッド)
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;
関連記事
クライアントモデル
- Winsock を初期化します。
- ソケットを作成します。
- サーバーに接続します。
- データの送受信。
- 切断します。
Winsockの初期化
サーバー モデルの「Winsockの初期化」と同じであるため記載省略。詳細はサーバー モデルを参照すること。
ソケットの作成
-
構造体を準備し名前解決を行う
-
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; }
-
-
リッスン用ソケットオブジェクトの作成
ソケット作成と統合してもよいSOCKET ConnectSocket = INVALID_SOCKET;
-
ソケットの作成
// 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;