1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ping もどきを書いた (Windows)

Posted at

はじめに

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の値か何かをうまく使ったラッパーでも用意するといいかもしれません

最後に

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?