張高興的 .NET IoT 入門指南:(八)基於 GPS 的 NTP 時間同步伺服器

来源:https://www.cnblogs.com/zhanggaoxing/archive/2022/08/05/16554114.html
-Advertisement-
Play Games

在繼承中,派生類可以拿到基類的方法,若是派生類很多,且有時某部分派生類的部分實現邏輯是一樣的,但其他的派生類又用不到,這個時候這些邏輯若是全部寫到派生類中,就會導致產生很多的重覆邏輯,但是若是寫到基類中就會導致其他用不到當前邏輯的派生類也能調用,這樣就會導致代碼維護出現了問題。由此產生了介面。 在C ...


時間究竟是什麼?這既可以是一個哲學問題,也可以是一個物理問題。古人對太陽進行觀測,利用太陽的投影發明瞭日晷,定義了最初的時間。隨著科技的發展,天文觀測的精度也越來越準確,人們發現地球的自轉並不是完全一致的,這就導致每天經過的時間是不一樣的。這點誤差對於基本生活基本沒有影響,但是對於股票交易、火箭發射等等要求高精度時間的場景就無法忍受了。科學家們開始把觀測轉移到了微觀世界,找到了一種運動高度穩定的原子——銫,最終定義出了準確的時間:銫原子電子躍遷 9192631770 個周期所持續的時間長度定義為 1 秒。基於這個定義製造出了高度穩定的原子鐘。

時間在電腦中又是如何定義的呢?通常使用 Unix 時間戳進行表示,記錄的是自公元 1970 年 1 月 1 日 0 時 0 分 0 秒以來的秒數。電腦為了維持時鐘的走時,硬體層面使用晶體振蕩器保障時鐘的精確性(也是石英鐘的原理),操作系統層面使用時鐘中斷去更新時間的流逝。現代電腦的硬體設計通常有獨立的時鐘(RTC),這源於 Intel 和微軟創立的標準 High Precision Event Timer(HPET),標準指定了 10 MHz 的時鐘速度,因此時鐘可以獲得 100 納秒的解析度。這也是 .NET 時間有關的類型中 Ticks 屬性的由來,1秒 = 10000000 Ticks。雖然電腦的時鐘已經足夠精準,但也會受到環境溫度的影響造成過快或者過慢的問題。為了對電腦的時鐘進行校準,通常使用 NTP 協議與網路中的時間伺服器進行同步。時間伺服器的時間又會使用 GPS 接收機、無線電或者是原子鐘進行校準。

本文將從 GPS 時間的獲取、NTP 報文的編寫實現一個“玩具”級別的時間同步伺服器,使用 .NET 6 編寫一個控制台應用程式,通過本文你可以學到:

  1. 串口 SerialPort 類的使用;
  2. 使用 Socket 類實現 UDP 的監聽與回覆;
  3. 在程式中使用 Process 類執行命令行指令;
  4. 瞭解 GPS 數據報文的 NMEA-0183 協議;
  5. 瞭解 NTP 協議報文。

硬體需求

名稱 描述 數量
電腦 可以是運行 Linux 的開發板,也可以是運行 Windows 的電腦 x1
NEO-6M GPS 模塊 x1
USB 串口 可選,使用 USB 串口將 GPS 模塊與電腦相連 x1
杜邦線 感測器與開發板的連接線 若幹

電路

感測器 介面 開發板介面
NEO-6M TX 開發板或 USB 串口的RX
RX 開發板或 USB 串口的TX
VCC 5V
GND GND

GPS 數據報文的 NMEA-0183 協議

NMEA-0183 是 GPS 設備輸出信息的標準格式,是由美國國家海洋電子協會(National Marine Electronics Association)定製的標準。NMEA-0183 有多種不同的數據報文,每種都是獨立的 ASCII 字元串,使用逗號隔開數據,數據流長度從 30-100 字元不等,通常以每秒間隔選擇輸出。NMEA-0183 協議定義的語句非常多,但是常用的或者說相容性最廣的語句只有 $GPGGA$GPGSA$GPGSV$GPRMC$GPVTG 等。下麵給出這些常用 NMEA-0183 語句的解釋。

幀名稱 說明 最大幀長
$GPGGA 全球定位數據 72
$GPGSA 衛星 PRN 數據 65
$GPGSV 衛星狀態信息 210
$GPRMC 推薦最小數據 70
$GPVTG 地面速度信息 34

由於我們只需要從 GPS 中獲取時間信息,選擇包含時間信息的 “$GPRMC 推薦最小數據”幀進行解析:

$GPRMC <1> <2> <3> <4> <5> <6> <7> <8> <9> <10> <11> <12>*<13>
幀頭 UTC 時間 定位狀態 緯度 緯度半球 經度 經度半球 地面速率 地面航向 UTC 日期 磁偏角 磁偏角方向 模式 * 校驗和

下麵以一個真實的數據幀為例 $GPRMC,013717.00,A,3816.57392,N,10708.73951,E,0.467,,050722,,,A*78

$GPRMC 013717.00 A 3816.57392 N 10708.73951 E 0.467 050722 A*78
幀頭 UTC 時間 01:37:17 A=有效定位,V=無效定位 緯度 38 度 16.57392 分 北緯 經度 107 度 8.73951 分 東經 地面速率 0.467 節 航向 度 UTC 日期 2022/07/05 磁偏角 度 磁偏角方向 A=自主定位,N=數據無效

因此,通過串口讀取 $GPRMC 數據幀後,需要解析 <1><9> 欄位的值,並將其轉換為 UTC 時間。

細心的你也許會發現獲取到的時間信息只精確到秒,GPS 明明使用的是原子鐘,這是為什麼?仔細觀察手中的 GPS 模塊,還有一個 PPS 針腳沒有使用。PPS(Pulse Per Second)是秒脈衝,一般是由 GPS 接收機或原子鐘按秒發出的、寬度小於1秒、有著急升或突降邊沿的脈衝信號,通常用於精確計時和測量時間。PPS 信號能精確地(亞毫秒級)指示每一秒的開始時間,但不能指示對應現實時間的哪一秒,因此只能作為輔助信號,與衛星導航信息組合使用,提供低延遲、低抖動的授時服務。很遺憾,.NET 目前沒法直接操作 PPS 引腳,我們只能實現一個“玩具”級的時間同步伺服器了。

NTP 協議報文

NTP(Network Time Protocol),網路時間協議,是一種使用 UDP 的電腦之間進行時間同步的網路協議,位於 OSI 7 層網路模型中的應用層,預設使用的埠為 123。那麼使用 NTP 是如何進行時間同步的呢?簡單的說將發送的報文打上本機的時間戳,配合報文來回傳輸的時延修正本機的時間。如下圖所示,可以計算出網路傳輸時延 \(\delta\),以及客戶端與服務端的時間偏移 \(\theta\)

\(\delta=(t_3-t_0)-(t_2-t_1)\)

\(\theta=\frac{(t_1-t_0)+(t_2-t_3)}{2}\)

其中,\(t_0\) 是請求報文傳輸的客戶端時間戳,\(t_1\) 是請求報文接收的伺服器時間戳,\(t_2\) 是回覆報文傳輸的伺服器時間戳,\(t_3\) 是回覆報文接收的客戶端時間戳。客戶端和服務端都有一個時間軸,分別代表著各自系統的時間,當客戶端想要同步服務端的時間時,客戶端會構造一個 NTP 報文發送到服務端,客戶端會記下此時發送的時間 \(t_0\),經過一段網路延時傳輸後,伺服器在 \(t_1\) 時刻收到報文,經過一段時間處理後在 \(t_2\) 時刻向客戶端返回報文,再經過一段網路延時傳輸後客戶端在 \(t_3\) 時刻收到伺服器報文。這樣客戶端就可以校準自己的本機時間了。

在瞭解 NTP 同步時間的過程後,下麵解析 NTP 報文具體包含的欄位,一般的 NTP 報文長度為 48 位元組:

欄位 說明
LI 閏秒指示,2bit
Version NTP 版本,3bit
Mode 工作模式,3bit ,客戶端=0b011,伺服器=0b100
Stratum 時鐘層數,8bit,層數為 0 的設備為高精度的時鐘(如原子鐘),層數為 1 的設備與層數 0 的設備直接相連,……
Poll Interval 輪詢時間,8bit,連續 NTP 報文之間的最大時間間隔
Precision 時鐘精度,8bit
Root Delay 根時延,32bit,表示在主參考源之間往返的總共時延
Root Dispersion 根離散,32bit,相對於主參考源的標稱誤差
Reference ID 參考源的標識,32bit,4 個字元或 IP 地址
Reference Timestamp 參考時間戳,64bit,本地時鐘最後一次被更新的時間

| Originate Timestamp | 原始時間戳 \(t_0\),64bit,客戶端發送的時間 |
| Receive Timestamp | 接受時間戳 \(t_1\),64bit,服務端接受到的時間 |
| Transmit Timestamp | 傳送時間戳 \(t_2\),64bit,服務端發送的時間 |

其中要註意的是 NTP 時間戳的起始時間是 1900-01-01 00:00:00,而不是 Unix 時間戳的起始時間 1970-01-01 00:00:00

下麵是使用 Wireshark 抓取的 Windows 時鐘同步的 NTP 報文:

編寫代碼

項目地址:https://github.com/ZhangGaoxing/gps-ntp

項目結構

創建一個控制台應用和類庫,項目結構如下:

項目依賴

添加如下 NuGet 包引用:

<ItemGroup>
   <PackageReference Include="System.IO.Ports" Version="6.0.0" />
</ItemGroup>

配置串口讀取 GPS 數據

絕大部分 GPS 模塊每秒會通過串口輸出 NMEA-0183 協議報文,因此我們只需要通過串口讀取需要的時間數據即可。此環節包含 3 個步驟:

  1. 初始化串口;
  2. 讀取 $GPRMC 數據幀的內容,提取時間信息;
  3. 更新系統時間。

初始化串口

使用串口時最重要的屬性是波特率,請查閱對應 GPS 模塊的數據手冊,這裡使用的 NEO-6M 模塊的波特率是 9600。串口的名稱取決於你的連接方式,在 Linux 中串口對應的驅動文件在 /dev 目錄下,使用內置串口可能的文件名稱為 ttySx,使用 USB 串口可能的文件名稱為 ttyUSBx,在 Windows 中串口的名稱為 COMx,其中 x 表示的是數字編號。

// 使用的串口名稱
const string SERIAL_NAME = "/dev/ttyUSB0";
using SerialPort gps = new SerialPort(SERIAL_NAME)
{
    BaudRate = 9600,
    Encoding = Encoding.UTF8,
    ReadTimeout = 500,
    WriteTimeout = 500,
};

從串口中獲取數據

從串口中讀取數據時使用的是 SerialPort 類中的 DataReceived 事件。事件(event)可以理解為一種廣播,當完成某種操作後向外發送通知。即串口接收到數據後,觸發數據處理事件。

gps.DataReceived += GpsFrameReceived;

/// <summary>
/// GPS 報文處理
/// </summary>
void GpsFrameReceived(object sender, SerialDataReceivedEventArgs e)
{
    // TODO:讀取 `$GPRMC` 數據幀;提取時間;更新系統時間
}

由於 GPS 模塊輸出的不只有 $GPRMC 數據幀,因此需要在處理事件中判斷幀頭以及幀的有效性。

void GpsFrameReceived(object sender, SerialDataReceivedEventArgs e)
{
    string frame = gps.ReadLine();

    if (frame.StartsWith("$GPRMC"))
    {
        // $GPRMC,UTC 時間,定位狀態,緯度,緯度半球,經度,經度半球,速度,航向,UTC 日期,磁偏角,磁偏角方向,指示模式*校驗和
        // $GPRMC,013717.00,A,3816.57392,N,10708.73951,E,0.467,,050722,,,A*78
        string[] field = frame.Split(',');

        // 幀數據有效
        if (!field[12].StartsWith("N"))
        {
            // TODO:提取時間;更新系統時間
        }
    }
}

在驗證 $GPRMC 數據幀有效後,根據幀解析提取對應欄位的時間信息。

void GpsFrameReceived(object sender, SerialDataReceivedEventArgs e)
{
    string frame = gps.ReadLine();

    if (frame.StartsWith("$GPRMC"))
    {
        string[] field = frame.Split(',');

        if (!field[12].StartsWith("N"))
        {
            // 獲取 GPS 時間
            string time = field[1][0..6];
            string date = field[9];
            DateTime utcNow = DateTime.ParseExact($"{date}{time}", "ddMMyyHHmmss", CultureInfo.InvariantCulture);

            // TODO:更新系統時間
        }
    }
}

更新系統時間

由於 .NET 並不提供修改系統時間的操作,因此我們要使用間接的方式修改系統時間。一種方式是使用 P/Invoke 調用 C++ 的函數,這種方式可以精確的修改時間,但涉及引用、數據類型轉換,過於複雜,和本入門指南不符。這裡使用的是運行命令行指令的方式修改系統的時間,但修改時間的精度只能精確到秒。在 Windows 中使用 PowerShellSet-Date 命令,在 Linux 中使用 date 命令。

/// <summary>
/// 更新系統時間
/// </summary>
void UpdateSystemTime(DateTime time)
{
    ProcessStartInfo processInfo;
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        processInfo = new ProcessStartInfo
        {
            FileName = "powershell.exe",
            Arguments = $"Set-Date \"\"\"{time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")}\"\"\"",
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
        };
    }
    else
    {
        processInfo = new ProcessStartInfo
        {
            FileName = "date",
            Arguments = $"-s \"{time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")}\"",
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
        };
    }

    var process = Process.Start(processInfo);
    process.WaitForExit();
}

最終報文處理事件由以下代碼構成:

void GpsFrameReceived(object sender, SerialDataReceivedEventArgs e)
{
    string frame = gps.ReadLine();

    if (frame.StartsWith("$GPRMC"))
    {
        string[] field = frame.Split(',');

        if (!field[12].StartsWith("N"))
        {
            string time = field[1][0..6];
            string date = field[9];
            DateTime utcNow = DateTime.ParseExact($"{date}{time}", "ddMMyyHHmmss", CultureInfo.InvariantCulture);

            UpdateSystemTime(utcNow);

            // 記錄時鐘最後一次被更新的時間
            lastUpdatedTime = utcNow;
        }
    }
}

使用 gps.Open(); 打開串口後就可以獲取時間數據了。

實現 NTP 服務

下麵使用 Socket 類實現一個簡單的 UDP 伺服器,用於監聽和回覆 NTP 報文。

初始化 UDP 服務

// NTP 服務初始化
using Socket ntpServer = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint ip = new IPEndPoint(IPAddress.Any, 123);
ntpServer.Bind(ip);

監聽和回覆 NTP 報文

在後臺新建一個進程用於監聽 NTP 請求報文:

new Thread(NtpFrameReceived)
{
    IsBackground = true
}.Start();

/// <summary>
/// NTP 報文接收與回覆
/// </summary>
void NtpFrameReceived()
{
    // 存儲接收到的 NTP 請求報文
    Span<byte> receiveFrame = stackalloc byte[48];

    while (true)
    {
        // 接收請求報文
        EndPoint clientPoint = new IPEndPoint(IPAddress.Any, 0);
        ntpServer.ReceiveFrom(receiveFrame, ref clientPoint);
        DateTime receiveTime = DateTime.UtcNow;

        // TODO:回覆 NTP 報文
    }
}

根據幀解析生成 NTP 回覆報文:

/// <summary>
/// 生成 NTP 報文
/// </summary>
Span<byte> GenerateNtpFrame(Span<byte> receivedFrame, DateTime receiveTime)
{
    Span<byte> ntpFrame = stackalloc byte[48]
    {
        0x1c, 0x01, 0x11, 0xe9, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    };

    // Client Transmit Timestamp => Server Origin Timestamp
    for (int i = 0; i < 8; i++)
    {
        ntpFrame[24 + i] = receivedFrame[40 + i];
    }

    // 本機時鐘最後更新時間
    long referenceTicks = (lastUpdatedTime - ntpStart).Ticks;
    uint referenceTimeInt = (uint)(referenceTicks / TICK_2_SECOND);
    uint referenceTimeFract = (uint)(referenceTicks % TICK_2_SECOND);
    var referenceTimeIntByte = BitConverter.GetBytes(referenceTimeInt);
    var referenceTimeFractByte = BitConverter.GetBytes(referenceTimeFract);

    // 接收報文時間
    long receiveTicks = (receiveTime - ntpStart).Ticks;
    uint receiveTimeInt = (uint)(receiveTicks / TICK_2_SECOND);
    uint receiveTimeFract = (uint)(receiveTicks % TICK_2_SECOND);
    var receiveTimeIntByte = BitConverter.GetBytes(receiveTimeInt);
    var receiveTimeFractByte = BitConverter.GetBytes(receiveTimeFract);

    // 發送報文時間
    long transmitTicks = (DateTime.UtcNow - ntpStart).Ticks;
    uint transmitTimeInt = (uint)(receiveTicks / TICK_2_SECOND);
    uint transmitTimeFract = (uint)(receiveTicks % TICK_2_SECOND);
    var transmitTimeIntByte = BitConverter.GetBytes(receiveTimeInt);
    var transmitTimeFractByte = BitConverter.GetBytes(receiveTimeFract);

    if (BitConverter.IsLittleEndian)
    {
        for (int i = 0; i < 4; i++)
        {
            ntpFrame[19 - i] = referenceTimeIntByte[i];
            ntpFrame[23 - i] = referenceTimeFractByte[i];

            ntpFrame[35 - i] = receiveTimeIntByte[i];
            ntpFrame[39 - i] = receiveTimeFractByte[i];

            ntpFrame[43 - i] = transmitTimeIntByte[i];
            ntpFrame[47 - i] = transmitTimeFractByte[i];
        }
    }
    else
    {
        for (int i = 0; i < 4; i++)
        {
            ntpFrame[16 + i] = referenceTimeIntByte[i];
            ntpFrame[20 + i] = referenceTimeFractByte[i];

            ntpFrame[32 + i] = receiveTimeIntByte[i];
            ntpFrame[36 + i] = receiveTimeFractByte[i];

            ntpFrame[40 + i] = transmitTimeIntByte[i];
            ntpFrame[44 + i] = transmitTimeFractByte[i];
        }
    }

    return ntpFrame.ToArray();
}

最終報文請求與回覆由以下代碼構成:

void NtpFrameReceived()
{
    Span<byte> receiveFrame = stackalloc byte[48];

    while (true)
    {
        EndPoint clientPoint = new IPEndPoint(IPAddress.Any, 0);
        ntpServer.ReceiveFrom(receiveFrame, ref clientPoint);
        DateTime receiveTime = DateTime.UtcNow;

        // 回覆 NTP 報文
        Span<byte> sendFrame = GenerateNtpFrame(receiveFrame, DateTime.UtcNow);
        ntpServer.SendTo(sendFrame, clientPoint);
        DateTime sendTime = DateTime.UtcNow;
    }
}

將上述代碼進行整合就構成了基於 GPS 的 NTP 時間同步伺服器。

部署應用

發佈到文件

  1. 切換到 GpsNtp 項目運行發佈命令:
dotnet publish -c release -r linux-x64 --no-self-contained
  1. 將發佈後的文件通過 FTP 等方式複製到 Linux 開發板;
  2. GpsNtp 文件增加可執行許可權
sudo chmod +x GpsNtp
  1. 運行程式
sudo ./GpsNtp

構建 Docker 鏡像

  1. 在項目的根目錄中創建 Dockerfile,並將整個項目複製到 Linux 開發板中:
FROM mcr.microsoft.com/dotnet/core/sdk:6.0-focal AS build
WORKDIR /app

# publish app
COPY src .
WORKDIR /app/GpsNtp
RUN dotnet restore
RUN dotnet publish -c release -r linux-arm -o out

# run app
FROM mcr.microsoft.com/dotnet/core/runtime:6.0-focal AS runtime
WORKDIR /app
COPY --from=build /app/GpsNtp/out ./

ENTRYPOINT ["dotnet", "GpsNtp.dll"]
  1. 切換到項目目錄,構建鏡像:
docker build -t gps-ntp -f Dockerfile .
  1. 運行鏡像:
docker run --rm -it --device /dev/ttySx gps-ntp

程式運行後,使用 Windows 時間同步服務進行一下測試。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 雙向鏈表與數據結構 引言 在上小節中 我們分析了ArrayList的底層實現, 知道了ArrayList底層是基於數組實現的,因此具有查找修改快而插入、刪除慢的特點 本章我們介紹的LinkedList是List介面的另一種實現 它的底層是基於雙向鏈表實現的 因此它具有插入、刪除快而查找修改慢的特點 ...
  • 大家好,我是Mic,一個工作了14年的Java程式員。 最近很多小伙伴私信我,讓我說一些線程池相關的問題。 線程池這個方向考察的點還挺多的,如果只是靠刷面試題 面試官很容易就能識別出來,我隨便舉幾個。 線程池是如何實現線程的回收的 核心線程是否能夠回收 當調用線程池的shutdown方法,會發生什麼 ...
  • 前言 最近複習操作系統,看到了lru演算法,就去網上搜索下,因此發現了GeeCache,順手寫了一遍。研究下lru演算法的實現。 正文: lru使用map+鏈表實現。map裡面存儲了key以及其對應的鏈表節點。當我們根據某個key訪問緩存值的時候,可以經過map快速定位到該鏈表節點。從而獲取值 下麵我們 ...
  • 什麼是商城系統?商城系統又稱線上商城系統,是一個功能完善的線上購物系統,主要為線上銷售和線上購物服務。 一般的商城系統運營模式有B2C單商戶商城系統,B2B2C多商戶商城系統以及SAAS運營版。但是搭建一個商城系統過程很麻煩,這時候我們這些現成的來幫忙啦!likeshop是一個100%開源免費商用而 ...
  • 繼 Tabby、Warp 後,今天再來給大家推薦一款終端神器——WindTerm,完全開源,在 GitHub 上已經收穫 6.6k 的 star。 https://github.com/kingToolbox/WindTerm 作者還拿 WindTerm 和 Putty、xterm、Windows ...
  • 一、前言 我們在實際開發中肯定會遇到後端的時間傳到前端是這個樣子的:2022-08-02T15:43:50 這個時候前後端就開始踢皮球了,!! 後端說:前端來做就可! 前端說:後端來做就可! 作為一名有責任感的後端,這種事情怎麼能讓前端來搞呢! 還有就是Long類型的返回到前端可能會損失精度,這個情 ...
  • 在上一遍文章中已經介紹了PixelShaderEffect 用hlsl(著色器) 可以實現各種自定義濾鏡效果了,本文將用 "ThresholdEffect" 來講解如何編寫,編譯hlsl,然後使用PixelShaderEffect製作自定義濾鏡。 效果圖: 一.hlsl幫助程式介紹 在寫hlsl 代 ...
  • 簡介 FTP是FileTransferProtocol(文件傳輸協議)的英文簡稱,而中文簡稱為“文傳協議”。用於Internet上的控制文件的雙向傳輸。同時,它也是一個應用程式(Application)。基於不同的操作系統有不同的FTP應用程式,而所有這些應用程式都遵守同一種協議以傳輸文件。 FTP ...
一周排行
    -Advertisement-
    Play Games
  • ## 引言 最近發現自己喜歡用的 Todo 軟體總是差點意思,畢竟每個人的習慣和工作流不太一樣,我就想著自己寫一個小的[Todo 項目]( https://github.com/circler3/TodoTrack ),核心的功能是自動記錄 Todo 執行過程中消耗的時間(尤其面向程式員),按照自己 ...
  • ### 前言 當我們編寫 C# 代碼時,經常需要處理大量的數據集合。在傳統的方式中,我們往往需要先將整個數據集合載入到記憶體中,然後再進行操作。但是如果數據集合非常大,這種方式就會導致記憶體占用過高,甚至可能導致程式崩潰。 C# 中的`yield return`機制可以幫助我們解決這個問題。通過使用`y ...
  • 1. ADO.NET的前世今生 ADO.NET的名稱起源於ADO(ActiveX Data Objects),是一個COM組件庫,用於在以往的Microsoft技術中訪問數據。之所以使用ADO.NET名稱,是因為Microsoft希望表明,這是在NET編程環境中優先使用的數據訪問介面。 ADO.NE ...
  • 1. 為什麼需要單元測試 在我們之前,測試某些功能是否能夠正常運行時,我們都將代碼寫到Main方法中,當我們測試第二個功能時,我們只能選擇將之前的代碼清掉,重新編寫。此時,如果你還想重新測試你之前的功能時,這時你就顯得有些難為情了,因為代碼都被你清掉了。當然你完全可以把代碼寫到一個記事本中進行記錄, ...
  • 1. 透過現象看本質 反射被譽為是 c#中的黑科技 ,在很多領域中都有反射的身影,例如,我們經常使用的ORM框架,ABP框架 等。 反射指程式可以訪問、檢測和修改它本身狀態或行為的一種能力。. 程式集包含模塊,而模塊包含類型,類型又包含成員。. 反射則提供了封裝程式集、模塊和類型的對象。. 您可以使 ...
  • # Rust Web 全棧開發之 Web Service 中的錯誤處理 ## Web Service 中的統一錯誤處理 ### Actix Web Service 自定義錯誤類型 -> 自定義錯誤轉為 HTTP Response - 資料庫 - 資料庫錯誤 - 串列化 - serde 錯誤 - I/ ...
  • 在前面的幾篇文章中,詳細地給大家介紹了Java里的集合。但在介紹集合時,我們涉及到了泛型的概念卻並沒有詳細學習,所以今天我們要花點時間給大家專門講解什麼是泛型、泛型的作用、用法、特點等內容 ...
  • ###BIO:同步阻塞 主線程發起io請求後,需要等待當前io操作完成,才能繼續執行。 ###NIO:同步非阻塞 引入selector、channel、等概念,當主線程發起io請求後,輪詢的查看系統是否準備好執行io操作,沒有準備好則主線程不會阻塞會繼續執行,準備好主線程會阻塞等待io操作完成。 # ...
  • 摘要:在讀多寫少的環境中,有沒有一種比ReadWriteLock更快的鎖呢?有,那就是JDK1.8中新增的StampedLock! 本文分享自華為雲社區《【高併發】高併發場景下一種比讀寫鎖更快的鎖》,作者: 冰 河。 什麼是StampedLock? ReadWriteLock鎖允許多個線程同時讀取共 ...
  • ## 併發與並行😣 ### 併發與並行的概念和區別 並行:同一個時間段內多個任務同時在不同的CPU核心上執行。強調同一時刻多個任務之間的”**同時執行**“。 併發:同一個時間段內多個任務都在進展。強調多個任務間的”**交替執行**“。 ![](https://img2023.cnblogs.co ...