はじめに
WEB+〇B PRESS の "Web ページが表示されるまで" という記事をみて、ping もどきならさほど時間をかけずに書けそうかもなどと思いWindows 向けに実装してみることにしました。
あえて Windows でやってみるひねくれものは自分しかいないような気もするが気にしない。
コード全体
概要
- Winsock 初期化
- socket 取得
- ICMP パケット作成
- 送信
- 受信
- 出力
- Winsock 後処理
詳細
Winsock 初期化
::WSAStartup(MAKEWORD(2, 2), &wsa_data);
- Winsock を使うための準備です。version は 2.2 を使います
socket 取得
SOCKET sock = ::WSASocketW(
AF_INET, // int af
SOCK_RAW, // int type
IPPROTO_ICMP, // int protocol
nullptr, // LPWSAPROTOCOL_INFO lpProtocolInfo
0, // GROUP g
WSA_FLAG_OVERLAPPED // DWORD dwFlags
);
- WSASocket() で socket を取得します
socket()
に対応する処理です - Winsock では SOCK_DGRAM と IPPROTO_ICMP を指定すると INVALID_SOCKET が返ってきてしまいました。WSAEPROTONOSUPPORT だそうです。そうですか。代わりに SOCK_RAW を指定します
- 受信の際にキャンセル的な処理を行いたいので OVERLAPPED フラグを指定します
ICMP パケット作成
- 構造体は以下のように定義しました
enum ICMPType : uint8_t { ECHO_REPLY = 0, ECHO_REQUEST = 8 };
#pragma pack(push,1)
typedef struct ICMPMessage {
ICMPType type;
uint8_t code;
uint16_t checksum;
uint16_t identifire;
uint16_t sequence;
} ICPMessage;
#pragma pack(pop)
-
#pragma pack
でアライメントを変更しています。構造体を何かのプロトコルに対応させる場合はアライメントに注意しましょう
ICMPMessage icmp = {};
icmp.type = ICMPType::ECHO_REQUEST;
icmp.checksum = CalcChecksum((uint16_t*)&icmp, sizeof(icmp));
CalcChecksum
uint16_t CalcChecksum(uint16_t* message, size_t size) {
uint16_t sum = 0;
while (1 < size) {
uint16_t temp = sum + *message++;
if (temp < sum) {
temp += 1;
}
sum = temp;
size -= 2;
}
if (size) {
uint16_t temp = sum + *(uint8_t*)message;
if (temp < sum) {
temp += 1;
}
sum = temp;
}
return ~sum;
}
- 1の補数和 って何だっけとなったのですが、左の桁への桁上がりを一番右に足せばいいと。でなんだか頭の悪そうなコードですが、計算としてはあっていると思われるのでこれでいい事にします
- sum をもっと大きな型にしておいて、単純に加算した後まとめて16bitより上のデータを足しこむテクニックが一般的な様ですので賢い人はそちらを使いましょう
送信
WSABUF buffer = { 0 };
buffer.buf = (CHAR*)&icmp;
buffer.len = sizeof(icmp);
DWORD bytes_sent = 0;
::WSASendTo(
sock, // SOCKET s
&buffer, // LPWSABUF lpBuffers
1, // DWORD dwBufferCount
&bytes_sent, // LPDWORD lpNumberOfByteSent
0, // DWORD dwFlags
(sockaddr*)&to_addr, // const sockaddr *lpTo
sizeof(to_addr), // int iTolen
nullptr, // LPWSAOVERLAPPED lpOverlapped
nullptr // LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
- 先に取得したsocketを使って WSASendTo で送信します。
sendto()
に対応する処理です - データは WSABUF を通して渡してやる必要があります。ちょっと面倒です
受信
std::array<byte, 256> b{};
WSABUF recv_buf = { 0 };
recv_buf.buf = (char*)b.data();
recv_buf.len = b.size();
sockaddr_in from = {};
int from_len = sizeof(from);
DWORD recv_len = 0;
DWORD flags = 0;
::WSARecvFrom(
sock, // SOCKET s
&recv_buf, // LPWSABUF lpBuffers
1, // DWORD dwBufferCount
&recv_len, // LPDWORD lpNumberObBytesRecvd
&flags, // LPDWORD lpFlags
(sockaddr*)&from, // sockaddr *lpFrom
&from_len, // LPINT lpFromlen
&overlapped, // LPWSAOVERLAPPED lpOverlapped
nullptr // LPWSAOVERLAPPED lpCompletionRoutine
);
- WSARecvFrom でICMP応答を受け取ります。
recvfrom()
に対応する処理です - overlapped 構造体を指定する事でノンブロッキングな処理になります (のはずです)
- 送信と同様に受信するデータは WSABUF を経由して受け取ります。面倒です
DWORD wait_res = ::WaitForMultipleObjects(2, handles, FALSE, 10000);
- ブロッキングしない受信処理なので、イベントを使って受信を待ちます
- タイムアウトもこれで実装したと言えますね (雑)
出力
ICMPMessage* result = (ICMPMessage*)&b[IP_HEADER_LENGTH];
char buf[INET_ADDRSTRLEN];
PCSTR ntop_res = ::InetNtopA(AF_INET, &from.sin_addr.s_addr, buf, sizeof(buf));
...
printf("receive from %s\n", buf);
- 受信処理の結果を最後に出力します
- SOCK_RAW を使ったので IPヘッダがついています (たぶん)。本当はヘッダの長さを見ないといけない気がしますが、とりあえず手元ではこれで問題なさそうだったので固定値で OK としましょう。
他
- main の冒頭で イベントハンドルと
SetConsoleCtrlHandler
を使って ctrl-c による中断を実装しています。 signal でもいいですが、こちらの方が Windows らしさが出ます(?) -
WaitForMultipleObjects
はちょっと読みづらいですよね。HANDLEの値か何かをうまく使ったラッパーでも用意するといいかもしれません
最後に
- Windows (Winsock) でもほぼ同じような流れで ping が実装出来ましたが、少しこちらの方が面倒な印象でしょうか
- 個人的には Windows のイベントハンドルの仕組みは簡単に書けて便利なので好きです
- 途中で MS の ping の サンプル実装を見つけました。少し前のものの様ですが参考にするならこちらの方が確実でしょうか https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/netds/winsock/ping/Ping.cpp