Olá, habrayuzery. Hoje eu quero falar sobre como escrever meu cliente NTP simples. Basicamente, falaremos sobre a estrutura do pacote e como processar a resposta do servidor NTP. O código será escrito em python, porque, ao que me parece, a melhor linguagem para essas coisas simplesmente não pode ser encontrada. Os especialistas prestarão atenção à semelhança do código com o código ntplib - eu fui "inspirado" por ele.
Então, o que exatamente é o NTP? NTP - protocolo de interação com servidores de horário exato. Este protocolo é usado em muitas máquinas modernas.
Por exemplo, o serviço w32tm no Windows.Existem 5 versões do protocolo NTP no total. A primeira versão 0 (1985, RFC958)) é atualmente considerada obsoleta. Agora, os mais novos são usados, 1º (1988, RFC1059), 2º (1989, RFC1119), 3º (1992, RFC1305) e 4º (1996, RFC2030). 1 a 4 versões são compatíveis entre si, diferem apenas nos algoritmos de operação do servidor.
Formato do pacote
Indicador de salto (
indicador de correção) - um número mostrando um aviso sobre a segunda coordenação. Valor:
- 0 - sem correção
- 1 - o último minuto do dia contém 61 segundos
- 2 - o último minuto do dia contém 59 segundos
- 3 - mau funcionamento do servidor (horário não sincronizado)
Número da versão - O número da versão do protocolo NTP (1-4).
Modo - modo de operação do remetente de pacote. Valor de 0 a 7, o mais comum:
- 3 - cliente
- 4 - servidor
- 5 - modo de transmissão
Estrato (nível de estratificação) - o número de camadas intermediárias entre o servidor e o relógio de referência (1 - o servidor obtém dados diretamente do relógio de referência, 2 - o servidor obtém dados do servidor com nível 1, etc.).
Poll é um número inteiro assinado que representa o intervalo máximo entre mensagens consecutivas. Aqui, o cliente NTP indica o intervalo no qual espera pesquisar o servidor, e o servidor NTP indica o intervalo no qual espera ser pesquisado. O valor é o logaritmo binário de segundos.
Precisão é um número inteiro assinado que representa a precisão do relógio do sistema. O valor é o logaritmo binário de segundos.
Atraso na raiz (
atraso do servidor) - o tempo durante o qual o relógio atinge o servidor NTP, como o número de segundos com um ponto fixo.
Dispersão raiz - a dispersão do relógio do servidor NTP como o número de segundos com um ponto fixo.
ID de referência (identificador de origem) - identificação do relógio. Se o servidor tiver um estrato 1, ref id será o nome do relógio atômico (4 caracteres ASCII). Se o servidor usar outro servidor, o endereço desse servidor será gravado no ID de referência.
Os últimos 4 campos são tempo - 32 bits - a parte inteira, 32 bits - parte fracionária.
Referência - o relógio mais recente no servidor.
Origem - a hora em que o pacote foi enviado (preenchido pelo servidor - mais sobre isso abaixo).
Receber - hora em que o pacote foi recebido pelo servidor.
Transmitir - tempo em que o pacote foi enviado do servidor para o cliente (preenchido pelo cliente, mais sobre isso abaixo).
Não consideraremos os dois últimos campos.
Vamos escrever nosso pacote:
Código do pacoteclass NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0):
Para enviar (e receber) um pacote para o servidor, devemos poder transformá-lo em uma matriz de bytes.
Para esta operação (e reversa), escreveremos duas funções - pack () e unpack ():
Função Pack 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 selecionar a parte fracionária do número para gravar no pacote, precisamos da função get_fraction ():
get_fraction () def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision)
Função desembalar def unpack(self, data: bytes): unpacked_data = struct.unpack(NTPPacket._FORMAT, data) self.leap_indicator = unpacked_data[0] >> 6
Para pessoas preguiçosas, como um aplicativo - um código que transforma um pacote em uma bela string 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)
Enviando um pacote para o servidor
É necessário enviar um pacote ao servidor com os campos
Versão ,
Modo e
Transmissão preenchidos. Em
Transmit, você precisa especificar o horário atual na máquina local (número de segundos desde 1º de janeiro de 1900), versão - qualquer um dos 1-4, modo - 3 (modo cliente).
Após aceitar a solicitação, o servidor preenche todos os campos no pacote NTP, copiando o valor
Transmitir da solicitação para o campo
Originate . É um mistério para mim o motivo pelo qual o cliente não pode preencher imediatamente o valor de seu tempo no campo
Origem . Como resultado, quando o pacote volta, o cliente tem quatro vezes - a hora em que a solicitação foi enviada (
Origem ), a hora em que o servidor recebeu a solicitação (
Receber ), a hora em que o servidor enviou a resposta (
Transmitir ) e a hora em que o cliente recebeu a resposta -
Chegar (não no pacote). Usando esses valores, podemos definir a hora correta.
Código de envio e recebimento de pacotes Processando dados do servidor
O processamento de dados do servidor é semelhante às ações do cavalheiro inglês da antiga tarefa de Raymond M. Sullian (1978): “Uma pessoa não tinha relógio, mas, por outro lado, havia um relógio de parede exato que às vezes esquecia de iniciar. Uma vez, esquecido de reiniciar o relógio, foi visitar o amigo, passou a noite naquele local e, quando voltou para casa, conseguiu acertar o relógio corretamente. Como ele conseguiu fazer isso se o tempo da viagem não era conhecido com antecedência? ” A resposta é: “Saindo de casa, uma pessoa liga o relógio e lembra em que posição os ponteiros estão. Chegando a um amigo e deixando os convidados, ele anota a hora de sua chegada e partida. Isso permite que ele descubra quanto estava visitando. Depois de voltar para casa e olhar para o relógio, uma pessoa determina a duração de sua ausência. Subtraindo desse tempo o tempo que ele passou em uma visita, uma pessoa aprende o tempo gasto na viagem de um lado para outro. Depois de adicionar metade do tempo gasto na viagem ao horário de saída dos convidados, ele tem a oportunidade de descobrir a hora de chegada em casa e traduzir os ponteiros do relógio de acordo.
Encontramos o tempo de trabalho do servidor na solicitação:
- Nós encontramos o tempo do caminho do pacote do cliente para o servidor: ((Chegada - Origem) - (Transmitir - Receber)) / 2
- Encontre a diferença entre o horário do cliente e do servidor:
Receber - Originar - ((Chegar - Originar) - (Transmitir - Receber)) / 2 =
2 * Receber - 2 * Originar - Chegar + Originar + Transmitir - Receber =
Receber - Originar - Chegar + Transmitir
Adicione o valor obtido à hora local e aproveite a vida.
Resultado de saída 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)
Link útil.