Page 1 of 1

GetNetworkTime() - NTP Server abfragen

Posted: 24 Jul 2017, 02:39
by just me
Die Funktion habe ich wegen dieser Frage entwickelt. Sie holt sich das aktuelle Datum inklusive Zeit von einem Zeitserver im Internet.

Grundlagen: Die Zeitserver liefern immer UTC Zeiten. Ich habe deshalb noch einige UTC-Funktionen zur Konvertierung zwischen UTC- und lokalen Zeiten dazugepackt.

Die Funktion berücksichtigt nicht die Laufzeiten im Internet. Die Rückgabewerte sind deshalb nicht immer 'absolut genaue Zeitangaben bis hin zur Sekunde'.

Code: Select all

#NoEnv
SetBatchLines, -1
NtpSvr := "de.pool.ntp.org"
S := A_TickCount
NetTime := UTC_ToLocalTime(GetNetworkTime(NtpSvr))
T := A_TickCOunt - S
MsgBox, 0, %NtpSvr% (%T% ms), %NetTime%
ExitApp

; ================================================================================================================================
; Server    -  The name of the NTP server to query.
;              Default: time.windows.com
; Port      -  The number of the port to query.
;              Default: 123
; Timeout   -  The number of milliseconds the client will wait for a response.
;              Default: 1000
; ================================================================================================================================
GetNetworkTime(Server := "time.windows.com", Port := 123, Timeout := 1000) {
   ; stackoverflow.com/questions/1193955/how-to-query-an-ntp-server-using-c
   ; www.cisco.com/c/en/us/about/press/internet-protocol-journal/back-issues/table-contents-58/154-ntp.html
   If !(Ws2 := DllCall("LoadLibrary", "Str", "Ws2_32.dll", "UPtr")) {
      Msg := "Ws2_32.dll could not be loaded!"
      Return GetNetworkTime_Error(Msg, Ws2, 0, 0)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Initialize the Winsock library - request Winsock 2.2
   VarSetCapacity(WsaData, 512, 0) ; more than sufficient
   If (Error := DllCall("Ws2_32.dll\WSAStartup", "UShort", 0x0202, "UInt", &WsaData, "Int")) {
      Msg := "WSAStartup() failed with error " . Error
      Return GetNetworkTime_Error(Msg, Ws2, 0, 0)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Name resolution
   If !(HostEnt := DllCall("Ws2_32.dll\gethostbyname", "AStr", Server, "UPtr")) {
      Msg := "gethostbyname() failed with error " . DllCall("Ws2_32.dll\WSAGetLastError", "Int")
      Return GetNetworkTime_Error(Msg, Ws2, 1, 0)
   }
   AddrList := NumGet(HostEnt + 0, (2 * A_PtrSize) + 4 + (A_PtrSize - 4), "UPtr")
   IpAddr := NumGet(AddrList + 0, 0, "UPtr")
   Addr := StrGet(DllCall("Ws2_32.dll\inet_ntoa", "UInt", NumGet(IpAddr + 0, 0, "UInt"), "UPtr"), "CP0")
   InetAddr := DllCall("Ws2_32.dll\inet_addr", "AStr", Addr, "UInt") ; convert address to 32-bit UInt
   If (InetAddr = 0xFFFFFFFF) { ; INADDR_NONE
      Msg := "inet_addr() failed for address " . Addr
      Return GetNetworkTime_Error(Msg, Ws2, 1, 0)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Create a socket: AF_INET = 2, SOCK_DGRAM = 2, IPPROTO_UDP = 17
   Socket := DllCall("Ws2_32.dll\socket", "UInt", 2, "UInt", 2, "UInt", 17, "UPtr")
   If (Socket = 0xFFFFFFFF) { ; INVALID_SOCKET
      Msg := "socket() indicated Winsock error " . DllCall("Ws2_32.dll\WSAGetLastError")
      Return GetNetworkTime_Error(Msg, Ws2, 1, 0)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Connect to the socket
   VarSetCapacity(SocketAddr, 16, 0) ; size of sockaddr = 16
   NumPut(2, SocketAddr, 0, "Short") ; sin_family = AF_INET
   NumPut(DllCall("Ws2_32.dll\htons", "UShort", Port), SocketAddr, 2, "UShort") ; sin_port
   Numput(InetAddr, SocketAddr, 4, "UInt") ; sin_addr.s_addr
   If DllCall("Ws2_32.dll\connect", "Ptr", Socket, "UInt", &SocketAddr, "Int", 16) {
      Msg := "connect() indicated Winsock error " . DllCall("Ws2_32.dll\WSAGetLastError")
      Return GetNetworkTime_Error(Msg, Ws2, 1, Socket)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Set the timeout for recv: SOL_SOCKET = 0xFFFF, SO_RCVTIMEO = 0x1006, SOCKET_ERROR = -1
   If (DllCall("Ws2_32.dll\setsockopt", "Ptr", Socket, "Int", 0xFFFF, "Int", 0x1006, "IntP", Timeout, "Int", 4, "Int") = -1) {
      Msg := "setsockopt() indicated Winsock error " . DllCall("Ws2_32.dll\WSAGetLastError")
      Return GetNetworkTime_Error(Msg, Ws2, 1, Socket)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Send an NTP request
   VarSetCapacity(Data, 48, 0) ; NTP needs 48 bytes of data
   NumPut(0x1B, Data, "UChar") ; LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode) -> 00 011 011
   If (DllCall("Ws2_32.dll\send", "Ptr", Socket, "Str", Data, "Int", 48, "Int", 0, "Int") = -1) { ; SOCKET_ERROR = -1
      Msg := "send() indicated Winsock error " . DllCall("Ws2_32.dll\WSAGetLastError")
      Return GetNetworkTime_Error(Msg, Ws2, 1, Socket)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Shutdown sending
   DllCall("Ws2_32.dll\shutdown", "Ptr", Socket, "Int", 1) ; SD_SEND = 1
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Get the answer
   If (DllCall("Ws2_32.dll\recv", "Ptr", Socket, "Ptr", &Data, "Int", 48, "Int", 0, "Int") = -1) { ; SOCKET_ERROR = -1
      Msg := "recv() indicated Winsock error " . DllCall("Ws2_32.dll\WSAGetLastError")
      Return GetNetworkTime_Error(Msg, Ws2, 1, Socket)
   }
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Free resources
   DllCall("Ws2_32.dll\closesocket", "Ptr", Socket)
   DllCall("Ws2_32.dll\WSACleanup") ; Terminate the use of the Winsock 2 DLL
   DllCall("FreeLibrary", "Ptr", Ws2)
   ; -----------------------------------------------------------------------------------------------------------------------------
   ; Get the time, we have to swap the byte order
   Sek := (NumGet(Data, 40, "Uchar") << 24) | (NumGet(Data, 41, "Uchar") << 16) | (NumGet(Data, 42, "Uchar") << 8)
         | NumGet(Data, 43, "Uchar")
   DT := "19000101"
   DT += Sek, S
   Return DT
}
; ================================================================================================================================
GetNetworkTime_Error(Msg, HMOD, Startup, Socket) {
   MsgBox, 16, %A_ThisFunc%, %Msg%
   If (Socket)
      DllCall("Ws2_32.dll\closesocket", "Ptr", Socket)
   If (Startup)
      DllCall("Ws2_32.dll\WSACleanup") ; Terminate the use of the Winsock 2 DLL
   If (HMOD)
      DllCall("FreeLibrary", "Ptr", HMOD)
   Return 0
}

; ================================================================================================================================
; UTC functions
; ================================================================================================================================
UTC_ToLocalTime(UTC := "") { ; UTC : (partial) date-time stamp (YYYYMMDDHH24MISS)
   UTC += 0, S
   If UTC Is Not Time
      Return ""
   UTC_TS2SYSTEMTIME(UTC, UTCTime)
   VarSetCapacity(LocTime, 16, 0)
   DllCall("SystemTimeToTzSpecificLocalTime", "Ptr", 0, "Ptr", &UTCTime, "Ptr", &LocTime)
   Return UTC_SYSTEMTIME2TS(LocTime)
}
; --------------------------------------------------------------------------------------------------------------------------------
UTC_FromLocalTime(Local := "") { ; Local : (partial) date-time stamp (YYYYMMDDHH24MISS)
   Local += 0, S
   If Local Is Not Time
      Return ""
   UTC_TS2SYSTEMTIME(Local, LocTime)
   VarSetCapacity(UTCTime, 16, 0)
   DllCall("TzSpecificLocalTimeToSystemTime", "Ptr", 0, "Ptr", &LocTime, "Ptr", &UTCTime)
   Return UTC_SYSTEMTIME2TS(UTCTime)
}
; --------------------------------------------------------------------------------------------------------------------------------
UTC_TS2SYSTEMTIME(TS, ByRef SYSTEMTIME) { ; Date-time stamp to SYSTEMTIME
   VarSetCapacity(SYSTEMTIME, 16, 0)
   NumPut(SubStr(TS, 1, 4), SYSTEMTIME, 0, "UShort")
   NumPut(SubStr(TS, 5, 2), SYSTEMTIME, 2, "UShort")
   NumPut(SubStr(TS, 7, 2), SYSTEMTIME, 6, "UShort")
   NumPut(SubStr(TS, 9, 2), SYSTEMTIME, 8, "UShort")
   NumPut(SubStr(TS, 11, 2), SYSTEMTIME, 10, "UShort")
   NumPut(SubStr(TS, 13, 2), SYSTEMTIME, 12, "UShort")
   Return &SYSTEMTIME
}
; --------------------------------------------------------------------------------------------------------------------------------
UTC_SYSTEMTIME2TS(ByRef SYSTEMTIME) { ; SYSTEMTIME to date-time stamp
   YYYY := NumGet(SYSTEMTIME, 0, "UShort")
   MM := NumGet(SYSTEMTIME, 2, "UShort")
   DD := NumGet(SYSTEMTIME, 6, "UShort")
   HH := NumGet(SYSTEMTIME, 8, "UShort")
   MN := NumGet(SYSTEMTIME, 10, "UShort")
   SS := NumGet(SYSTEMTIME, 12, "UShort")
   Return Format("{:04}{:02}{:02}{:02}{:02}{:02}", YYYY, MM, DD, HH, MN, SS)
}

Re: GetNetworkTime() - NTP Server abfragen

Posted: 24 Jul 2017, 04:08
by BoBo
Aaaasrein - :thumbup:
Ne frage von einer bekennenden logikwurst: 'der echtzeit' ließe sich annähern durch ermitteln und aufaddieren von script- und weblaufzeit (IMHO irgendwas in richtung A_TickCount/QueryPerformanceCounter()-frickelei), oder??
Das windows-interne w32tm.exe retourniert (nach abfrage mit parameter /stripchart) die zeitdifferenz zw lokalem und zeitserver ("Display a strip chart of the offset between this computer and another computer"). Dazu die verprasste zeit der anfrage und scriptverwurstung, und man/frau wäre irgendwie schon nahe dran ... :shifty:

Re: GetNetworkTime() - NTP Server abfragen

Posted: 24 Jul 2017, 04:53
by just me
Ja, das lässt sich bestimmt irgendwie berechnen. Das Cisco Dokument beschreibt z.B. noch weitere 'mögliche' Zeitstempeleinträge für Frage und Antwort. Allerdings glaube ich, dass sich Windows bei der Zeitsynchronisierung nicht auf nur eine Abfrage gegen einen Server verlässt.

Mit einer 'schnellen' Internetanbindung (ich habe 6 MBit) und nicht allzu 'verstopftem' Netz läuft die komplette Abfrage hier in der Regel weniger als 150 Millisekunden. Außerdem wartet die Funktion nur maximal 1 Sekunde auf eine Antwort und gibt 0 zurück, wenn in dieser Zeit keine ankommt. Die Abweichung sollte deshalb maximal 1 Sekunde betragen. Mir reicht das.

Re: GetNetworkTime() - NTP Server abfragen

Posted: 27 Jul 2017, 01:54
by just me
Moin BoBo,

wenn Dir eine 'grobe' Bestimmung der Abweichung genügt, reicht es, den von der Funktion gelieferten Zeitstempel (UTC) von A_NowUTC abzuziehen:

Code: Select all

NetTime := GetNetworkTime()
Diff := A_NowUTC
Diff -= NetTime, S
MsgBox, Abweichung = %Diff% Sekunden
Ich bekomme damit hier immer 0 oder 1.

Re: GetNetworkTime() - NTP Server abfragen

Posted: 27 Jul 2017, 03:33
by BoBo
Yo funzt! Thx :)
Mit nachfolgendem code habe ich allerdings bei unmittelbar aufeinanderfolgender ausführung via ScITE4AutoHoteky einen fehler provoziert.
Keine ahnung ob S4AHK oder GetNetworkTime() hierfür ursächlich ist??

Code: Select all

#SingleInstance, Force
#Include GetNetworkTime().ahk

	UTC := A_NowUTC
	UTC -= GetNetworkTime(), S
	MsgBox % UTC "`n" GetNetworkTime() "`n" A_NowUTC

; "gethostbyname failed with error 11004"

Re: GetNetworkTime() - NTP Server abfragen

Posted: 27 Jul 2017, 04:29
by jNizM
msdn wrote:WSANO_DATA -> 11004
Valid name, no data record of requested type.
Der angeforderte Name ist gültig und wurde in der Datenbank gefunden, aber es sind ihm nicht die korrekten aufgelösten Daten zugeordnet. Das übliche Beispiel hierfür ist ein Versuch, einen Hostnamen mit dem DNS (Domänennamenserver) in eine Adresse aufzulösen (mit gethostbyname oder WSAAsyncGetHostByName). Es wird ein MX-Eintrag, aber kein A-Eintrag zurückgegeben—womit angezeigt wird, dass der Host vorhanden, aber nicht direkt erreichbar ist.
Konnte das Problem aber mit SciTE4Ahk nicht nachvollziehen oder rekonstruieren.. Vlt. war die Abfrage einfach zu schnell hintereinander und er konnte deshalb in der Zeit den Namen nicht auflösen bzw zuordnen (wie im Fehler beschrieben)