[VC]NTP鯖からSYSTEMTIME構造体で取得

VC++のソフトでNTPサーバから時刻を取得して、
システムの時刻を設定をしたいってお話。

まず色々と調べたんだけど、
「インターネット時刻設定」の画面にある
「今すぐ更新」ボタン的な処理を行うAPIが見当たらない・・・。
いや、ボタンが存在するから方法はあるとは思うけど、不明・・・

代わりに標準で入ってる「w32tm.exe」を使えば、
NTPの設定をしたり、即座に同期したり出来るコトが分かった。
最初はコレを起動して丸投げしようかと思ったんだけど、
ネットワークに未接続とかで同期に失敗しても終了コードが0で、
成功したのか失敗したのか分からなかった・・・
これは仕様的に困る。。。

で、調べ続けたけど、Windows標準のNTPを良い感じに
どうこうするのは厳しそうだったから、
それなら自力で通信して設定しちゃえ・・・!
ってことで、作ってみたー
幸いNTPの通信自体は単純で簡単だった。

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

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

struct NTP_Packet
{
	int Control_Word;
	int root_delay;
	int root_dispersion;
	int reference_identifier;
	unsigned long long reference_timestamp;
	unsigned long long originate_timestamp;
	unsigned long long receive_timestamp;
	unsigned int transmit_timestamp_seconds;
	unsigned int transmit_timestamp_fractions;
};

/*
 *	NTPサーバから現在の時刻を取得
 * @param lpSystemTime
 *	取得した日時を格納する SYSTEMTIME 構造体のポインタ
 * @param lpNtpServer
 *	取得先のサーバ
 * @param timeout
 *	タイムアウト時間(ms)
 * @param lpService
 *	接続するサービス名
 */
bool GetNtpTime(SYSTEMTIME *lpSystemTime,
		LPCTSTR lpNtpServer,
		unsigned long timeout = INFINITE,
		LPCTSTR lpService = _T("ntp"))
{
	WSADATA wsaData;
	ADDRINFOT ai, *res_ai, *res;
	SOCKET sock = INVALID_SOCKET;
	struct timeval t, *pT = nullptr;
	fd_set fds;
	NTP_Packet packet;
	bool ret = false;

	// WinSock初期化
	if (WSAStartup(WINSOCK_VERSION, &wsaData) != 0)
		return false;
	
	memset(&ai, 0, sizeof(ADDRINFOT));
	ai.ai_socktype = SOCK_DGRAM;
	ai.ai_family = PF_UNSPEC;
	
	// アドレスとサービスを変換
	if (GetAddrInfo(lpNtpServer, lpService, &ai, &res_ai) == 0)
	{
		for (res = res_ai; res != nullptr; res = res->ai_next)
		{
			// ソケット作成
			sock = socket(res->ai_family,
				res->ai_socktype,
				res->ai_protocol);
			if (sock != INVALID_SOCKET)
			{
				// 接続
				if (connect(sock, res->ai_addr,
					static_cast<int>(res->ai_addrlen)) == 0)
				{
					break;
				}
				else
				{
					closesocket(sock);
					sock = INVALID_SOCKET;
				}
			}
		}
		// addrinfo を破棄
		FreeAddrInfo(res_ai);

		if (sock != INVALID_SOCKET)
		{
			if (timeout != INFINITE)
			{
				// タイムアウトを設定
				t.tv_sec = timeout / 1000;
				t.tv_usec = (timeout % 1000) * 1000;
				pT = &t;
			}

			// ファイルディスクリプタを初期化
			FD_ZERO(&fds);
			FD_SET(sock, &fds);

			// 送信データ作成
			memset(&packet, 0, sizeof(NTP_Packet));
			packet.Control_Word = htonl(0x0B000000);
	
			// 送信
			if (send(sock, reinterpret_cast<const char *>(&packet),
						sizeof(NTP_Packet), 0) != SOCKET_ERROR)
			{
				// 受信待ち
				if (select(0, &fds, nullptr, nullptr, pT) > 0)
				{
					// 受信
					int recvLen;
					recvLen = recv(sock, reinterpret_cast<char *>(&packet),
								sizeof(NTP_Packet), 0);
					if (recvLen != SOCKET_ERROR ||
						recvLen == sizeof(NTP_Packet))
					{
						// 固定小数点数を浮動小数点数へ変換
						unsigned int f = ntohl(packet.transmit_timestamp_fractions);
						double frac = 0.0f, d = 0.5f;
						for (int i = sizeof(unsigned int) * 8 - 1; i >= 0; --i, d /= 2.0f)
							if (f & (1 << i)) frac += d;

						FILETIME ft;

						// 100ナノ秒単位へ変換、1900年~を1601年~へ変換
						*reinterpret_cast<unsigned long long*>(&ft) = 
							UInt32x32To64(ntohl(packet.transmit_timestamp_seconds), 10000000U) +
							static_cast<unsigned long long>(frac * 10000000U) + 94354848000000000U;

						// FILETIME構造体からSYSTEMTIME構造体へ変換
						if (FileTimeToSystemTime(&ft, lpSystemTime))
							ret = true;
					}
				}
			}

			// 切断
			shutdown(sock, SD_BOTH);
			closesocket(sock);
		}
	}

	// WinSock破棄
	WSACleanup();

	return ret;
}

関数1個にまとめる為にWinSockの初期化とか接続処理を詰め込んで、
えらく段差の多い関数になちゃってるけど、
クラスにしてコンストラクタでWinSockの初期化をしたり、
Connect関数を用意したり処理を分ければ綺麗かも。
少し改造すればキャンセル出来るようにしたり、非同期にしたりも・・・

そもそもUDP通信だからconnectとかせずにsendto,recvfromで
通信すれば良いのかもしれないけど、
NTPって仕様的にはTCP/UDPを選ばないみたいだから、
↑のような処理にしておけばSOCK_STREAMに変えるだけで
TCPにも対応出来るかなー?っと。
TCPを受け付けてるNTPサーバが見当たらず未確認。

特に面倒だったのがNTPのタイムスタンプから
時計設定に必要なSYSTEMTIME構造体への変換・・・

time_t型へ変換するサンプルは見かけたんだけど、
time_t型 → FILETIME構造体 → SYSTEMTIME構造体
と回りくどい上に、取得した元データには秒未満のデータも
入ってるのに、time_t型へ変換するときに捨てちゃうのもなーっと。

どうせならナノ秒まで入るFILETIME構造体へ直接変換したい!
タイムスタンプとFILETIME構造体の違いを調べたところ、
タイムスタンプは、1900/1/1~の秒単位で
上位32bitが整数部、32bitが小数部の固定小数点数、
FILETIME構造体は、1601/1/1~の100ナノ秒単位で64bit整数。
それを踏まえて地道変換を。

まず固定小数点数をCで扱うのが超面倒・・・!
もっと綺麗に変換する方法とかあるのかなぁ。

あと100ナノ秒単位で299年もずらすとなると、
途方もない数字がソース上に登場することに^^;
いやー64bitって凄い・・・!

で、↑の関数で取得したSYSTEMTIME構造体を使って、
SetSystemTime関数を呼び出せば設定出来る!

もしローカルのタイムゾーンで取得したければ、
FILETIME構造体からSYSTEMTIME構造体へ変換する前に、
FileTimeToLocalFileTime関数で変換しておけばおk。

それと秒未満を気にしといてアレなんだけど、
そこまで精度を求めてないから通信の遅延とか無視(苦笑)
Wikipediaを見てると、ある程度の遅延時間を求められそうなんだけど・・・

その辺りのことを調べてるときに知ったんだけど、
Unixのntpdとかって単に合わせてるだけじゃなくて、
システムの影響を考慮して1秒の進む間隔を微妙に変化させて
徐々に合わせるとか凄いことやってるんだねぇ。
流石にそこまでは面倒だから考えるのを止めた^^;
でもSetSystemTimeAdjustment関数を使えば、
Windowsでも出来るっぽい・・・?

じゃ、ゲームして寝るー
バイニー☆

test?

コメントを残す