シンプルなNTPクライアントの作成

こんにちは、habrauzers。 今日は、単純なNTPクライアントの作成方法についてお話します。 基本的に、パケットの構造と、NTPサーバーからの応答を処理する方法について説明します。 コードはpythonで書かれます。なぜなら、私にとっては、そのようなものに最適な言語は単に見つからないからです。 鑑定家は、コードとntplibコードの類似性に注意を払うでしょう-私はそれに触発されました。



NTPとは正確には何ですか? NTP-正確な時間サーバーとの相互作用のプロトコル。 このプロトコルは、多くの最新のマシンで使用されています。 たとえば、Windowsのw32tmサービス。



NTPプロトコルには合計で5つのバージョンがあります。 最初の0番目のバージョン(1985、RFC958))は、現在は時代遅れと見なされています。 現在、新しいものが使用されています:1番目(1988、RFC1059)、2番目(1989、RFC1119)、3番目(1992、RFC1305)および4番目(1996、RFC2030)。 1-4バージョンは相互に互換性があり、サーバー操作アルゴリズムのみが異なります。



パッケージ形式







飛躍インジケータ (修正インジケータ)-2番目の調整に関する警告を示す数値。 値:





バージョン番号 -NTPプロトコルのバージョン番号(1〜4)。



モード -パケット送信者の動作モード。 最も一般的な0〜7の値:





階層 (階層化レベル)-サーバーと基準クロックの間の中間層の数(1-サーバーは基準クロックから直接データを取得します、2-サーバーはレベル1のサーバーからデータを取得しますなど)。

ポーリングは、連続したメッセージ間の最大間隔を表す符号付き整数です。 ここで、NTPクライアントはサーバーをポーリングする間隔を示し、NTPサーバーはポーリングする間隔を示します。 値は秒の2進対数です。

精度は、システムクロックの精度を表す符号付き整数です。 値は秒の2進対数です。

ルート遅延 (サーバー遅延)-クロックがNTPサーバーに到達する時間(固定小数点の秒数)。

ルート分散 (サーバー読み取り値の分散 )-固定小数点を使用した秒数としてのNTPサーバーのクロックの分散。

Ref id (ソース識別子)-ウォッチのID。 サーバーのストラタムが1の場合、ref idは原子時計の名前(4 ASCII文字)です。 サーバーが別のサーバーを使用する場合、このサーバーのアドレスはref idに書き込まれます。

最後の4つのフィールドは、時間-32ビット-整数部、32ビット-小数部です。

参照 -サーバーの最新の時計。

Originate-パケットが送信された時間(サーバーによって満たされます-詳細は以下)。

受信 -パケットがサーバーによって受信された時間。

送信 -サーバーからクライアントにパケットを送信する時間(クライアントによって満たされます。詳細は以下を参照)。



最後の2つのフィールドは考慮しません。



パッケージを書きましょう:



パッケージコード
class NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0): # Necessary of enter leap second (2 bits) self.leap_indicator = 0 # Version of protocol (3 bits) self.version_number = version_number # Mode of sender (3 bits) self.mode = mode # The level of "layering" reading time (1 byte) self.stratum = 0 # Interval between requests (1 byte) self.pool = 0 # Precision (log2) (1 byte) self.precision = 0 # Interval for the clock reach NTP server (4 bytes) self.root_delay = 0 # Scatter the clock NTP-server (4 bytes) self.root_dispersion = 0 # Indicator of clocks (4 bytes) self.ref_id = 0 # Last update time on server (8 bytes) self.reference = 0 # Time of sending packet from local machine (8 bytes) self.originate = 0 # Time of receipt on server (8 bytes) self.receive = 0 # Time of sending answer from server (8 bytes) self.transmit = transmit
      
      







サーバーにパケットを送信(および受信)するには、パケットをバイトの配列に変換できる必要があります。

この(および逆の)操作のために、pack()およびunpack()の2つの関数を作成します。



パック機能
 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))
      
      







パッケージに書き込むために数値の小数部分を選択するには、get_fraction()関数が必要です。

get_fraction()
 def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision)
      
      







アンパック機能
 def unpack(self, data: bytes): unpacked_data = struct.unpack(NTPPacket._FORMAT, data) self.leap_indicator = unpacked_data[0] >> 6 # 2 bits self.version_number = unpacked_data[0] >> 3 & 0b111 # 3 bits self.mode = unpacked_data[0] & 0b111 # 3 bits self.stratum = unpacked_data[1] # 1 byte self.pool = unpacked_data[2] # 1 byte self.precision = unpacked_data[3] # 1 byte # 2 bytes | 2 bytes self.root_delay = (unpacked_data[4] >> 16) + \ (unpacked_data[4] & 0xFFFF) / 2 ** 16 # 2 bytes | 2 bytes self.root_dispersion = (unpacked_data[5] >> 16) + \ (unpacked_data[5] & 0xFFFF) / 2 ** 16 # 4 bytes self.ref_id = str((unpacked_data[6] >> 24) & 0xFF) + " " + \ str((unpacked_data[6] >> 16) & 0xFF) + " " + \ str((unpacked_data[6] >> 8) & 0xFF) + " " + \ str(unpacked_data[6] & 0xFF) self.reference = unpacked_data[7] + unpacked_data[8] / 2 ** 32 # 8 bytes self.originate = unpacked_data[9] + unpacked_data[10] / 2 ** 32 # 8 bytes self.receive = unpacked_data[11] + unpacked_data[12] / 2 ** 32 # 8 bytes self.transmit = unpacked_data[13] + unpacked_data[14] / 2 ** 32 # 8 bytes return self
      
      







怠け者の場合、アプリケーションとして-パッケージを美しい文字列に変えるコード
 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)
      
      







サーバーにパケットを送信する



バージョンモード 、および送信フィールドが入力されたパケットをサーバーに送信する必要があります。 送信では、ローカルマシンの現在の時間(1900年1月1日からの秒数)、バージョン-1〜4のいずれか、モード-3(クライアントモード)を指定する必要があります。



サーバーは、要求を受け入れた後、要求から送信値を送信 フィールドにコピーすることにより、NTPパケットのすべてのフィールドに入力します。 クライアントが「 Originate」フィールドで自分の時間の値をすぐに入力できない理由は私には謎です。 その結果、パケットが戻ってくると、クライアントには4回あります-要求が送信された時間( Originate )、サーバーが要求を受信した時間( Receive )、サーバーが応答を送信した時間( Transmit )、およびクライアントが応答を受信した時間- 到着 (パケットではありません) これらの値を使用して、正しい時間を設定できます。



パッケージの送受信コード
 # Time difference between 1970 and 1900, seconds FORMAT_DIFF = (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 3600 # Waiting time for recv (seconds) WAITING_TIME = 5 server = "pool.ntp.org" port = 123 packet = NTPPacket(version_number=2, mode=3, transmit=time.time() + FORMAT_DIFF) answer = NTPPacket() with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.settimeout(WAITING_TIME) s.sendto(packet.pack(), (server, port)) data = s.recv(48) arrive_time = time.time() + FORMAT_DIFF answer.unpack(data)
      
      







サーバーからのデータを処理する



サーバーからのデータの処理は、レイモンド・M・サリアン(1978)の古い仕事の英国紳士の行動に似ています:「ある人は時計を持っていませんでしたが、一方で、彼は時々始めるのを忘れた正確な壁時計がありました。 時計を再び起動するのを忘れていた彼は、友人を訪ねてその場所で夜を過ごし、家に戻ったときに時計を正しく設定することができました。 移動時間が事前にわからなかった場合、どうやってこれをやったのでしょうか?答えは次のとおりです。 友人のもとに来て客を残して、彼は彼の到着と出発の時間を書き留めます。 これにより、彼はどのくらい訪問していたかを知ることができます。 家に戻って時計を見ると、人は不在の期間を決定します。 この時間から彼が訪問に費やした時間を引くと、人は往復に費やした時間を見つけます。 旅行に費やした時間の半分をゲストに残す時間を増やしたため、彼は帰宅時間を見つけ、それに応じて時計の針を翻訳する機会を得ました。



リクエストのサーバー稼働時間を確認します。



  1. クライアントからサーバーへのパケットパス時間を見つけます: ((到着-発信)-(送信-受信))/ 2
  2. クライアントとサーバーの時間の違いを見つける:

    受信-発信-((到着-発信)-(送信-受信))/ 2 =

    2 *受信-2 *発信-到着+発信+送信-受信=

    受信-発信-到着+送信


得られた価値を現地時間に加えて、人生を楽しみましょう。



出力結果
 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)
      
      







便利なリンク



All Articles