天天看點

ESP8266(ESP12F)學習筆記2 -- NTP網絡時間擷取

對于已經掌握了ESP8266網絡連接配接的小夥伴來說,第一件事應該就是想着利用網路擷取一些資料,或者利用網絡去控制一些裝置,這裡利用NTP伺服器來擷取網絡時間

索引

      • NTP伺服器
      • Arduino NTPClient庫調用
        • NTPClient庫安裝
        • NTPClient庫示例
      • NTPClient庫函數粗步解析
        • NTPClient.h
        • 構造函數NTPClient ( )
        • NTP伺服器設定函數
        • 初始化函數
        • 初始化函數(帶端口設定參數)
        • NTP更新函數
        • 強制更新函數
        • 時間擷取函數
        • 設定時間間隔(時區)
        • 設定更新間隔
        • 擷取時間格式 - 字元串
        • 擷取絕對時間
        • NTP資料請求
      • 時間列印示例
        • 示例代碼
        • 示例結果

NTP伺服器

NTP伺服器【Network Time Protocol(NTP)】是用來使計算機時間同步化的一種協定,它可以使計算機對其伺服器或時鐘源(如石英鐘,GPS等等)做同步化,它可以提供高精準度的時間校正(LAN上與标準間差小于1毫秒,WAN上幾十毫秒),且可介由加密确認的方式來防止惡毒的協定攻擊。時間按NTP伺服器的等級傳播。按照離外部UTC源的遠近把所有伺服器歸入不同的Stratum(層)中。

特征介紹

NTP提供準确時間,首先要有準确的時間來源,這一時間應該是國際标準時間UTC。 NTP獲得UTC的時間來源可以是原子鐘、天文台、衛星,也可以從Internet上擷取。這樣就有了準确而可靠的時間源。時間按NTP伺服器的等級傳播。按照離外部UTC 源的遠近将所有伺服器歸入不同的Stratum(層)中。Stratum-1在頂層,有外部UTC接入,而Stratum-2則從Stratum-1擷取時間,Stratum-3從Stratum-2擷取時間,以此類推,但Stratum層的總數限制在15以内。所有這些伺服器在邏輯上形成階梯式的架構互相連接配接,而Stratum-1的時間伺服器是整個系統的基礎。

計算機主機一般同多個時間伺服器連接配接, 利用統計學的算法過濾來自不同伺服器的時間,以選擇最佳的路徑和來源來校正主機時間。即使主機在長時間無法與某一時間伺服器相聯系的情況下,NTP服務依然有效運轉。

為防止對時間伺服器的惡意破壞,NTP使用了識别(Authentication)機制,檢查來對時的資訊是否是真正來自所宣稱的伺服器并檢查資料的傳回路徑,以提供對抗幹擾的保護機制。

網絡校時

時間伺服器可以利用以下三種方式與其他伺服器對時:

  • broadcast/multicast
  • client/server
  • symmetric

(1)broadcast/multicast方式主要适用于區域網路的環境,時間伺服器周期性的以廣播的方式,将時間資訊傳送給其他網路中的時間伺服器,其時間僅會有少許的延遲,而且配置非常的簡單。但是此方式的精确度并不高,對時間精确度要求不是很高的情況下可以采用。

(2)symmetric的方式得一台伺服器可以從遠端時間伺服器擷取時鐘,如果需要也可提供時間資訊給遠端的時間伺服器。此一方式适用于配置備援的時間伺服器,可以提供更高的精确度給主機。

(3)client/server方式與symmetric方式比較相似,隻是不提供給其他時間伺服器時間資訊,此方式适用于一台時間伺服器接收上層時間伺服器的時間資訊,并提供時間資訊給下層的使用者。

上述三種方式,時間資訊的傳輸都使用UDP協定。時間伺服器利用一個過濾演算法,及先前八個校時資料計算出時間參考值,判斷後續校時包的精确性,一個相對較高的離散程度,表示一個對時資料的可信度比較低。僅從一個時間伺服器獲得校時資訊,不能校正通訊過程所造成的時間偏差,而同時與許多時間伺服器通信校時,就可利用過濾算法找出相對較可靠的時間來源,然後采用它的時間來校時。

曆史發展

網絡時間協定(NTP)的首次實作記載在Internet Engineering Note之中,其精确度為數百毫秒。稍後出現了首個時間協定的規範,即RFC-778,它被命名為DCNET網際網路時間服務,而它提供這種服務還是借助于Internet control MessageProtocol(ICMP),即網際網路控制消息協定中的時間戳和時間戳應答消息。作為NTP

名稱的首次出現是在RFC-958之中,該版本也被稱為NTP v0,其目的是為ARPA網提供時間同步。它己完全脫離ICMP,是作為獨立的協定以完成更高要求的時間同步。它對于如本地時鐘的誤差估算和精密度等基本運算、參考時鐘的特性、網絡上的分組資料包及其消息格式進行了描述。但是不對任何頻率誤差進行補償,也沒有規定濾波和同步的算法。

美國特拉華大學(University of Delaware)的David L .Mills主持了由美國國防部進階研究計劃局DARPA、美國國家科學基金NSF和美國海軍水面武器中心NSWC資助的網絡時間同步項目,成功的開發出了NTP協定的第1, 2, 3版。

出現時間

NTP version 1 出現于1988年6月,在RFC-1059中描述了首個完整的NTP的規範和相關算法。這個版本已經采用了client/server模式以及對稱操作,但是它不支援授權鑒别和NTP的控制消息。

1989年9月推出了取代RFC-958和RFC-1059的NTP v2版本即RFC-1119。

幾乎同時,DEC公司也推出了一個時間同步協定,數字時間同步服務DTSS(Digital Time Synchronization Service).在 1992 年3月,NTP v3版本RFC-1305問世,該版本總結和綜合了NTP先前版本和DTSS,正式引入了校正原則,并改進了時鐘選擇和時鐘濾波的算法,而且還引入了時間消息發送的廣播模式,這個版本取代了NTP的先前版本。NT P v 3 釋出後,一直在不斷地進行改進,NTP實作的一個重要功能是對計算機作業系統的時鐘調整。在NTPv3研究和推出的同時,有關在作業系統核心中改進時間保持功能的研究也在并行地進行。1994年推出了RFC-1559,名為A KernelModel for Precision Time keening,即精密時01保持的核心模式,這個實作可以把計算機作業系統的時間精确度保持在微秒數量級。幾乎同時,改進建議。對本地時鐘調整算法,通信模式,新的時鐘驅動器,又提出了NTP v4适配規則等方面的改進描述了具體方向。

發展方向

NTP的第4版正在研究和測試中,網絡時間同步技術也将向更高精度、更強的相容性和多平台的适應性方向發展。網絡時間協定NTP是用于網際網路中時間同步的标準之一,它的用途是把計算機的時鐘同步到世界協調時UTC,其精度在區域網路内可達0.1ms,在Internet上絕大多數的地方其精度可以達到1- 50ms 。

NTP伺服器隻要做個大概了解就好,我們需要清楚知道的是怎麼去擷取到NTP伺服器時間的方法

Arduino NTPClient庫調用

NTPClient庫安裝

打開

項目 → 加載庫 → 管理庫

,檢視Arduino的相關庫。

ESP8266(ESP12F)學習筆記2 -- NTP網絡時間擷取

輸入

ntp

搜尋,找到名字為

NTPClient

的庫進行安裝,我這裡安裝的版本是3.2.0的。

ESP8266(ESP12F)學習筆記2 -- NTP網絡時間擷取

NTPClient庫示例

檔案示例中打開NTPClient庫示例

NTPClientBasic

,代碼如下,需要修改WiFi名稱跟密碼,擷取NTP時間需要ESP8266首先連上可用網絡。

#include <NTPClient.h>
// change next line to use with another board/shield
#include <ESP8266WiFi.h>
//#include <WiFi.h> // for WiFi shield
//#include <WiFi101.h> // for WiFi 101 shield or MKR1000
#include <WiFiUdp.h>

const char *ssid     = "<SSID>";
const char *password = "<PASSWORD>";

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

void setup(){
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

  timeClient.begin();
}

void loop() {
  timeClient.update();

  Serial.println(timeClient.getFormattedTime());

  delay(1000);
}
           

NTP網絡時間協定基于UDP,需要調用到

WiFiUdp.h

,連接配接上WiFi之後,

timeClient.begin()

開啟NTP同步對時,loop函數中

timeClient.update()

更新目前時間,

timeClient.getFormattedTime()

在将擷取到的時間以時分秒的字元串形式列印出來。

NTPClient庫函數粗步解析

NTPClient.h

先看看

NTPClient.h

中的函數,看注釋基本都能看明白。

#pragma once

#include "Arduino.h"

#include <Udp.h>

#define SEVENZYYEARS 2208988800UL
#define NTP_PACKET_SIZE 48
#define NTP_DEFAULT_LOCAL_PORT 1337

class NTPClient {
  private:
    UDP*          _udp;
    bool          _udpSetup       = false;

    const char*   _poolServerName = "pool.ntp.org"; // Default time server
    int           _port           = NTP_DEFAULT_LOCAL_PORT;
    long          _timeOffset     = 0;		// In s

    unsigned long _updateInterval = 60000;  // In ms

    unsigned long _currentEpoc    = 0;      // In s
    unsigned long _lastUpdate     = 0;      // In ms

    byte          _packetBuffer[NTP_PACKET_SIZE];

    void          sendNTPPacket();

  public:
    NTPClient(UDP& udp);
    NTPClient(UDP& udp, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval);

    /**
     * Set time server name
     *
     * @param poolServerName
     */
    void setPoolServerName(const char* poolServerName);

    /**
     * Starts the underlying UDP client with the default local port
     */
    void begin();

    /**
     * Starts the underlying UDP client with the specified local port
     */
    void begin(int port);

    /**
     * This should be called in the main loop of your application. By default an update from the NTP Server is only
     * made every 60 seconds. This can be configured in the NTPClient constructor.
     *
     * @return true on success, false on failure
     */
    bool update();

    /**
     * This will force the update from the NTP Server.
     *
     * @return true on success, false on failure
     */
    bool forceUpdate();

    int getDay() const;
    int getHours() const;
    int getMinutes() const;
    int getSeconds() const;

    /**
     * Changes the time offset. Useful for changing timezones dynamically
     */
    void setTimeOffset(int timeOffset);

    /**
     * Set the update interval to another frequency. E.g. useful when the
     * timeOffset should not be set in the constructor
     */
    void setUpdateInterval(unsigned long updateInterval);

    /**
     * @return time formatted like `hh:mm:ss`
     */
    String getFormattedTime() const;

    /**
     * @return time in seconds since Jan. 1, 1970
     */
    unsigned long getEpochTime() const;

    /**
     * Stops the underlying UDP client
     */
    void end();
};

           

構造函數NTPClient ( )

NTPClient(udp, poolServerName, timeOffset,updateInterval)

首先,看構造函數,以參數最多的函數做個解析

NTPClient(UDP& udp);
    NTPClient(UDP& udp, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval);
           

描述:NTPClient類構造函數之一,設定NTP的基本參數。

文法:NTPClient(udp, poolServerName, timeOffset,updateInterval)。

參數:udp:WiFiUDP類對象;poolServerName:NTP伺服器路徑;timeOffset:NTP時間偏移(時區);updateInterval:時間間隔。

用法:

WiFiUDP    ntp_udp;
timeClient(ntp_udp,"ntp1.aliyun.com",60*60*8,60000);
           
  • 參數1 - ntp_udp:WiFiUDP對象
  • 參數2 - “ntp1.aliyun.com”:阿裡雲NTP伺服器(任意NTP伺服器都可)
  • 參數3 -

    60*60*8

    :該參數機關為秒,60*60為東一區時間,中原標準時間為東八區,是以這裡參數為

    60*60*8

  • 參數4 - 60000:設定NTP更新最小時間間隔,參數機關為毫秒
NTPClient(udp, poolServerName, timeOffset,updateInterval) 函數體
NTPClient::NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval) {
  this->_udp            = &udp;
  this->_timeOffset     = timeOffset;
  this->_poolServerName = poolServerName;
  this->_updateInterval = updateInterval;
}
           

NTP伺服器設定函數

setPoolServerName(poolServerName)
/**
     * Set time server name
     *
     * @param poolServerName
     */
    void setPoolServerName(const char* poolServerName);
           

描述:NTP伺服器設定函數。

文法:setPoolServerName(const char* poolServerName)。

參數:poolServerName:伺服器路徑。

setPoolServerName(poolServerName) 函數體
void NTPClient::setPoolServerName(const char* poolServerName) {
    this->_poolServerName = poolServerName;
}
           

初始化函數

begin()
/**
     * Starts the underlying UDP client with the default local port
     */
    void begin();
           

描述:NTP初始化函數。

文法:begin()。

參數:無。

begin()函數體
void NTPClient::begin() {
  this->begin(NTP_DEFAULT_LOCAL_PORT);
}
           

初始化函數(帶端口設定參數)

begin(port)
/**
     * Starts the underlying UDP client with the specified local port
     */
    void begin(int port);
           

描述:使用指定的本地端口啟動底層UDP用戶端。

文法:begin(port)。

參數:port:本地端口。

begin(port)函數體
void NTPClient::begin(int port) {
  this->_port = port;

  this->_udp->begin(this->_port);

  this->_udpSetup = true;
}
           

NTP更新函數

update()
/**
     * This should be called in the main loop of your application. By default an update from the NTP Server is only
     * made every 60 seconds. This can be configured in the NTPClient constructor.
     *
     * @return true on success, false on failure
     */
    bool update();
           

描述:在使用者函數中循環調用,用于更新NTP用戶端,調用間隔需要大于或等于60秒。

文法:update()。

參數:無。

update()函數體
bool NTPClient::update() {
  if ((millis() - this->_lastUpdate >= this->_updateInterval)     // Update after _updateInterval
    || this->_lastUpdate == 0) {                                // Update if there was no update yet.
    if (!this->_udpSetup) this->begin();                         // setup the UDP client if needed
    return this->forceUpdate();
  }
  return true;
}
           

強制更新函數

forceUpdate()

可以發現該函數在update中被調用了

/**
     * This will force the update from the NTP Server.
     *
     * @return true on success, false on failure
     */
    bool forceUpdate();
           

描述:強制更新NTP伺服器。

文法:forceUpdate()。

參數:無。

forceUpdate()函數體
bool NTPClient::forceUpdate() {
  #ifdef DEBUG_NTPClient
    Serial.println("Update from NTP Server");
  #endif

  this->sendNTPPacket();

  // Wait till data is there or timeout...
  byte timeout = 0;
  int cb = 0;
  do {
    delay ( 10 );
    cb = this->_udp->parsePacket();
    if (timeout > 100) return false; // timeout after 1000 ms
    timeout++;
  } while (cb == 0);

  this->_lastUpdate = millis() - (10 * (timeout + 1)); // Account for delay in reading the time

  this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE);

  unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]);
  unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]);
  // combine the four bytes (two words) into a long integer
  // this is NTP time (seconds since Jan 1 1900):
  unsigned long secsSince1900 = highWord << 16 | lowWord;

  this->_currentEpoc = secsSince1900 - SEVENZYYEARS;

  return true;
}
           

時間擷取函數

調用以擷取目前時間為周幾,小時數,分鐘數,秒數等的函數。

int getDay() const;
    int getHours() const;
    int getMinutes() const;
    int getSeconds() const;
           

getDay()函數

描述:擷取目前時間星期幾。

參數:無。

傳回值:數字0 - 6,注意,國外将每周星期天定為一周的第一天。是以星期天的傳回值為0。星期六的傳回值為6。

getHours()函數

描述:擷取小時數。

參數:無。

傳回值:0 - 23。

getMinutes()函數

描述:擷取分鐘數。

參數:無。

傳回值:0 - 59。

getSeconds()函數

描述:擷取秒數。

參數:無。

傳回值:0 - 59。

時間擷取函數實作方法
int NTPClient::getDay() const {
  return (((this->getEpochTime()  / 86400L) + 4 ) % 7); //0 is Sunday
}
int NTPClient::getHours() const {
  return ((this->getEpochTime()  % 86400L) / 3600);
}
int NTPClient::getMinutes() const {
  return ((this->getEpochTime() % 3600) / 60);
}
int NTPClient::getSeconds() const {
  return (this->getEpochTime() % 60);
}
           

設定時間間隔(時區)

setTimeOffset(timeOffset)
/**
     * Changes the time offset. Useful for changing timezones dynamically
     */
    void setTimeOffset(int timeOffset);
           

描述:設定/修改時間間隔,即時區。

文法:setTimeOffset(timeOffset)。

參數:timeOffset:時間間隔,以秒為機關。

用法:

setTimeOffset(timeOffset)函數體
void NTPClient::setTimeOffset(int timeOffset) {
  this->_timeOffset     = timeOffset;
}
           

設定更新間隔

setUpdateInterval(updateInterval)
/**
     * Set the update interval to another frequency. E.g. useful when the
     * timeOffset should not be set in the constructor
     */
    void setUpdateInterval(unsigned long updateInterval);
           

描述:設定/修改NTP更新間隔。

文法:setUpdateInterval(updateInterval)。

參數:updateInterval:時間間隔,機關為毫秒。

setUpdateInterval(updateInterval)函數體
void NTPClient::setUpdateInterval(unsigned long updateInterval) {
  this->_updateInterval = updateInterval;
}
           

擷取時間格式 - 字元串

getFormattedTime()
/**
     * @return time formatted like `hh:mm:ss`
     */
    String getFormattedTime() const;
           

描述:擷取時間格式為

hh:mm:ss

的字元串。

文法:getFormattedTime()。

參數:無。

傳回值:字元串

hh:mm:ss

getFormattedTime()函數體
String NTPClient::getFormattedTime() const {
  unsigned long rawTime = this->getEpochTime();
  unsigned long hours = (rawTime % 86400L) / 3600;
  String hoursStr = hours < 10 ? "0" + String(hours) : String(hours);

  unsigned long minutes = (rawTime % 3600) / 60;
  String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes);

  unsigned long seconds = rawTime % 60;
  String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds);

  return hoursStr + ":" + minuteStr + ":" + secondStr;
}
           

擷取絕對時間

getEpochTime()
/**
     * @return time in seconds since Jan. 1, 1970
     */
    unsigned long getEpochTime() const;
           

描述:擷取自1970年1月1日到現在的時間秒數(很大一個數字)。

文法:getEpochTime()。

參數:無。

傳回值:時間秒數。

getEpochTime()函數體
unsigned long NTPClient::getEpochTime() const {
  return this->_timeOffset + // User offset
         this->_currentEpoc + // Epoc returned by the NTP server
         ((millis() - this->_lastUpdate) / 1000); // Time since last update
}
           

NTP資料請求

sendNTPPacket()

在forceUpdate()函數中被調用

void NTPClient::sendNTPPacket() {
  // set all bytes in the buffer to 0
  memset(this->_packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  this->_packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  this->_packetBuffer[1] = 0;     // Stratum, or type of clock
  this->_packetBuffer[2] = 6;     // Polling Interval
  this->_packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  this->_packetBuffer[12]  = 49;
  this->_packetBuffer[13]  = 0x4E;
  this->_packetBuffer[14]  = 49;
  this->_packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  this->_udp->beginPacket(this->_poolServerName, 123); //NTP requests are to port 123
  this->_udp->write(this->_packetBuffer, NTP_PACKET_SIZE);
  this->_udp->endPacket();
}
           

時間列印示例

示例代碼

在序列槽列印時間(WiFi需要能聯網的)。

#include <NTPClient.h>
// change next line to use with another board/shield
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

const char *ssid     = "<SSID>";
const char *password = "<PASSWORD>";

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

void setup(){
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }
  timeClient.setTimeOffset(60*60*8);	//東八區時間設定
  timeClient.begin();
}

void loop() {
  timeClient.update();

  Serial.println(timeClient.getFormattedTime());

  delay(1000);
}
           

示例結果

時間戳與NTP網絡時間對比

ESP8266(ESP12F)學習筆記2 -- NTP網絡時間擷取

繼續閱讀