• C++如何实现DNS域名解析<转>


    C++如何实现DNS域名解析

    这片文章介绍了C++如何实现DNS域名解析,还有对相关技术的介绍,代码很详细,需要的朋友可以参考下
     

    一、概述

    现在来搞定DNS域名解析,其实这是前面一篇文章C++实现Ping里面的遗留问题,要干的活是ping的过程中画红线的部分:

    cmd下域名解析的命令是nslookup,比如“nslookup www.baidu.com”的结果如下:

    其中,Address返回的就是www.baidu.com对应的IP地址,这个可能有多个

    Alias指别名,也就是说www.baidu.comwww.a.shifen.com的别名,而www.a.shifen.com则是www.baidu.com的规范名(Canonical Name,CName),具体参考RFC1035 3.2.2 & wikipedia

    二、实现结果预览

    先看一下最终搞成了什么样子

    输入:域名字符串

    输出:IP列表、CName列表、DNS查询所用时间

    三、相关技术

    3.1、UDP or TCP ? (RFC1035 4.2)

    UDP:DNS查询和回复采用低开销高性能的UDP,端口号为53。

    TCP:辅助DNS服务器从主DNS服务器拉取最新数据时,采用可靠的TCP传输,端口号也为53。

    我们这里做DNS查询采用UDP,53端口。

    3.2、DNS查询/回复包头部解析 (RFC1035 4.1.1)

    重点介绍一下我们关心的部分:

    ID(16bits):标识符,一般填入本进程的标识符

    QR(1bits):标志位,查询包为0,回复包为1

    Opcode(4bits):查询的种类,标准查询为0

    QDCOUNT(16bits):DNS查询/回复包数据部分Question字段的个数

    ANCOUNT(16bits):DNS查询/回复包数据部分Answer字段的个数

    3.2、DNS查询/回复包数据部分解析 (RFC1035 4.1.2 & 4.1.3)

    查询/回复包的数据部分依次为QDCOUNT个Question字段、ANCOUNT个Answer字段....

    对于任意字段,其格式如下:

    Name(不定长):域名,这部分的格式比较复杂,后面单独说。

    TYPE(16bits):查询类型/回复包RDATA类型,比如TYPE=1表示主机IP地址、TYPE=5表示CNAME,详见RFC1035 3.2.2

    CLASS(16bits):类,一般情况下CLASS=1表示Internet,详见RFC1035 3.2.4

    TTL(32bits,仅回复包):生存时间

    RDLENGTH(16bits,仅回复包):RDATA部分的字节数

    RDATA(不定长,仅回复包):资源数据,具体格式取决于TYPE和CLASS,比如TYPE=1、CLASS=1时,RDATA为四个字节的IP地址

    3.3、Name解析&消息压缩

    3.3.1、一般格式 (RFC1035 4.1.2)

    标签内容长度(1个字节) + 标签内容,以标签内容长度0作为Name的结束符,例如:

    3.3.2、消息压缩格式 (RFC1035 4.1.4)

    如果标签内容长度的二进制前两位是11,则表示消息压缩。

    此时,标签内容长度1个字节+后面的1个字节一共16位,后14位表示相对DNS包起始地址的偏移(Byte),例如:

    上述例子中,DNS包起始地址为0x0000,c0 13的二进制为11000000 00010003,即跳转偏移为0x13个字节,对应的数据为03 63 6f 6d 00。

    RFC1035中规定,支持的消息压缩规则为:

    ①以内容长度0结尾的标签序列

    ②偏移指针

    ③标签序列+偏移指针

    也就是说,Name的消息压缩要求偏移指针必须在Name的尾部,且不支持同一级存在多个偏移指针(偏移指针序列),

    但Name的消息压缩支持嵌套的偏移指针,即指针指向的偏移位置仍然是以偏移指针结尾的数据

    四、代码实现

    #pragma once
    
    //这里需要导入库 Ws2_32.lib,在不同的IDE下可能不太一样
    //#pragma comment(lib, "Ws2_32.lib")
    
    #include <windows.h>
    #include <string>
    #include <vector>
    
    #define MAX_DOMAINNAME_LEN 255
    #define DNS_PORT   53
    #define DNS_TYPE_SIZE  2
    #define DNS_CLASS_SIZE  2
    #define DNS_TTL_SIZE  4
    #define DNS_DATALEN_SIZE 2
    #define DNS_TYPE_A   0x0001 //1 a host address
    #define DNS_TYPE_CNAME  0x0005 //5 the canonical name for an alias
    #define DNS_PACKET_MAX_SIZE (sizeof(DNSHeader) + MAX_DOMAINNAME_LEN + DNS_TYPE_SIZE + DNS_CLASS_SIZE)
    
    struct DNSHeader
    {
     USHORT usTransID; //标识符
     USHORT usFlags; //各种标志位
     USHORT usQuestionCount; //Question字段个数 
     USHORT usAnswerCount; //Answer字段个数
     USHORT usAuthorityCount; //Authority字段个数
     USHORT usAdditionalCount; //Additional字段个数
    };
    
    class CDNSLookup
    {
    public:
     CDNSLookup();
     ~CDNSLookup();
    
     BOOL DNSLookup(ULONG ulDNSServerIP, char *szDomainName, std::vector<ULONG> *pveculIPList = NULL, std::vector<std::string> *pvecstrCNameList = NULL, ULONG ulTimeout = 1000, ULONG *pulTimeSpent = NULL);
     BOOL DNSLookup(ULONG ulDNSServerIP, char *szDomainName, std::vector<std::string> *pvecstrIPList = NULL, std::vector<std::string> *pvecstrCNameList = NULL, ULONG ulTimeout = 1000, ULONG *pulTimeSpent = NULL);
    
    private:
     BOOL Init();
     BOOL UnInit();
     BOOL DNSLookupCore(ULONG ulDNSServerIP, char *szDomainName, std::vector<ULONG> *pveculIPList, std::vector<std::string> *pvecstrCNameList, ULONG ulTimeout, ULONG *pulTimeSpent);
     BOOL SendDNSRequest(sockaddr_in sockAddrDNSServer, char *szDomainName);
     BOOL RecvDNSResponse(sockaddr_in sockAddrDNSServer, ULONG ulTimeout, std::vector<ULONG> *pveculIPList, std::vector<std::string> *pvecstrCNameList, ULONG *pulTimeSpent);
     BOOL EncodeDotStr(char *szDotStr, char *szEncodedStr, USHORT nEncodedStrSize);
     BOOL DecodeDotStr(char *szEncodedStr, USHORT *pusEncodedStrLen, char *szDotStr, USHORT nDotStrSize, char *szPacketStartPos = NULL);
     ULONG GetTickCountCalibrate();
    
    private:
     BOOL m_bIsInitOK;
     SOCKET m_sock;
     WSAEVENT m_event;
     USHORT m_usCurrentProcID;
     char *m_szDNSPacket;
    };
     [DNSLookup.h]
    
     
    
    #include "DNSLookup.h"
    #include <stdio.h>
    #include <string.h>
    
    CDNSLookup::CDNSLookup() : 
     m_bIsInitOK(FALSE), 
     m_sock(INVALID_SOCKET),
     m_szDNSPacket(NULL)
    {
     m_bIsInitOK = Init();
    }
    
    CDNSLookup::~CDNSLookup()
    {
     UnInit();
    }
    
    BOOL CDNSLookup::DNSLookup(ULONG ulDNSServerIP, char *szDomainName, std::vector<ULONG> *pveculIPList, std::vector<std::string> *pvecstrCNameList, ULONG ulTimeout, ULONG *pulTimeSpent)
    {
     return DNSLookupCore(ulDNSServerIP, szDomainName, pveculIPList, pvecstrCNameList, ulTimeout, pulTimeSpent);
    }
    
    BOOL CDNSLookup::DNSLookup(ULONG ulDNSServerIP, char *szDomainName, std::vector<std::string> *pvecstrIPList, std::vector<std::string> *pvecstrCNameList, ULONG ulTimeout, ULONG *pulTimeSpent)
    {
     std::vector<ULONG> *pveculIPList = NULL;
     if (!pvecstrIPList)
     {
       return FALSE;
     }

       std::vector<ULONG> veculIPList;
       pveculIPList = &veculIPList;

    
    
     BOOL bRet = DNSLookupCore(ulDNSServerIP, szDomainName, pveculIPList, pvecstrCNameList, ulTimeout, pulTimeSpent);
    
     if (bRet)
     {
      pvecstrIPList->clear();
      char szIP[16] = {''};
      for (std::vector<ULONG>::iterator iter = pveculIPList->begin(); iter != pveculIPList->end(); ++iter)
      {
       BYTE *pbyIPSegment = (BYTE*)(&(*iter));
       //sprintf_s(szIP, 16, "%d.%d.%d.%d", pbyIPSegment[0], pbyIPSegment[1], pbyIPSegment[2], pbyIPSegment[3]);
       sprintf(szIP, "%d.%d.%d.%d", pbyIPSegment[0], pbyIPSegment[1], pbyIPSegment[2], pbyIPSegment[3]);
       pvecstrIPList->push_back(szIP);
      }
     }
    
     return bRet;
    }
    
    
    BOOL CDNSLookup::Init()
    {
     WSADATA wsaData;
     if (WSAStartup(MAKEWORD(2, 2), &wsaData) == SOCKET_ERROR)
     {
      return FALSE;
     }
     
     if ((m_sock = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET)
     {
      return FALSE;
     }
    
     m_event = WSACreateEvent();
     WSAEventSelect(m_sock, m_event, FD_READ);
    
     m_szDNSPacket = new (std::nothrow) char[DNS_PACKET_MAX_SIZE];
     if (m_szDNSPacket == NULL)
     {
      return FALSE;
     }
    
     m_usCurrentProcID = (USHORT)GetCurrentProcessId();
    
     return TRUE;
    }
    
    BOOL CDNSLookup::UnInit()
    {
     WSACleanup();
    
     if (m_szDNSPacket != NULL)
     {
      delete [] m_szDNSPacket;
     }
    
     return TRUE;
    }
    
    BOOL CDNSLookup::DNSLookupCore(ULONG ulDNSServerIP, char *szDomainName, std::vector<ULONG> *pveculIPList, std::vector<std::string> *pvecstrCNameList, ULONG ulTimeout, ULONG *pulTimeSpent)
    {
     if (m_bIsInitOK == FALSE || szDomainName == NULL)
     {
      return FALSE;
     }
    
     //配置SOCKET
     sockaddr_in sockAddrDNSServer; 
     sockAddrDNSServer.sin_family = AF_INET; 
     sockAddrDNSServer.sin_addr.s_addr = ulDNSServerIP;
     sockAddrDNSServer.sin_port = htons( DNS_PORT );
    
     //DNS查询与解析
     if (!SendDNSRequest(sockAddrDNSServer, szDomainName)
      || !RecvDNSResponse(sockAddrDNSServer, ulTimeout, pveculIPList, pvecstrCNameList, pulTimeSpent))
     {
      return FALSE;
     }
    
     return TRUE;
    }
    
    BOOL CDNSLookup::SendDNSRequest(sockaddr_in sockAddrDNSServer, char *szDomainName)
    {
     char *pWriteDNSPacket = m_szDNSPacket;
     memset(pWriteDNSPacket, 0, DNS_PACKET_MAX_SIZE); 
    
     //填充DNS查询报文头部
     DNSHeader *pDNSHeader = (DNSHeader*)pWriteDNSPacket;
     pDNSHeader->usTransID = m_usCurrentProcID;
     pDNSHeader->usFlags = htons(0x0100);
     pDNSHeader->usQuestionCount = htons(0x0001);
     pDNSHeader->usAnswerCount = 0x0000;
     pDNSHeader->usAuthorityCount = 0x0000;
     pDNSHeader->usAdditionalCount = 0x0000;
    
     //设置DNS查询报文内容
     USHORT usQType = htons(0x0001);
     USHORT usQClass = htons(0x0001);
     USHORT nDomainNameLen = strlen(szDomainName);
     char *szEncodedDomainName = (char *)malloc(nDomainNameLen + 2);
     if (szEncodedDomainName == NULL)
     {
      return FALSE;
     }
     if (!EncodeDotStr(szDomainName, szEncodedDomainName, nDomainNameLen + 2))
     {
      return FALSE;
     }
    
     //填充DNS查询报文内容
     USHORT nEncodedDomainNameLen = strlen(szEncodedDomainName) + 1;
     memcpy(pWriteDNSPacket += sizeof(DNSHeader), szEncodedDomainName, nEncodedDomainNameLen);
     memcpy(pWriteDNSPacket += nEncodedDomainNameLen, (char*)(&usQType), DNS_TYPE_SIZE);
     memcpy(pWriteDNSPacket += DNS_TYPE_SIZE, (char*)(&usQClass), DNS_CLASS_SIZE);
     free (szEncodedDomainName);
    
     //发送DNS查询报文
     USHORT nDNSPacketSize = sizeof(DNSHeader) + nEncodedDomainNameLen + DNS_TYPE_SIZE + DNS_CLASS_SIZE;
     if (sendto(m_sock, m_szDNSPacket, nDNSPacketSize, 0, (sockaddr*)&sockAddrDNSServer, sizeof(sockAddrDNSServer)) == SOCKET_ERROR)
     {
      return FALSE;
     }
    
     return TRUE;
    }
    
    BOOL CDNSLookup::RecvDNSResponse(sockaddr_in sockAddrDNSServer, ULONG ulTimeout, std::vector<ULONG> *pveculIPList, std::vector<std::string> *pvecstrCNameList, ULONG *pulTimeSpent)
    {
     ULONG ulSendTimestamp = GetTickCountCalibrate();
    
     if (pveculIPList != NULL)
     {
      pveculIPList->clear();
     }
     if (pvecstrCNameList != NULL)
     {
      pvecstrCNameList->clear();
     }
    
     char recvbuf[1024] = {''};
     char szDotName[128] = {''};
     USHORT nEncodedNameLen = 0;
    
     while (TRUE)
     {
      if (WSAWaitForMultipleEvents(1, &m_event, FALSE, 100, FALSE) != WSA_WAIT_TIMEOUT)
      {
       WSANETWORKEVENTS netEvent;
       WSAEnumNetworkEvents(m_sock, m_event, &netEvent);
    
       if (netEvent.lNetworkEvents & FD_READ)
       {
        ULONG ulRecvTimestamp = GetTickCountCalibrate();
        int nSockaddrDestSize = sizeof(sockAddrDNSServer);
    
        //接收响应报文
        if (recvfrom(m_sock, recvbuf, 1024, 0, (struct sockaddr*)&sockAddrDNSServer, &nSockaddrDestSize) != SOCKET_ERROR)
        {
         DNSHeader *pDNSHeader = (DNSHeader*)recvbuf;
         USHORT usQuestionCount = 0;
         USHORT usAnswerCount = 0;
    
         if (pDNSHeader->usTransID == m_usCurrentProcID
          && (ntohs(pDNSHeader->usFlags) & 0xfb7f) == 0x8100 //RFC1035 4.1.1(Header section format)
          && (usQuestionCount = ntohs(pDNSHeader->usQuestionCount)) >= 0
          && (usAnswerCount = ntohs(pDNSHeader->usAnswerCount)) > 0)
         {
          char *pDNSData = recvbuf + sizeof(DNSHeader);
    
          //解析Question字段
          for (int q = 0; q != usQuestionCount; ++q)
          {
           if (!DecodeDotStr(pDNSData, &nEncodedNameLen, szDotName, sizeof(szDotName)))
           {
            return FALSE;
           }
           pDNSData += (nEncodedNameLen + DNS_TYPE_SIZE + DNS_CLASS_SIZE);
          }
    
          //解析Answer字段
          for (int a = 0; a != usAnswerCount; ++a)
          {
           if (!DecodeDotStr(pDNSData, &nEncodedNameLen, szDotName, sizeof(szDotName), recvbuf))
           {
            return FALSE;
           }
           pDNSData += nEncodedNameLen;
    
           USHORT usAnswerType = ntohs(*(USHORT*)(pDNSData));
           USHORT usAnswerClass = ntohs(*(USHORT*)(pDNSData + DNS_TYPE_SIZE));
           ULONG usAnswerTTL = ntohl(*(ULONG*)(pDNSData + DNS_TYPE_SIZE + DNS_CLASS_SIZE));
           USHORT usAnswerDataLen = ntohs(*(USHORT*)(pDNSData + DNS_TYPE_SIZE + DNS_CLASS_SIZE + DNS_TTL_SIZE));
           pDNSData += (DNS_TYPE_SIZE + DNS_CLASS_SIZE + DNS_TTL_SIZE + DNS_DATALEN_SIZE);
    
           if (usAnswerType == DNS_TYPE_A && pveculIPList != NULL)
           {
            ULONG ulIP = *(ULONG*)(pDNSData);
            pveculIPList->push_back(ulIP);
           }
           else if (usAnswerType == DNS_TYPE_CNAME && pvecstrCNameList != NULL)
           {
            if (!DecodeDotStr(pDNSData, &nEncodedNameLen, szDotName, sizeof(szDotName), recvbuf))
            {
             return FALSE;
            }
            pvecstrCNameList->push_back(szDotName);
           }
    
           pDNSData += (usAnswerDataLen);
          }
    
          //计算DNS查询所用时间
          if (pulTimeSpent != NULL)
          {
           *pulTimeSpent = ulRecvTimestamp - ulSendTimestamp;
          }
    
          break;
         }
        }
       }
      }
    
      //超时退出
      if (GetTickCountCalibrate() - ulSendTimestamp > ulTimeout)
      {
       *pulTimeSpent = ulTimeout + 1;
       return FALSE;
      }
     }
    
     return TRUE;
    }
    
    /*
     * convert "www.baidu.com" to "x03wwwx05baidux03com"
     * 0x0000 03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00 ff
     */
    BOOL CDNSLookup::EncodeDotStr(char *szDotStr, char *szEncodedStr, USHORT nEncodedStrSize)
    {
     USHORT nDotStrLen = strlen(szDotStr);
    
     if (szDotStr == NULL || szEncodedStr == NULL || nEncodedStrSize < nDotStrLen + 2)
     {
      return FALSE;
     }
    
     char *szDotStrCopy = new char[nDotStrLen + 1];
     //strcpy_s(szDotStrCopy, nDotStrLen + 1, szDotStr);
     strcpy(szDotStrCopy, szDotStr);
    
     char *pNextToken = NULL;
     //char *pLabel = strtok_s(szDotStrCopy, ".", &pNextToken);
     char *pLabel = strtok(szDotStrCopy, ".");
     USHORT nLabelLen = 0;
     USHORT nEncodedStrLen = 0;
     while (pLabel != NULL)
     {
      if ((nLabelLen = strlen(pLabel)) != 0)
      {
       //sprintf_s(szEncodedStr + nEncodedStrLen, nEncodedStrSize - nEncodedStrLen, "%c%s", nLabelLen, pLabel);
       sprintf(szEncodedStr + nEncodedStrLen, "%c%s", nLabelLen, pLabel);
       nEncodedStrLen += (nLabelLen + 1);
      }
      //pLabel = strtok_s(NULL, ".", &pNextToken);
      pLabel = strtok(NULL, ".");
     }
    
     delete [] szDotStrCopy;
     
     return TRUE;
    }
    
    /*
     * convert "x03wwwx05baidux03comx00" to "www.baidu.com"
     * 0x0000 03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00 ff
     * convert "x03wwwx05baiduxc0x13" to "www.baidu.com"
     * 0x0000 03 77 77 77 05 62 61 69 64 75 c0 13 ff ff ff ff
     * 0x0010 ff ff ff 03 63 6f 6d 00 ff ff ff ff ff ff ff ff
     */
    BOOL CDNSLookup::DecodeDotStr(char *szEncodedStr, USHORT *pusEncodedStrLen, char *szDotStr, USHORT nDotStrSize, char *szPacketStartPos)
    {
     if (szEncodedStr == NULL || pusEncodedStrLen == NULL || szDotStr == NULL)
     {
      return FALSE;
     }
    
     char *pDecodePos = szEncodedStr;
     USHORT usPlainStrLen = 0;
     BYTE nLabelDataLen = 0; 
     *pusEncodedStrLen = 0;
    
     while ((nLabelDataLen = *pDecodePos) != 0x00)
     {
      if ((nLabelDataLen & 0xc0) == 0) //普通格式,LabelDataLen + Label
      {
       if (usPlainStrLen + nLabelDataLen + 1 > nDotStrSize)
       {
        return FALSE;
       }
       memcpy(szDotStr + usPlainStrLen, pDecodePos + 1, nLabelDataLen);
       memcpy(szDotStr + usPlainStrLen + nLabelDataLen, ".", 1);
       pDecodePos += (nLabelDataLen + 1);
       usPlainStrLen += (nLabelDataLen + 1);
       *pusEncodedStrLen += (nLabelDataLen + 1);
      }
      else //消息压缩格式,11000000 00000000,两个字节,前2位为跳转标志,后14位为跳转的偏移
      {
       if (szPacketStartPos == NULL)
       {
        return FALSE;
       }
       USHORT usJumpPos = ntohs(*(USHORT*)(pDecodePos)) & 0x3fff;
       USHORT nEncodeStrLen = 0;
       if (!DecodeDotStr(szPacketStartPos + usJumpPos, &nEncodeStrLen, szDotStr + usPlainStrLen, nDotStrSize - usPlainStrLen, szPacketStartPos))
       {
        return FALSE;
       }
       else
       {
        *pusEncodedStrLen += 2;
        return TRUE;
       }
      }
     }
    
     szDotStr[usPlainStrLen - 1] = '';
     *pusEncodedStrLen += 1;
    
     return TRUE;
    }
    
    ULONG CDNSLookup::GetTickCountCalibrate()
    {
     static ULONG s_ulFirstCallTick = 0;
     static LONGLONG s_ullFirstCallTickMS = 0;
    
     SYSTEMTIME systemtime;
     FILETIME filetime;
     GetLocalTime(&systemtime); 
     SystemTimeToFileTime(&systemtime, &filetime);
     LARGE_INTEGER liCurrentTime;
     liCurrentTime.HighPart = filetime.dwHighDateTime;
     liCurrentTime.LowPart = filetime.dwLowDateTime;
     LONGLONG llCurrentTimeMS = liCurrentTime.QuadPart / 10000;
    
     if (s_ulFirstCallTick == 0)
     {
      s_ulFirstCallTick = GetTickCount();
     }
     if (s_ullFirstCallTickMS == 0)
     {
      s_ullFirstCallTickMS = llCurrentTimeMS;
     }
    
     return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS);
    }
     [DNSLookup.cpp]
    
     
    
    #include <stdio.h>
    #include <windows.h>
    #include "DNSLookup.h"
    
    int main(void)
    {
     char szDomainName[] = "www.baidu.com";
     std::vector<ULONG> veculIPList;
     std::vector<std::string> vecstrIPList;
     std::vector<std::string> vecCNameList;
     ULONG ulTimeSpent = 0;
     CDNSLookup dnslookup;
     BOOL bRet = dnslookup.DNSLookup(inet_addr("114.114.114.114"), szDomainName, &vecstrIPList, &vecCNameList, 1000, &ulTimeSpent);
    
     printf("DNSLookup result (%s):
    ", szDomainName);
     if (!bRet)
     {
      printf("timeout!
    ");
      return -1;
     }
    
     for (int i = 0; i != veculIPList.size(); ++i)
     {
      printf("IP%d(ULONG) = %u
    ", i + 1, veculIPList[i]);
     }
     for (int i = 0; i != vecstrIPList.size(); ++i)
     {
      printf("IP%d(string) = %s
    ", i + 1, vecstrIPList[i].c_str());
     }
     for (int i = 0; i != vecCNameList.size(); ++i)
     {
      printf("CName%d = %s
    ", i + 1, vecCNameList[i].c_str());
     }
     printf("time spent = %ums
    ", ulTimeSpent);
     
     return 0;
    }

    http://www.jb51.net/article/70359.htm

  • 相关阅读:
    js判断用户是否在浏览当前页面
    js不改变原对象情况下复制对象object
    js实现jquery中的addClass,removeClass,hasClass方法
    npm install总结 (--savenpm install --save与npm install --save-dev区别及其他相关)
    通过正则实时监听检查input输入,实时反应,不符合不能输入的功能详解
    IE8常见兼容问题及解决方法总结
    javascript中十六进制和ASCII码互相转换
    vue2.0中watch总结:普通监听和深度监听
    js对象数组操作之一:对象数组中对象去重的方法总结
    webpack4配置详细过程及采坑
  • 原文地址:https://www.cnblogs.com/wainiwann/p/8087035.html
Copyright © 2020-2023  润新知