Hola habrayuzery. Hoy quiero hablar sobre cómo escribir mi cliente NTP simple. Básicamente, hablaremos sobre la estructura del paquete y cómo procesar la respuesta del servidor NTP. El código se escribirá en Python, porque, me parece, el mejor lenguaje para tales cosas simplemente no se puede encontrar. Los entendidos prestarán atención a la similitud del código con el código ntplib: me “inspiró”.
Entonces, ¿qué es exactamente NTP? NTP: protocolo de interacción con servidores de hora exactos. Este protocolo se usa en muchas máquinas modernas.
Por ejemplo, el servicio w32tm en windows.Hay 5 versiones del protocolo NTP en total. La primera versión 0 (1985, RFC958) se considera actualmente obsoleta. Ahora se utilizan los más nuevos: primero (1988, RFC1059), segundo (1989, RFC1119), tercero (1992, RFC1305) y cuarto (1996, RFC2030). Las versiones 1-4 son compatibles entre sí, solo difieren en los algoritmos de operación del servidor.
Formato del paquete
Indicador de salto (
indicador de corrección): un número que muestra una advertencia sobre la segunda coordinación. Valor:
- 0 - sin corrección
- 1 - el último minuto del día contiene 61 segundos
- 2 - el último minuto del día contiene 59 segundos
- 3 - mal funcionamiento del servidor (hora no sincronizada)
Número de versión: el número de versión del protocolo NTP (1-4).
Modo : modo de funcionamiento del remitente del paquete. Valor de 0 a 7, el más común:
- 3 - cliente
- 4 - servidor
- 5 - modo de transmisión
Estrato (nivel de capas): el número de capas intermedias entre el servidor y el reloj de referencia (1 - el servidor toma datos directamente del reloj de referencia, 2 - el servidor toma datos del servidor con nivel 1, etc.).
Encuesta es un entero con signo que representa el intervalo máximo entre mensajes consecutivos. Aquí, el cliente NTP indica el intervalo en el que espera sondear el servidor, y el servidor NTP indica el intervalo en el que espera sondear. El valor es el logaritmo binario de segundos.
La precisión es un entero con signo que representa la precisión del reloj del sistema. El valor es el logaritmo binario de segundos.
Root delay (
retraso del servidor): el tiempo durante el cual el reloj llega al servidor NTP, como el número de segundos con un punto fijo.
Dispersión de la raíz : la dispersión del reloj del servidor NTP como el número de segundos con un punto fijo.
ID de referencia (identificador de fuente): identificación del reloj. Si el servidor tiene un estrato de 1, entonces id de referencia es el nombre del reloj atómico (4 caracteres ASCII). Si el servidor usa otro servidor, la dirección de este servidor se escribe en la ID de referencia.
Los últimos 4 campos son tiempo - 32 bits - la parte entera, 32 bits - parte fraccionaria.
Referencia : el último reloj del servidor.
Originar : la hora en que se envió el paquete (llenado por el servidor; más sobre eso a continuación).
Recibir : hora en que el servidor recibió el paquete.
Transmisión : hora en que se envió el paquete desde el servidor al cliente (llenado por el cliente, más sobre eso a continuación).
No consideraremos los dos últimos campos.
Escribamos nuestro paquete:
Código del paqueteclass NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0):
Para enviar (y recibir) un paquete al servidor, debemos poder convertirlo en una matriz de bytes.
Para esta operación (e inversa), escribiremos dos funciones: pack () y unpack ():
Función de paquete def pack(self): return struct.pack(NTPPacket._FORMAT, (self.leap_indicator << 6) + (self.version_number << 3) + self.mode, self.stratum, self.pool, self.precision, int(self.root_delay) + get_fraction(self.root_delay, 16), int(self.root_dispersion) + get_fraction(self.root_dispersion, 16), self.ref_id, int(self.reference), get_fraction(self.reference, 32), int(self.originate), get_fraction(self.originate, 32), int(self.receive), get_fraction(self.receive, 32), int(self.transmit), get_fraction(self.transmit, 32))
Para seleccionar la parte fraccionaria del número para escribir en el paquete, necesitamos la función get_fraction ():
get_fraction () def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision)
Función de desempaquetado def unpack(self, data: bytes): unpacked_data = struct.unpack(NTPPacket._FORMAT, data) self.leap_indicator = unpacked_data[0] >> 6
Para las personas perezosas, como una aplicación: un código que convierte un paquete en una hermosa cadena def to_display(self): return "Leap indicator: {0.leap_indicator}\n" \ "Version number: {0.version_number}\n" \ "Mode: {0.mode}\n" \ "Stratum: {0.stratum}\n" \ "Pool: {0.pool}\n" \ "Precision: {0.precision}\n" \ "Root delay: {0.root_delay}\n" \ "Root dispersion: {0.root_dispersion}\n" \ "Ref id: {0.ref_id}\n" \ "Reference: {0.reference}\n" \ "Originate: {0.originate}\n" \ "Receive: {0.receive}\n" \ "Transmit: {0.transmit}"\ .format(self)
Enviar un paquete al servidor
Es necesario enviar un paquete al servidor con los campos
Versión ,
Modo y
Transmisión completos. En
Transmitir, debe especificar la hora actual en la máquina local (número de segundos desde el 1 de enero de 1900), versión - cualquiera de 1-4, modo - 3 (modo cliente).
Una vez aceptada la solicitud, el servidor completa todos los campos en el paquete NTP copiando el valor de
transmisión de la solicitud en el campo
Originar . Para mí es un misterio por qué el cliente no puede completar inmediatamente el valor de su tiempo en el campo
Originar . Como resultado, cuando el paquete regresa, el cliente tiene 4 veces: la hora en que se envió la solicitud (
Originar ), la hora en que el servidor recibió la solicitud (
Recibir ), la hora en que el servidor envió la respuesta (
Transmitir ) y la hora en que el cliente recibió la respuesta:
Llegada (no en el paquete). Usando estos valores, podemos establecer la hora correcta.
Código de envío y recepción de paquetes Procesando datos desde el servidor
El procesamiento de datos desde el servidor es similar a las acciones de un caballero inglés de la antigua tarea de Raymond M. Sullian (1978): “Una persona no tenía reloj, pero por otro lado había un reloj de pared exacto que a veces olvidaba comenzar. Una vez, una vez que se olvidó de volver a encender el reloj, fue a visitar a su amigo, pasó la noche en ese lugar y, cuando regresó a casa, logró ajustar el reloj correctamente. ¿Cómo logró hacer esto si el tiempo de viaje no se conocía de antemano? La respuesta es: “Al salir de la casa, una persona enciende el reloj y recuerda en qué posición están las manecillas. Al acercarse a un amigo y dejar a los invitados, anota la hora de su llegada y partida. Esto le permite saber cuánto estaba visitando. Al regresar a casa y mirar el reloj, una persona determina la duración de su ausencia. Restando de este tiempo el tiempo que pasó en una visita, una persona aprende el tiempo que pasó en el viaje de ida y vuelta. Después de haber agregado la mitad del tiempo dedicado al viaje a la hora de dejar a los invitados, tiene la oportunidad de averiguar la hora de llegada a casa y traducir las manecillas del reloj en consecuencia ".
Encontramos el tiempo de trabajo del servidor en la solicitud:
- Encontramos el tiempo de ruta del paquete desde el cliente al servidor: ((Llegada - Originar) - (Transmitir - Recibir)) / 2
- Encuentre la diferencia entre la hora del cliente y del servidor:
Recibir - Originar - ((Llegar - Originar) - (Transmitir - Recibir)) / 2 =
2 * Recibir - 2 * Originar - Llegar + Originar + Transmitir - Recibir =
Recibir - Originar - Llegar + Transmitir
Agregue el valor obtenido a la hora local y disfrute de la vida.
Resultado de salida time_different = answer.get_time_different(arrive_time) result = "Time difference: {}\nServer time: {}\n{}".format( time_different, datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"), answer.to_display()) print(result)
Enlace útil