Internet Programming

SNTPクライアントの作成(非同期型)

[1997/12/01]

実際に非同期APIを使用したUDPクライアントを作成する例として、SNTPクライアントを作成してみます。
SNTPは、123番ポートを使用してNTPサーバから時刻を取得するプロトコルです。
SNTP(Simple Network Time Protocol)についての詳細は、RFC 1769を参照してください。

SNTPクライアントの流れは、
  1. ソケットの作成を行う。

  2. 受信ポートを指定する。

  3. NTPサーバにパケットを送信する。

  4. NTPサーバから時刻の情報を受信する。

  5. ソケットを破棄する。
という感じになります。
(上記はNTPサーバにリクエストを出して時刻情報を受信するパターンです)
ブロードキャストしている時刻情報を取得するには、「3. NTPサーバにパケットを送信する。」を外します。

以下のソースは、サンプル中の実際にNTPサーバから時刻情報を取得する部分です。

このサンプルでは、タイムアウトを行うのにタイマーを使っています。

#define WSOCK_GETHOST	WM_USER + 1	/* ホスト情報のイベントを通知するメッセージ */
#define WSOCK_SELECT	WM_USER + 2	/* ソケットイベントを通知するメッセージ */

/*---------------------------------------------------
	ソケットイベントの通知を設定する関数
---------------------------------------------------*/
BOOL SocketSelect(int sSoc,HWND hWnd)
{
	/* 受信のソケットイベントを通知させるように設定する */
	/* ソケットイベントは、ウィンドウにWSOCK_SELECTメッセージで通知されるように設定する */
	if(WSAAsyncSelect(sSoc, hWnd, WSOCK_SELECT, FD_READ) == SOCKET_ERROR){
		return FALSE;
	}
	return TRUE;
}

/*---------------------------------------------------
	サーバにアドレスにIPアドレスとポート番号を設定する
---------------------------------------------------*/
void SetSockaddr(unsigned long cIPaddr,int cPort)
{

	/* サーバのアドレスの構造体にサーバのIPアドレスとポート番号を設定します */
	serversockaddr.sin_family	= AF_INET;
	serversockaddr.sin_addr.s_addr = cIPaddr;					/* IPアドレス */
	serversockaddr.sin_port		= htons((unsigned short)cPort);		/* ポート番号 */
	memset(serversockaddr.sin_zero,(int)0,sizeof(serversockaddr.sin_zero));
}

/*---------------------------------------------------
	NTPパケットを送信する
---------------------------------------------------*/
BOOL SendSNTPPacket(int sSoc)
{
	struct NTP_Packet	NTP_Send;		/* 送信するNTPパケット */

	/* NTPパケットをSNTP用に初期化する */
	NTP_Send.Control_Word = htonl(0x0B000000);
	NTP_Send.root_delay = 0;
	NTP_Send.root_dispersion = 0;
	NTP_Send.reference_identifier = 0;
	NTP_Send.reference_timestamp = 0;
	NTP_Send.originate_timestamp = 0;
	NTP_Send.receive_timestamp = 0;
	NTP_Send.transmit_timestamp_seconds = 0;
	NTP_Send.transmit_timestamp_fractions = 0;

	/* サーバを指定してNTPパケットを送信する */
	if(sendto(sSoc,(const char *)&NTP_Send, sizeof( NTP_Send ),0,(struct sockaddr *)&serversockaddr,sizeof(serversockaddr)) == SOCKET_ERROR){
		return FALSE;
	}
	return TRUE;
}

/*---------------------------------------------------
	ソケットを破棄する関数
---------------------------------------------------*/
void SocketClose(HWND hWnd)
{
	/* タイムアウト用のタイマーを破棄する */
	KillTimer(hWnd,TIMER_ID);

	/* WSAAsyncGetHostByName関数の非同期なタスクがある場合はキャンセルする */
	if(hGetHost != NULL){
		WSACancelAsyncRequest(hGetHost);
		hGetHost = NULL;
	}
	/* ソケットイベントの通知を取り消す */
	WSAAsyncSelect(soc, hWnd, 0,0);
	/* ソケットを破棄する */
	closesocket(soc);
	soc = -1;

	if(sync_flag == FALSE){
		SetWindowText(hBTNGET,"GET");
	}else{
		SetWindowText(hBTNSYNC,"SYNC");
	}
}

/*---------------------------------------------------
	ソケットのエラー処理関数
---------------------------------------------------*/
void SocketError(HWND hWnd,char *msgbuf)
{
	/* ソケットを破棄する */
	SocketClose(hWnd);

	/* エラーメッセージ */
	MessageBox(hWnd,msgbuf,"Error",MB_OK | MB_ICONERROR);

}

/*---------------------------------------------------
	SNTPセッションの開始
---------------------------------------------------*/
void SNTPStart(HWND hWnd,char *buf)
{
	unsigned long serveraddr;		/* サーバのIPアドレス */

	/* 入力文字からサーバ名とパスを取得する */
	Port = GetServerPort(buf,SvName);
	if(Port == -1){
		MessageBox(hWnd, "サーバ名が入力されていません。", "Error", MB_OK | MB_ICONERROR);
		if(sync_flag == FALSE){
			SetWindowText(hBTNGET,"GET");
		}else{
			SetWindowText(hBTNSYNC,"SYNC");
		}
		return;
	}
	/* socにソケットを作成します */
	soc = socket(PF_INET, SOCK_DGRAM, 0);
	if(soc == INVALID_SOCKET){
		MessageBox(hWnd, "Socketの作成に失敗しました。", "Error", MB_OK | MB_ICONERROR);
		soc = -1;
		if(sync_flag == FALSE){
			SetWindowText(hBTNGET,"GET");
		}else{
			SetWindowText(hBTNSYNC,"SYNC");
		}
		return;
	}

	/* svNameにドットで区切った10進数のIPアドレスが入っている場合、serveraddrに32bit整数のIPアドレスが返ります */
	serveraddr = inet_addr((char*)SvName);
	if(serveraddr == -1){
		/* サーバ名からサーバのホスト情報を取得する */
		/* ホスト情報が取得できると、ウィンドウにWSOCK_GETHOSTが通知されるようにする */
		hGetHost = WSAAsyncGetHostByName(hWnd,WSOCK_GETHOST,SvName,gHostEnt, MAXGETHOSTSTRUCT);
		if(hGetHost == 0){
			SocketError(hWnd,"サーバのIPアドレスの取得に失敗しました。");
			return;
		}
		return;
	}
	/* 接続、受信、切断のイベントをウィンドウメッセージで通知されるようにする */
	if(SocketSelect(soc, hWnd) == FALSE){
		SocketError(hWnd,"Socketイベントを通知させる設定に失敗しました。");
		return;
	}

	SetSockaddr(serveraddr,Port);

	if(SendSNTPPacket(soc) == FALSE){
		SocketError(hWnd,"送信に失敗しました。");
		return;
	}

	/* タイムアウト用のタイマーをセットする */
	SetTimer(hWnd, TIMER_ID, 3000, NULL);

}

/*---------------------------------------------------
	IPアドレスを取得してデータを送信する
---------------------------------------------------*/
void GetHostToIPaddr(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
	struct hostent FAR *HostEntry;	/* ホスト情報 */
	unsigned long serveraddr;		/* サーバのIPアドレス */

	/* エラーの判定 */
	if(WSAGETASYNCERROR(lParam) != 0){
		SocketError(hWnd,"サーバのIPアドレスの取得に失敗しました。");
		return;
	}
	/* WSAAsyncGetHostByNameの戻り値である非同期なタスクのハンドルか判定*/
	if(hGetHost != (HANDLE)wParam){
		return;
	}
	HostEntry = (struct hostent FAR *)gHostEnt;
	serveraddr =  *((unsigned long *)((HostEntry->h_addr_list)[0]));	/* IPアドレスを取得する */

	hGetHost = NULL;

	/* ソケットイベントをウィンドウメッセージで通知されるようにする */
	if(SocketSelect(soc, hWnd) == FALSE){
		SocketError(hWnd,"Socketイベントを通知させる設定に失敗しました。");
		return;
	}

	SetSockaddr(serveraddr,Port);

	if(SendSNTPPacket(soc) == FALSE){
		SocketError(hWnd,"送信に失敗しました。");
		return;
	}

	/* タイムアウト用のタイマーをセットする */
	SetTimer(hWnd, TIMER_ID, 3000, NULL);

}

/*---------------------------------------------------
	SNTPのデータを受信する
---------------------------------------------------*/
void SNTPRecvData(HWND hWnd)
{
	struct NTP_Packet	NTP_Recv;		/* 受信するNTPパケット */
	int sockaddr_Size;			/* サーバのアドレスのサイズ */
	char buf[BUFSIZE];

	time_t Cur_time;			/* ローカルマシンの時刻 */
	time_t ntp_time;			/* NTPサーバから取得した時刻 */
	struct tm *lpNewLocalTime;		/* 現地時刻に変換したNTPサーバの時刻 */
	SYSTEMTIME	Timecall;		/* ローカルマシンに設定する時刻 */
	float	Splitseconds;

	/* サーバを指定して受信を行う */
	sockaddr_Size = sizeof(serversockaddr);
	if(recvfrom(soc, (char *)&NTP_Recv, sizeof(NTP_Recv), 0,(struct sockaddr *)&serversockaddr,&sockaddr_Size) == SOCKET_ERROR ){
		SocketError(hWnd,"受信に失敗しました。");
		return;
	}
	/* ソケットを破棄する */
	SocketClose(hWnd);

	/* ローカルマシンの時刻を取得する */
	Cur_time = time(NULL);

	/* NTPサーバから取得した時刻を現地時間に変換する */
	ntp_time = ntohl(NTP_Recv.transmit_timestamp_seconds) - 2208988800;		/* 1970/01/01 からの秒数に変換 */
	lpNewLocalTime = localtime((unsigned int *)&ntp_time);
	if(lpNewLocalTime == NULL){
		MessageBox(hWnd,"時刻の取得に失敗しました。","Error",MB_OK | MB_ICONERROR);
		return;
	}

	/* 同期フラグが立っている場合は、時刻の同期を行う */
	if(sync_flag == TRUE){
		/* ローカル時刻の計算 */
		Timecall.wYear = lpNewLocalTime->tm_year + 1900;
		Timecall.wMonth = lpNewLocalTime->tm_mon + 1;
		Timecall.wDay = lpNewLocalTime->tm_mday;
		Timecall.wHour = lpNewLocalTime->tm_hour;
		Timecall.wMinute = lpNewLocalTime->tm_min;
		Timecall.wSecond = lpNewLocalTime->tm_sec;

		Splitseconds = (float)ntohl(NTP_Recv.transmit_timestamp_fractions);
		Splitseconds = (float)0.000000000200 * Splitseconds;
		Splitseconds = (float)1000.0 * Splitseconds;
		Timecall.wMilliseconds = (unsigned short)Splitseconds;

		/* ローカル時刻をNTPサーバから取得した時刻に設定する */
		if(SetLocalTime(&Timecall) == FALSE){
			MessageBox(hWnd,"時刻の設定に失敗しました。","Error",MB_OK | MB_ICONERROR);
		}else{
			MessageBox(hWnd,"NTPサーバの時刻に同期しました。","info",MB_OK | MB_ICONINFORMATION);
		}
	}

	/* ローカル時刻を表示する */
	wsprintf(buf,"%s",ctime((unsigned int *)&Cur_time));
	buf[strlen(buf) - 1] = '\0';
	SendMessage(LocalEdit,WM_SETTEXT,0,(LPARAM) ((LPSTR)buf));

	/* NTPサーバから取得した時刻を表示する */
	wsprintf(buf,"%s",ctime((unsigned int *)&ntp_time));
	buf[strlen(buf) - 1] = '\0';
	SendMessage(NTPEdit,WM_SETTEXT,0,(LPARAM) ((LPSTR)buf));

}

/*---------------------------------------------------
	メインウィンドウのプロシージャ
---------------------------------------------------*/
LONG APIENTRY  MainProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	char buf[BUFSIZE];

	switch (uMsg)
	{
	case WM_CREATE:		/* ウィンドウ作成時 */
		/* ウィンドウ内のコントロールを作成する */
		CreateWindow("STATIC",(LPSTR)"Server:",WS_CHILD | WS_VISIBLE,5,10,50,25,hWnd,(HMENU)NULL,g_hinst,NULL);
		CreateWindow("STATIC",(LPSTR)"Local:",WS_CHILD | WS_VISIBLE,5,50,50,25,hWnd,(HMENU)NULL,g_hinst,NULL);
		CreateWindow("STATIC",(LPSTR)"NTP:",WS_CHILD | WS_VISIBLE,5,80,50,25,hWnd,(HMENU)NULL,g_hinst,NULL);
		ServerEdit = CreateWindowEx(WS_EX_CLIENTEDGE,"EDIT",(LPSTR)NULL,WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL,60,10,300,25,hWnd,(HMENU)ID_EDIT_SERVER,g_hinst,NULL);
		LocalEdit = CreateWindowEx(WS_EX_CLIENTEDGE,"EDIT",(LPSTR)NULL,WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_READONLY,60,50,420,25,hWnd,(HMENU)ID_EDIT_CUR,g_hinst,NULL);
		NTPEdit = CreateWindowEx(WS_EX_CLIENTEDGE,"EDIT",(LPSTR)NULL,WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_READONLY,60,80,420,25,hWnd,(HMENU)ID_EDIT_NTP,g_hinst,NULL);
		hBTNGET = CreateWindow("BUTTON",(LPSTR)"GET",WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,370,10,50,25,hWnd,(HMENU)ID_GET,g_hinst,NULL);
		hBTNSYNC = CreateWindow("BUTTON",(LPSTR)"SYNC",WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,430,10,50,25,hWnd,(HMENU)ID_SYNC,g_hinst,NULL);
		SetFocus(ServerEdit);
		/* 初期化 */
		soc = -1;
		hGetHost = NULL;

		break;

	case WSOCK_GETHOST:	/* サーバのホスト情報関数のイベント */
		/* ホスト情報からIPアドレスを取得して接続を開始する */
		GetHostToIPaddr(hWnd,wParam,lParam);
		break;

	case WSOCK_SELECT:		/* ソケットイベントのメッセージの場合(WSAAsyncSelect関数にて登録) */
		/* エラーの判定 */
		if(soc != -1 && WSAGETSELECTERROR(lParam) != 0){
			SocketError(hWnd,"Socketイベントの通知でエラーが発生しました。");
			break;
		}
		/* 処理すべきソケットか判定 */
		if(soc != (int)wParam){
			break;
		}
		/* 受信のイベントの場合は処理を行う */
		if(WSAGETSELECTEVENT(lParam) == FD_READ){
			/* データを受信する */
			SNTPRecvData(hWnd);
		}
		break;

	case WM_COMMAND:
		switch(LOWORD(wParam)){
		case ID_GET:			/* 取得するボタンが押されたときの処理 */
			if(soc != -1){		/* 既に通信中の場合はキャンセルする */
				/* ソケットを破棄する */
				SocketClose(hWnd);
			}else{			/* 通信を開始する */
				/* サーバ名を取得する */
				SendMessage(ServerEdit,WM_GETTEXT,BUFSIZE - 1,(LPARAM)buf);
				SetWindowText(hBTNGET,"Cancel");
				/* 同期フラグを偽に設定する */
				sync_flag = FALSE;
				/* SNTTPセッションを開始する */
				SNTPStart(hWnd,buf);
			}
			break;

		case ID_SYNC:			/* 同期するボタンが押されたときの処理 */
			if(soc != -1){		/* 既に通信中の場合はキャンセルする */
				/* ソケットを破棄する */
				SocketClose(hWnd);
			}else{			/* 通信を開始する */
				/* サーバ名を取得する */
				SendMessage(ServerEdit,WM_GETTEXT,BUFSIZE - 1,(LPARAM)buf);
				SetWindowText(hBTNSYNC,"Cancel");
				/* 同期フラグを真に設定する */
				sync_flag = TRUE;
				/* SNTPセッションを開始する */
				SNTPStart(hWnd,buf);
			}
			break;
		}
		break;

	case WM_TIMER:		/* タイムアウト時の処理 */
		if(wParam == TIMER_ID){
			/* タイムアウトのメッセージを表示する */
			SocketError(hWnd,"タイムアウトしました。");
			break;
		}
		break;
		:
		:


サンプルのダウンロード:

sntpcex00.zip
- sntpcex.c
- sntpcex.rc
- resource.h
Res
    - icon1.ico
- sntpcex.exe
このプログラムはGUIベースで動作します。

NTPサーバについては、http://www.ntp.org/を参照してください。
※環境変数に TZ=JST-9の指定がないと9時間ずれた時刻になってしまいます。

sntpcex.cをコンパイルする場合は、wsock32.lib をリンクしてください。

サンプルは、NTPサーバに時刻情報をリクエストしてから取得していますが、ブロードキャストしている時刻情報を取得する場合は、送信部分(sendto)とタイムアウト処理部分(タイマー)を削るとブロードキャストしている時刻情報を取得できるはずです。



mail
メールアドレス
<nakka@nakka.com>


Internet Programming
Internet Programming