主頁 > 後端開發 > Golang Context 的原理與實戰

Golang Context 的原理與實戰

2020-09-12 21:57:03 後端開發

本文讓我們一起來學習 golang Context 的使用和標準庫中的Context的實作,

golang context 包 一開始只是 Google 內部使用的一個 Golang 包,在 Golang 1.7的版本中正式被引入標準庫,下面開始學習,

簡單介紹

在學習 context 包之前,先看幾種日常開發中經常會碰到的業務場景:

  1. 業務需要對訪問的資料庫,RPC ,或API介面,為了防止這些依賴導致我們的服務超時,需要針對性的做超時控制,
  2. 為了詳細了解服務性能,記錄詳細的呼叫鏈Log,

上面兩種場景在web中是比較常見的,context 包就是為了方便我們應對此類場景而使用的,

接下來, 我們首先學習 context 包有哪些方法供我們使用;接著舉一些例子,使用 context 包應用在我們上述場景中去解決我們遇到的問題;最后從原始碼角度學習 context 內部實作,了解 context 的實作原理,

Context 包

Context 定義

context 包中實作了多種 Context 物件,Context 是一個介面,用來描述一個程式的背景關系,介面中提供了四個抽象的方法,定義如下:

type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key interface{}) interface{}
}
  • Deadline() 回傳的是背景關系的截至時間,如果沒有設定,ok 為 false
  • Done() 當執行的背景關系被取消后,Done回傳的chan就會被close,如果這個背景關系不會被取消,回傳nil
  • Err() 有幾種情況:
    • 如果Done() 回傳 chan 沒有關閉,回傳nil
    • 如果Done() 回傳的chan 關閉了, Err 回傳一個非nil的值,解釋為什么會Done()
      • 如果Canceled,回傳 "Canceled"
      • 如果超過了 Deadline,回傳 "DeadlineEsceeded"
  • Value(key) 回傳背景關系中 key 對應的 value 值

Context 構造

為了使用 Context,我們需要了解 Context 是怎么構造的,

Context 提供了兩個方法做初始化:

func Background() Context{}
func TODO() Context {}

上面方法均會回傳空的 Context,但是 Background 一般是所有 Context 的基礎,所有 Context 的源頭都應該是它,TODO 方法一般用于當傳入的方法不確定是哪種型別的 Context 時,為了避免 Context 的引數為nil而初始化的 Context,

其他的 Context 都是基于已經構造好的 Context 來實作的,一個 Context 可以派生多個子 context,基于 Context 派生新Context 的方法如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc){}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}

上面三種方法比較類似,均會基于 parent Context 生成一個子 ctx,以及一個 Cancel 方法,如果呼叫了cancel 方法,ctx 以及基于 ctx 構造的子 context 都會被取消,不同點在于 WithCancel 必需要手動呼叫 cancel 方法,WithDeadline
可以設定一個時間點,WithTimeout 是設定呼叫的持續時間,到指定時間后,會呼叫 cancel 做取消操作,

除了上面的構造方式,還有一類是用來創建傳遞 traceId, token 等重要資料的 Context,

func WithValue(parent Context, key, val interface{}) Context {}

withValue 會構造一個新的context,新的context 會包含一對 Key-Value 資料,可以通過Context.Value(Key) 獲取存在 ctx 中的 Value 值,

通過上面的理解可以直到,Context 是一個樹狀結構,一個 Context 可以派生出多個不一樣的Context,我們大概可以畫一個如下的樹狀圖:

一個background,衍生出一個帶有traceId的valueCtx,然后valueCtx衍生出一個帶有cancelCtx
的context,最終在一些db查詢,http查詢,rpc沙遜等異步呼叫中體現,如果出現超時,直接把這些異步呼叫取消,減少消耗的資源,我們也可以在呼叫時,通過Value 方法拿到traceId,并記錄下對應請求的資料,

當然,除了上面的幾種 Context 外,我們也可以基于上述的 Context 介面實作新的Context.

使用方法

下面我們舉幾個例子,學習上面講到的方法,

超時查詢的例子

在做資料庫查詢時,需要對資料的查詢做超時控制,例如:

ctx = context.WithTimeout(context.Background(), time.Second)
rows, err := pool.QueryContext(ctx, "select * from products where id = ?", 100)

上面的代碼基于 Background 派生出一個帶有超時取消功能的ctx,傳入帶有context查詢的方法中,如果超過1s未回傳結果,則取消本次的查詢,使用起來非常方便,為了了解查詢內部是如何做到超時取消的,我們看看DB內部是如何使用傳入的ctx的,

在查詢時,需要先從pool中獲取一個db的鏈接,代碼大概如下:

// src/database/sql/sql.go
// func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) *driverConn, error)

// 阻塞從req中獲取鏈接,如果超時,直接回傳
select {
case <-ctx.Done():
  // 獲取鏈接超時了,直接回傳錯誤
  // do something
  return nil, ctx.Err()
case ret, ok := <-req:
  // 拿到鏈接,校驗并回傳
  return ret.conn, ret.err
}

req 也是一個chan,是等待鏈接回傳的chan,如果Done() 回傳的chan 關閉后,則不再關心req的回傳了,我們的查詢就超時了,

在做SQL Prepare、SQL Query 等操作時,也會有類似方法:

select {
default:
// 校驗是否已經超時,如果超時直接回傳
case <-ctx.Done():
  return nil, ctx.Err()
}
// 如果還沒有超時,呼叫驅動做查詢
return queryer.Query(query, dargs)

上面在做查詢時,首先判斷是否已經超時了,如果超時,則直接回傳錯誤,否則才進行查詢,

可以看出,在派生出的帶有超時取消功能的 Context 時,內部方法在做異步操作(比如獲取鏈接,查詢等)時會先查看是否已經
Done了,如果Done,說明請求已超時,直接回傳錯誤;否則繼續等待,或者做下一步作業,這里也可以看出,要做到超時控制,需要不斷判斷 Done() 是否已關閉,

鏈路追蹤的例子

在做鏈路追蹤時,Context 也是非常重要的,(所謂鏈路追蹤,是說可以追蹤某一個請求所依賴的模塊,比如db,redis,rpc下游,介面下游等服務,從這些依賴服務中找到請求中的時間消耗)

下面舉一個鏈路追蹤的例子:

// 建議把key 型別不匯出,防止被覆寫
type traceIdKey struct{}{}

// 定義固定的Key
var TraceIdKey = traceIdKey{}

func ServeHTTP(w http.ResponseWriter, req *http.Request){
  // 首先從請求中拿到traceId
  // 可以把traceId 放在header里,也可以放在body中
  // 還可以自己建立一個 (如果自己是請求源頭的話)
  traceId := getTraceIdFromRequest(req)

  // Key 存入 ctx 中
  ctx := context.WithValue(req.Context(), TraceIdKey, traceId)

  // 設定介面1s 超時
  ctx = context.WithTimeout(ctx, time.Second)

  // query RPC 時可以攜帶 traceId
  repResp := RequestRPC(ctx, ...)

  // query DB 時可以攜帶 traceId
  dbResp := RequestDB(ctx, ...)

  // ...
}

func RequestRPC(ctx context.Context, ...) interface{} {
    // 獲取traceid,在呼叫rpc時記錄日志
    traceId, _ := ctx.Value(TraceIdKey)
    // request

    // do log
    return
}

上述代碼中,當拿到請求后,我們通過req 獲取traceId, 并記錄在ctx中,在呼叫RPC,DB等時,傳入我們構造的ctx,在后續代碼中,我們可以通過ctx拿到我們存入的traceId,使用traceId 記錄請求的日志,方便后續做問題定位,

當然,一般情況下,context 不會單純的僅僅是用于 traceId 的記錄,或者超時的控制,很有可能二者兼有之,

如何實作

知其然也需知其所以然,想要充分利用好 Context,我們還需要學習 Context 的實作,下面我們一起學習不同的 Context 是如何實作 Context 介面的,

空背景關系

Background(), Empty() 均會回傳一個空的 Context emptyCtx,emptyCtx 物件在方法 Deadline(), Done(), Err(), Value(interface{}) 中均會回傳nil,String() 方法會回傳對應的字串,這個實作比較簡單,我們這里暫時不討論,

有取消功能的背景關系

WithCancel 構造的context 是一個cancelCtx實體,代碼如下,

type cancelCtx struct {
  Context

  // 互斥鎖,保證context協程安全
  mu       sync.Mutex
  // cancel 的時候,close 這個chan
  done     chan struct{}
  // 派生的context
  children map[canceler]struct{}
  err      error
}

WithCancel 方法首先會基于 parent 構建一個新的 Context,代碼如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
  c := newCancelCtx(parent)  // 新的背景關系
  propagateCancel(parent, &c) // 掛到parent 上
  return &c, func() { c.cancel(true, Canceled) }
}

其中,propagateCancel 方法會判斷 parent 是否已經取消,如果取消,則直接呼叫方法取消;如果沒有取消,會在parent的children 追加一個child,這里就可以看出,context 樹狀結構的實作, 下面是propateCancel 的實作:

// 把child 掛在到parent 下
func propagateCancel(parent Context, child canceler) {
  // 如果parent 為空,則直接回傳
  if parent.Done() == nil {
    return // parent is never canceled
  }
  
  // 獲取parent型別
  if p, ok := parentCancelCtx(parent); ok {
    p.mu.Lock()
    if p.err != nil {
      // parent has already been canceled
      child.cancel(false, p.err)
    } else {
      if p.children == nil {
        p.children = make(map[canceler]struct{})
      }
      p.children[child] = struct{}{}
    }
    p.mu.Unlock()
  } else {
    // 啟動goroutine,等待parent/child Done
    go func() {
      select {
      case <-parent.Done():
        child.cancel(false, parent.Err())
      case <-child.Done():
      }
    }()
  }
}

Done() 實作比較簡單,就是回傳一個chan,等待chan 關閉,可以看出 Done 操作是在呼叫時才會構造 chan done,done 變數是延時初始化的,

func (c *cancelCtx) Done() <-chan struct{} {
  c.mu.Lock()
  if c.done == nil {
    c.done = make(chan struct{})
  }
  d := c.done
  c.mu.Unlock()
  return d
}

在手動取消 Context 時,會呼叫 cancelCtx 的 cancel 方法,代碼如下:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  // 一些判斷,關閉 ctx.done chan
  // ...
  if c.done == nil {
    c.done = closedchan
  } else {
    close(c.done)
  }

  // 廣播到所有的child,需要cancel goroutine 了
  for child := range c.children {
    // NOTE: acquiring the child's lock while holding parent's lock.
    child.cancel(false, err)
  }
  c.children = nil
  c.mu.Unlock()

  // 然后從父context 中,洗掉當前的context
  if removeFromParent {
    removeChild(c.Context, c)
  }
}

這里可以看到,當執行cancel時,除了會關閉當前的cancel外,還做了兩件事,① 所有的child 都呼叫cancel方法,② 由于該背景關系已經關閉,需要從父背景關系中移除當前的背景關系,

定時取消功能的背景關系

WithDeadline, WithTimeout 提供了實作定時功能的 Context 方法,回傳一個timerCtx結構體,WithDeadline 是給定了執行截至時間,WithTimeout 是倒計時時間,WithTImeout 是基于WithDeadline實作的,因此我們僅看其中的WithDeadline
即可,WithDeadline 內部實作是基于cancelCtx 的,相對于 cancelCtx 增加了一個計時器,并記錄了 Deadline 時間點,下面是timerCtx 結構體:

type timerCtx struct {
  cancelCtx
  // 計時器
  timer *time.Timer
  // 截止時間
  deadline time.Time
}

WithDeadline 的實作:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  // 若父背景關系結束時間早于child,
  // 則child直接掛載在parent背景關系下即可
  if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    return WithCancel(parent)
  }

  // 創建個timerCtx, 設定deadline
  c := &timerCtx{
    cancelCtx: newCancelCtx(parent),
    deadline:  d,
  }

  // 將context掛在parent 之下
  propagateCancel(parent, c)

  // 計算倒計時時間
  dur := time.Until(d)
  if dur <= 0 {
    c.cancel(true, DeadlineExceeded) // deadline has already passed
    return c, func() { c.cancel(false, Canceled) }
  }
  c.mu.Lock()
  defer c.mu.Unlock()
  if c.err == nil {
    // 設定一個計時器,到時呼叫cancel
    c.timer = time.AfterFunc(dur, func() {
      c.cancel(true, DeadlineExceeded)
    })
  }
  return c, func() { c.cancel(true, Canceled) }
}

構造方法中,將新的context 掛在到parent下,并創建了倒計時器定期觸發cancel,

timerCtx 的cancel 操作,和cancelCtx 的cancel 操作是非常類似的,在cancelCtx 的基礎上,做了關閉定時器的操作

func (c *timerCtx) cancel(removeFromParent bool, err error) {
  // 呼叫cancelCtx 的cancel 方法 關閉chan,并通知子context,
  c.cancelCtx.cancel(false, err)
  // 從parent 中移除
  if removeFromParent {
    removeChild(c.cancelCtx.Context, c)
  }
  c.mu.Lock()
  // 關掉定時器
  if c.timer != nil {
    c.timer.Stop()
    c.timer = nil
  }
  c.mu.Unlock()
}

timeCtx 的 Done 操作直接復用了cancelCtx 的 Done 操作,直接關閉 chan done 成員,

傳遞值的背景關系

WithValue 構造的背景關系與上面幾種有區別,其構造的context 原型如下:

type valueCtx struct {
  // 保留了父節點的context
  Context
  key, val interface{}
}

每個context 包含了一個Key-Value組合,valueCtx 保留了父節點的Context,但沒有像cancelCtx 一樣保留子節點的Context. 下面是valueCtx的構造方法:

func WithValue(parent Context, key, val interface{}) Context {
  if key == nil {
    panic("nil key")
  }
  // key 必須是課比較的,不然無法獲取Value
  if !reflect.TypeOf(key).Comparable() {
    panic("key is not comparable")
  }
  return &valueCtx{parent, key, val}
}

直接將Key-Value賦值給struct 即可完成構造,下面是獲取Value 的方法:

func (c *valueCtx) Value(key interface{}) interface{} {
  if c.key == key {
    return c.val
  }
  // 從父context 中獲取
  return c.Context.Value(key)
}

Value 的獲取是采用鏈式獲取的方法,如果當前 Context 中找不到,則從父Context中獲取,如果我們希望一個context 多放幾條資料時,可以保存一個map 資料到 context 中,這里不建議多次構造context來存放資料,畢竟取資料的成本也是比較高的,

注意事項

最后,在使用中應該注意如下幾點:

  • context.Background 用在請求進來的時候,所有其他context 來源于它,
  • 在傳入的conttext 不確定使用的是那種型別的時候,傳入TODO context (不應該傳入一個nil 的context)
  • context.Value 不應該傳入可選的引數,應該是每個請求都一定會自帶的一些資料,(比如說traceId,授權token 之類的),在Value 使用時,建議把Key 定義為全域const 變數,并且key 的型別不可匯出,防止資料存在沖突,
  • context goroutines 安全,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/17403.html

標籤:Go

上一篇:10. Go 語言流程控制:for 回圈

下一篇:金九銀十刷題必備,面試4大難點:JVM+微服務+MySQL+Redis

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more