単純な NTP クライアントの作成

ハブラユーザーの皆さん、こんにちは。 今日は、独自の単純な NTP クライアントを作成する方法について話したいと思います。 基本的に、会話はパケットの構造と、NTP サーバーからの応答がどのように処理されるかに移ります。 コードは Python で書かれます。私の意見では、このようなことを行うのにこれ以上適した言語はないと考えています。 愛好家は、このコードと ntplib コードの類似性に注目するでしょう。私はそれに「インスピレーションを受けました」。

そもそも NTP とは何でしょうか? NTP はタイム サーバーと通信するためのプロトコルです。 このプロトコルは多くの最新のマシンで使用されています。 たとえば、Windows の w32tm サービスです。

NTP プロトコルには合計 5 つのバージョンがあります。 最初のバージョン 0 (1985、RFC958) は、現在廃止されていると考えられています。 現在、より新しいものが使用されており、1st (1988、RFC1059)、2nd (1989、RFC1119)、3rd (1992、RFC1305)、および 4th (1996、RFC2030) です。 バージョン 1 ~ 4 は相互に互換性があり、サーバーのアルゴリズムのみが異なります。

パケットフォーマット

単純な NTP クライアントの作成

飛躍インジケーター (修正インジケータ)は、うるう秒の警告を示す数字です。 意味:

  • 0 - 補正なし
  • 1 - 61 日の最後の XNUMX 分には XNUMX 秒が含まれます
  • 2 - その日の最後の 59 分には XNUMX 秒が含まれます
  • 3 - サーバー障害 (タイムアウト同期)

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

モード (mode) — パケット送信者の動作モード。 0 ~ 7 の値、最も一般的:

  • 3 - クライアント
  • 4 - サーバー
  • 5 - ブロードキャストモード

地層 (階層化レベル) - サーバーと基準クロック間の中間層の数 (1 - サーバーは基準クロックから直接データを取得します、2 - サーバーはレベル 1 のサーバーからデータを取得します、など)。
プール 連続するメッセージ間の最大間隔を表す符号付き整数です。 ここで NTP クライアントはサーバーへのポーリングを予期する間隔を指定し、NTP サーバーはポーリングを予期する間隔を指定します。 値は秒の XNUMX 進対数に等しくなります。
精度 (精度) は、システム クロックの精度を表す符号付き整数です。 値は秒の XNUMX 進対数に等しくなります。
ルート遅延 (サーバー遅延) は、クロックが NTP サーバーに到達するまでにかかる時間 (秒単位の固定小数点数) です。
根の分散 (サーバー散乱) - 固定小数点秒数としての NTP サーバー クロックの散乱。
参照ID (ソース ID) – ウォッチ ID。 サーバーにストラタム 1 がある場合、ref id は原子時計の名前 (4 つの ASCII 文字) になります。 サーバーが別のサーバーを使用している場合、ref ID にはこのサーバーのアドレスが含まれます。
最後の 4 つのフィールドは、時間 - 32 ビット - 整数部分、32 ビット - 小数部分です。
参照 - サーバー上の最新の時計。
発信 – パケットが送信された時刻 (サーバーによって入力されます – 詳細は後述します)。
受け取ります – サーバーがパケットを受信した時刻。
送信します – パケットがサーバーからクライアントに送信された時刻 (クライアントによって入力されます。詳細は後述します)。

最後の XNUMX つのフィールドは考慮されません。

パッケージを書いてみましょう:

パッケージコード

class NTPPacket:
    _FORMAT = "!B B b b 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() という XNUMX つの関数を作成します。

パック機能

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))

アンパック関数

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)

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

フィールドが埋められたパケットをサーバーに送信します , モード и 送信します。 で 送信します ローカル マシンの現在時刻 (1 年 1900 月 1 日からの秒数)、バージョン - 4 ~ 3 のいずれか、モード - XNUMX (クライアント モード) を指定する必要があります。

リクエストを受信したサーバーは、NTP パケットのすべてのフィールドに値を入力し、フィールドにコピーします。 発信 からの値 送信します、リクエストに来ました。 なぜクライアントが現場での時間の価値をすぐに記入できないのかは私には謎です 発信。 その結果、パケットが戻ってくると、クライアントは 4 つの時間値 - リクエストが送信された時間 (発信)、サーバーがリクエストを受信した時刻(受け取ります)、サーバーが応答を送信した時刻 (送信します) およびクライアントによる応答の受信時刻 - 到着する (パッケージには入っていません)。 これらの値を使用して、正しい時刻を設定できます。

パッケージ送受信コード

# 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)

有用 リンク.

出所: habr.com

コメントを追加します