ESP8266開發之旅 網絡篇⑮ DNSServer——真正的域名服務

1. 前言

    Arduino for esp8266中有兩個DNS服務相關的庫:html

  1. ESP8266mDNS庫
  • 這個庫是mDNS庫,使用這個庫的時候ESP8266能夠在AP模式或是以STA模式接入局域網;
  • 局域網中的其餘開啓mDNS服務的設備就能夠經過網址訪問ESP8266;
  • 這個博主在以前的博文中有講解過 —— ESP8266開發之旅 網絡篇⑫ 域名服務——ESP8266mDNS庫
  • 有個明顯缺點,須要其餘設備也開啓mDNS服務,像window系統須要安裝一個 Bonjour,同時域名爲 xxx.local;
  1. DNSServer庫
  • 這個庫就是本文將用到的創建DNS服務的方式,使用該庫時ESP8266必須工做在AP模式下;
  • 這個屬於真正意義的精簡版DNS服務器;
  • DNSServer運行於 UDP服務,請回顧:ESP8266開發之旅 網絡篇⑩ UDP服務;

看看DNSServer具體工做原理:
imageweb

  • 在這裏,DNS服務器惟一的做用就是把域名轉成對應映射的地址

2. DNS server庫

    ESP8266使用DNS服務(通常和WebServer服務一塊兒使用,WebServer請回顧 ESP8266開發之旅 網絡篇⑪ WebServer——ESP8266WebServer庫的使用),請在代碼中加入如下頭文件:瀏覽器

#include <DNSServer.h>

    講解方法以前,先來看看博主總結的百度腦圖:
image服務器

經常使用方法很是簡單,就4個方法,畢竟DNS服務器的功能比較單一。網絡

2.1 start —— 啓動DNS服務器

函數說明:dom

/**
 * 啓動DNS服務器
 * @param port  端口號 DNS端口通常佔用53
 * @param domainName 映射域名
 * @param resolvedIP 映射IP地址
 * @return  bool 是否啓動成功
 */
bool start(const uint16_t &port,
           const String &domainName,
           const IPAddress &resolvedIP);

源碼說明:函數

bool DNSServer::start(const uint16_t &port, const String &domainName,
                     const IPAddress &resolvedIP)
{
  _port = port;
  _buffer = NULL;
  _domainName = domainName;
  _resolvedIP[0] = resolvedIP[0];
  _resolvedIP[1] = resolvedIP[1];
  _resolvedIP[2] = resolvedIP[2];
  _resolvedIP[3] = resolvedIP[3];
  downcaseAndRemoveWwwPrefix(_domainName);
  //啓動了UDP服務 監聽客戶端向DNS服務器查詢域名
  return _udp.begin(_port) == 1;
}

2.2 stop —— 中止DNS服務器

函數說明:oop

/**
 * 中止DNS服務器
 */
void stop();

源碼說明:網站

void DNSServer::stop()
{
  //中止udp服務
  _udp.stop();
  free(_buffer);
  _buffer = NULL;
}

2.3 setErrorReplyCode —— 設置錯誤響應碼

函數說明:ui

/**
 * 設置錯誤響應碼
 * @param  DNSReplyCode  錯誤響應碼
 */
void setErrorReplyCode(const DNSReplyCode &replyCode);

DNSReplyCode 定義以下:

enum class DNSReplyCode
{
  NoError = 0,
  FormError = 1,
  ServerFailure = 2, //服務錯誤
  NonExistentDomain = 3,
  NotImplemented = 4,//未定義
  Refused = 5,//拒絕訪問
  YXDomain = 6,
  YXRRSet = 7,
  NXRRSet = 8
};

2.4 processNextRequest —— 處理DNS請求服務

函數說明:

/**
 * 處理DNS請求服務
 */
void processNextRequest();

源碼說明:

/**
 * 處理DNS請求服務
 */
void DNSServer::processNextRequest()
{
  //獲取UDP請求內容
  _currentPacketSize = _udp.parsePacket();
  if (_currentPacketSize)
  {
    if (_buffer != NULL) free(_buffer);
    _buffer = (unsigned char*)malloc(_currentPacketSize * sizeof(char));
    if (_buffer == NULL) return;
    _udp.read(_buffer, _currentPacketSize);
    _dnsHeader = (DNSHeader*) _buffer;

    //判斷請求是否查找域名映射的IP地址 *在這裏有很是特殊做用 讀者請注意
    if (_dnsHeader->QR == DNS_QR_QUERY &&
        _dnsHeader->OPCode == DNS_OPCODE_QUERY &&
        requestIncludesOnlyOneQuestion() &&
        (_domainName == "*" || getDomainNameWithoutWwwPrefix() == _domainName)
       )
    {
      //返回IP地址
      replyWithIP();
    }
    else if (_dnsHeader->QR == DNS_QR_QUERY)
    {
      //響應錯誤碼
      replyWithCustomCode();
    }

    free(_buffer);
    _buffer = NULL;
  }
}

/**
 * 響應域名對應的IP地址
 */
void DNSServer::replyWithIP()
{
  if (_buffer == NULL) return;
  _dnsHeader->QR = DNS_QR_RESPONSE;
  _dnsHeader->ANCount = _dnsHeader->QDCount;
  _dnsHeader->QDCount = _dnsHeader->QDCount; 
  //_dnsHeader->RA = 1;  

  _udp.beginPacket(_udp.remoteIP(), _udp.remotePort());
  _udp.write(_buffer, _currentPacketSize);

  _udp.write((uint8_t)192); //  answer name is a pointer
  _udp.write((uint8_t)12);  // pointer to offset at 0x00c

  _udp.write((uint8_t)0);   // 0x0001  answer is type A query (host address)
  _udp.write((uint8_t)1);

  _udp.write((uint8_t)0);   //0x0001 answer is class IN (internet address)
  _udp.write((uint8_t)1);
 
  _udp.write((unsigned char*)&_ttl, 4);

  // Length of RData is 4 bytes (because, in this case, RData is IPv4)
  _udp.write((uint8_t)0);
  _udp.write((uint8_t)4);
  _udp.write(_resolvedIP, sizeof(_resolvedIP));
  _udp.endPacket();



  #ifdef DEBUG_ESP_DNS
    DEBUG_ESP_PORT.printf("DNS responds: %s for %s\n",
            IPAddress(_resolvedIP).toString().c_str(), getDomainNameWithoutWwwPrefix().c_str() );
  #endif
}

/**
 * 響應錯誤碼
 */
void DNSServer::replyWithCustomCode()
{
  if (_buffer == NULL) return;
  _dnsHeader->QR = DNS_QR_RESPONSE;
  _dnsHeader->RCode = (unsigned char)_errorReplyCode;
  _dnsHeader->QDCount = 0;

  _udp.beginPacket(_udp.remoteIP(), _udp.remotePort());
  _udp.write(_buffer, sizeof(DNSHeader));
  _udp.endPacket();
}

注意點:

  • ESP8266 DNSServer 運行於UDP協議之上
  • ESP8266 DNSServer只能支持一個域名映射
  • 當ESP8266設置的域名爲「*」,意味着全部請求都會被連接到該IP地址,咱們能夠利用這一點作一些特殊操做;

3. 實例

3.1 訪問主機名

實驗說明

在手機瀏覽器訪問 "www.danpianji.com"會顯示「Hello World」

實驗源碼

/**
 * 功能描述:在手機瀏覽器訪問 "www.danpianji.com"會顯示「Hello World」 
 */

#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>

//調試定義
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )

const byte DNS_PORT = 53;
IPAddress apIP(192, 168, 1, 1);
DNSServer dnsServer;
ESP8266WebServer webServer(80);

void setup() {

  DebugBegin(115200);
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  WiFi.softAP("DNSServer example");

  // modify TTL associated  with the domain name (in seconds)
  // default is 60 seconds
  dnsServer.setTTL(300);
  // set which return code will be used for all other domains (e.g. sending
  // ServerFailure instead of NonExistentDomain will reduce number of queries
  // sent by clients)
  // default is DNSReplyCode::NonExistentDomain
  dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure);

  // 啓動DNS server,映射主機名爲 www.danpianji.com
  bool status = dnsServer.start(DNS_PORT, "www.danpianji.com", apIP);

  if(status){
      DebugPrintln("start dnsserver success.");
  }else{
     DebugPrintln("start dnsserver failed.");
  }

  // simple HTTP server to see that DNS server is working
  webServer.onNotFound([]() {
    String message = "Hello World!\n\n";
    message += "URI: ";
    message += webServer.uri();

    webServer.send(200, "text/plain", message);
  });
  webServer.begin();
}

void loop() {
  dnsServer.processNextRequest();
  webServer.handleClient();
}

實驗結果

會看到一個DNSServer example開放式AP熱點,鏈接上:

image

而後在手機瀏覽器訪問 www.danpianji.com

image

3.2 Portal 認證

實驗說明

一般,當咱們連上一些wifi熱點,只要沒有認證手機號碼信息,不管訪問哪一個頁面都會彈出一個web認證頁面(這就是商家用來收集手機用戶信息的一種手段,慎重),這就是 Portal 認證。

Portal服務器也就是接收Portal客戶端認證請求的服務器端系統,其主要做用是提供免費的門戶服務和基於Web認證的界面,以及接入設備交互認證客戶端的認證信息。其中的Web認證方案首先須要給用戶分配一個地址,用於訪問門戶網站。

Portal 基於瀏覽器,採用的是B/S構架, 對不一樣權限的用戶下發不一樣的VLAN 訪問不一樣的服務器資源,當經過認證後才能訪問internet資源,Portal認證方式不須要安裝認證客戶端, 減小了客戶端的維護工做量,便於運營。

能夠在Portal頁面上開展業務拓展,如展現商家廣告, 聯繫方式等基本信息。Portal普遍應用於運營商、學校等網絡。

經過DNSServer,咱們也可使用到Portal認證

實驗源碼

/**
 * 功能描述:portal認證
 */

#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>

//調試定義
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )

const byte DNS_PORT = 53;
IPAddress apIP(192, 168, 1, 1);
DNSServer dnsServer;
ESP8266WebServer webServer(80);

String responseHTML = ""
                      "<!DOCTYPE html><html><head><title>CaptivePortal</title></head><body>"
                      "<h1>Hello World!</h1><p>This is a captive portal example. All requests will "
                      "be redirected here.</p></body></html>";

void setup() {
  DebugBegin(115200);
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  WiFi.softAP("DNSServer CaptivePortal example");

 
  // 全部請求都映射到一個具體地址
  dnsServer.start(DNS_PORT, "*", apIP);

  // replay to all requests with same HTML
  webServer.onNotFound([]() {
    DebugPrintln("webServer handle.");
    webServer.send(200, "text/html", responseHTML);
  });
  webServer.begin();
}

void loop() {
  dnsServer.processNextRequest();
  webServer.handleClient();
}

實驗結果
會看到一個 DNSServer CaptivePortal example 開放式AP熱點,鏈接上:

image

而後在手機瀏覽器訪問 www.danpianji.com

image

4. 總結

DNSServer也是相對來講很是重要的一章,特別對於web配網,須要使用到它,請讀者認真理解使用。

相關文章
相關標籤/搜索