淺談如何使用 github.com/yuin/gopher-lua

来源:https://www.cnblogs.com/huageyiyangdewo/archive/2023/05/13/17397667.html
-Advertisement-
Play Games

最近熟悉 go 項目時,發現項目中有用到 github.com/yuin/gopher-lua 這個包,之前並沒有接觸過,特意去看了官方文檔和找了些網上的資料,特此記錄下。 本次介紹計劃分為兩篇文章,這一次主要介紹 github.com/yuin/gopher-lua 這個包的介紹以及基礎使用,下一 ...


最近熟悉 go 項目時,發現項目中有用到 github.com/yuin/gopher-lua 這個包,之前並沒有接觸過,特意去看了官方文檔和找了些網上的資料,特此記錄下。

本次介紹計劃分為兩篇文章,這一次主要介紹 github.com/yuin/gopher-lua 這個包的介紹以及基礎使用,下一邊將介紹 github.com/yuin/gopher-lua 是如何在項目中使用的。如有不對的地方,請不吝賜教,謝謝。

文章中的 gopher-lua 如果沒有特別說明,即為:github.com/yuin/gopher-lua。

1、 gopher-lua 基礎介紹

我們先開看看官方是如何介紹自己的:

GopherLua is a Lua5.1(+ goto statement in Lua5.2) VM and compiler written in Go. GopherLua has a same goal with Lua: Be a scripting language with extensible semantics . It provides Go APIs that allow you to easily embed a scripting language to your Go host programs.

GopherLua是一個Lua5.1(Lua5.2中的+goto語句)虛擬機和用Go編寫的編譯器。GopherLua與Lua有著相同的目標:成為一種具有可擴展語義的腳本語言。它提供了Go API,允許您輕鬆地將腳本語言嵌入到Go主機程式中。

看上面的翻譯還是有點抽象,說說自己的理解。 github.com/yuin/gopher-lua 是一個純 Golang 實現的 Lua 虛擬機,它能夠很輕鬆的在 go 寫的程式中調用 lua 腳本。另外提一嘴,使用插件後,也能夠在 lua 腳本中調用 go 寫好的代碼。挺秀的!

接下來我們看一看, github.com/yuin/gopher-lua 的性能如何,這裡就直接引用官方自己做的測試來介紹。詳情見 wiki page 鏈接。點進鏈接過後,發現性能還不錯,執行效率和性能僅比 C 實現的 bindings 差點。

官方測試例子是生成斐波那契數列,測試執行結果如下:

prog time
anko 182.73s
otto 173.32s
go-lua 8.13s
Python3.4 5.84s
GopherLua 5.40s
lua5.1.4 1.71s

2、 gopher-lua 基礎介紹

下麵的介紹,都是基於 v1.1.0 版本進行的。

go get github.com/yuin/[email protected]

Go的版本需要 >= 1.9

2.1 gopher-lua 中的 hello world

這裡寫一個簡單的程式,瞭解 gopher-lua 是如何使用的。

package main

import (
	lua "github.com/yuin/gopher-lua"
)

func main() {
	// 1、創建 lua 的虛擬機
	L := lua.NewState()
	// 執行完畢後關閉虛擬機
	defer L.Close()
	// 2、載入fib.lua
	if err := L.DoString(`print("hello world")`); err != nil {
		panic(err)
	}

}

執行結果:

hello world

看到這裡,感覺沒啥特別的地方,接下來,我們看一看 gopher-lua 如何調用事先寫好的 lua腳本

fib.lua 腳本內容:

function fib(n)
    if n < 2 then return n end
    return fib(n-1) + fib(n-2)
end

main.go

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"
)

func main() {
	// 1、創建 lua 的虛擬機
	L := lua.NewState()
	defer L.Close()
	// 載入fib.lua
	if err := L.DoFile(`fib.lua`); err != nil {
		panic(err)
	}
	// 調用fib(n)
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("fib"), // 獲取fib函數引用
		NRet:    1,                  // 指定返回值數量
		Protect: true,               // 如果出現異常,是panic還是返回err
	}, lua.LNumber(10)) // 傳遞輸入參數n
	if err != nil {
		panic(err)
	}
	// 獲取返回結果
	ret := L.Get(-1)
	// 從堆棧中扔掉返回結果
    // 這裡一定要註意,不調用此方法,後續再調用 L.Get(-1) 獲取的還是上一次執行的結果
    // 這裡大家可以自己測試下
	L.Pop(1)
	// 列印結果
	res, ok := ret.(lua.LNumber)
	if ok {
		fmt.Println(int(res))
	} else {
		fmt.Println("unexpected result")
	}
}

執行結果:

55

從上面我們已經能夠感受到部分 gopher-lua 的魅力了。接下來,我們就一起詳細的學習學習 gopher-lua

2.2 gopher-lua 中的數據類型

All data in a GopherLua program is an LValue . LValue is an interface type that has following methods.

GopherLua程式中的所有數據都是一個LValue。LValue是一種具有以下方法的介面類型。

  • String() string
  • Type() LValueType
// value.go:29

type LValue interface {
	String() string
	Type() LValueType
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
	assertFloat64() (float64, bool)
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
	assertString() (string, bool)
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
	assertFunction() (*LFunction, bool)
}

上面來自官方的介紹,接下來我們看看 gopher-lua 支持那些數據類型。

Type name Go type Type() value Constants
LNilType (constants) LTNil LNil
LBool (constants) LTBool LTrue, LFalse
LNumber float64 LTNumber -
LString string LTString -
LFunction struct pointer LTFunction -
LUserData struct pointer LTUserData -
LState struct pointer LTThread -
LTable struct pointer LTTable -
LChannel chan LValue LTChannel -

具體的實現,大家有興趣,可以自己去看看源碼,這裡就不做分析了。

那我們是如何知道 go 調用 lua 函數後,得到結果的類型呢?我們可以通過以下方式來知道:

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"
)

func main() {
	// 1、創建 lua 的虛擬機
	L := lua.NewState()
	defer L.Close()
	// 載入fib.lua
	if err := L.DoFile(`fib.lua`); err != nil {
		panic(err)
	}

	TestString(L)
}

func TestString(L *lua.LState) {
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("TestLString"), // 獲取函數引用
		NRet:    1,                          // 指定返回值數量
		Protect: true,                       // 如果出現異常,是panic還是返回err
	})
	if err != nil {
		panic(err)
	}

	lv := L.Get(-1) // get the value at the top of the stack
	// 從堆棧中扔掉返回結果
	L.Pop(1)
	if str, ok := lv.(lua.LString); ok {
		// lv is LString
		fmt.Println(string(str))
	}
	if lv.Type() != lua.LTString {
		panic("string required.")
	}
}

fib.lua中的代碼:

function TestLString()
    return "this is test"
end

接下來看看指針類型是如何判斷的:

lv := L.Get(-1) // get the value at the top of the stack
if tbl, ok := lv.(*lua.LTable); ok {
    // lv is LTable
    fmt.Println(L.ObjLen(tbl))
}

特別註意:

  • LBool , LNumber , LString 這三類不是指針類型,其他的都屬於指針類型。
  • LNilType and LBool 這裡沒看懂官方在說什麼,知道的可以告知下,謝謝。
  • lua 中,nil和false都是認為是錯誤的情況。nil表示一個無效值(在條件表達式中相當於false)。

大家有不明白的地方,推薦去看看官方怎麼說的。

2.3 gopher-lua 中的調用堆棧和註冊表大小

官方還介紹了性能優化這塊的內容,我就不介紹了,大家感興趣可以去看官方。

主要是對於我這種非科班出生的菜鳥來說,還是有點難度的,這裡就不瞎說了,免得誤導大家。哈哈......

一般來說,使用預設的方式,性能不會太差。對性能沒有特別高的要求,也沒有必要去折騰這個。

3、gopher-lua 中常用的API

3.1 lua 調用 Go 中的代碼

test.lua 腳本內容:

print(double(100))

main.go 中的內容:

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"
)

func main() {
	L := lua.NewState()
	defer L.Close()
	L.SetGlobal("double", L.NewFunction(Double)) /* Original lua_setglobal uses stack... */
	L.DoFile("test.lua")
}

func Double(L *lua.LState) int {
	fmt.Println("coming go code.............")
	lv := L.ToInt(1)            /* get argument */
	L.Push(lua.LNumber(lv * 2)) /* push result */
	return 1                    /* number of results */
}

執行結果:

coming go code.............
200

上面我們已經實現了一個簡單的 lua 腳本中調用 go 代碼的功能。

3.2 使用Go創建模塊給lua使用

上面介紹了 lua 中調用 Go中的代碼,Go提供的功能不多還好,直接使用即可,但是實際項目中,既然使用到了Go和lua結合的模式,必然會存在Go提供基礎功能,lua來編寫業務的方式,這個時候如果還是使用上面的方式,使用起來將非常不方便。這裡提供了一種方式,將Go中的功能封裝成一個模塊,提供給 lua 使用,這樣就方便許多。

接下來我們一起看看怎麼做。

mymodule.go 的內容:

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"
)

func Loader(L *lua.LState) int {
	// register functions to the table
	mod := L.SetFuncs(L.NewTable(), exports)
	// register other stuff
	L.SetField(mod, "name", lua.LString("testName"))

	// returns the module
	L.Push(mod)
	return 1
}

var exports = map[string]lua.LGFunction{
	"MyAdd": MyAdd,
}

func MyAdd(L *lua.LState) int {
	fmt.Println("coming custom MyAdd")
	x, y := L.ToInt(1), L.ToInt(2)
	// 原諒我還不知道怎麼把計算結果返回給 lua ,太菜了啦
    // 不過用上另外一個包後,我知道,具體看實戰篇。
	fmt.Println(x)
	fmt.Println(y)
	return 1
}

main.go 的內容:

package main

import lua "github.com/yuin/gopher-lua"

func main() {
	L := lua.NewState()
	defer L.Close()
	L.PreloadModule("myModule", Loader)
	if err := L.DoFile("main.lua"); err != nil {
		panic(err)
	}
}

main.lua 的內容:

local m = require("myModule")
m.MyAdd(10, 20)
print(m.name)

運行 main.go 得到執行結果:

coming custom MyAdd
10      
20      
testName

3.3 Go 調用 lua 中的代碼

lua 中的代碼

function TestGoCallLua(x, y)
    return x+y, x*y
end

go 中的代碼

func TestTestGoCallLua(L *lua.LState) {
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("TestGoCallLua"), // 獲取函數引用
		NRet:    2,                            // 指定返回值數量,註意這裡的值是 2
		Protect: true,                         // 如果出現異常,是panic還是返回err
	}, lua.LNumber(10), lua.LNumber(20))
	if err != nil {
		panic(err)
	}

	multiplicationRet := L.Get(-1)
	addRet := L.Get(-2)
	if str, ok := multiplicationRet.(lua.LNumber); ok {
		fmt.Println("multiplicationRet is: ", int(str))
	}

	if str, ok := addRet.(lua.LNumber); ok {
		fmt.Println("addRet is: ", int(str))
	}

}

具體的可以看 xxx 中的 TestTestGoCallLua 函數。

執行結果:

multiplicationRet is:  200
addRet is:  30

3.4 lua中使用go中定義好的類型

這裡我們直接使用官方的例子:

type Person struct {
    Name string
}

const luaPersonTypeName = "person"

// Registers my person type to given L.
func registerPersonType(L *lua.LState) {
    mt := L.NewTypeMetatable(luaPersonTypeName)
    L.SetGlobal("person", mt)
    // static attributes
    L.SetField(mt, "new", L.NewFunction(newPerson))
    // methods
    L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
}

// Constructor
func newPerson(L *lua.LState) int {
    person := &Person{L.CheckString(1)}
    ud := L.NewUserData()
    ud.Value = person
    L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName))
    L.Push(ud)
    return 1
}

// Checks whether the first lua argument is a *LUserData with *Person and returns this *Person.
func checkPerson(L *lua.LState) *Person {
    ud := L.CheckUserData(1)
    if v, ok := ud.Value.(*Person); ok {
        return v
    }
    L.ArgError(1, "person expected")
    return nil
}

var personMethods = map[string]lua.LGFunction{
    "name": personGetSetName,
}

// Getter and setter for the Person#Name
func personGetSetName(L *lua.LState) int {
    p := checkPerson(L)
    if L.GetTop() == 2 {
        p.Name = L.CheckString(2)
        return 0
    }
    L.Push(lua.LString(p.Name))
    return 1
}

func main() {
    L := lua.NewState()
    defer L.Close()
    registerPersonType(L)
    if err := L.DoString(`
        p = person.new("Steeve")
        print(p:name()) -- "Steeve"
        p:name("Alice")
        print(p:name()) -- "Alice"
    `); err != nil {
        panic(err)
    }
}

官方還講解瞭如何使用 go 中的context 來結束lua代碼的執行,這裡我就不演示了,大家自行研究。

3.5 gopher_lua 中goroutine的說明

這裡直接放官方的文檔,大家自行理解

The LState is not goroutine-safe. It is recommended to use one LState per goroutine and communicate between goroutines by using channels.

LState不是goroutine安全的。建議每個goroutine使用一個LState,並通過使用通道在goroutine之間進行通信。

Channels are represented by channel objects in GopherLua. And a channel table provides functions for performing channel operations.

在GopherLua中,通道由通道對象表示。通道表提供了執行通道操作的函數。這意味著,我們可以使用通道對象來創建、發送和接收消息,並使用通道表中的函數來控制通道的行為。通道是一種非常有用的併發編程工具,可以幫助我們在不同的goroutine之間進行通信和同步。通過使用GopherLua中的通道對象和通道表,我們可以輕鬆地在Lua代碼中實現併發編程。

Some objects can not be sent over channels due to having non-goroutine-safe objects inside itself.

某些對象無法通過通道發送,因為其內部有非goroutine安全的對象。

  • a thread(state)
  • a function
  • an userdata
  • a table with a metatable

上面這四種類型就不支持往通道中發送。

package main

import (
	lua "github.com/yuin/gopher-lua"
	"time"
)

func receiver(ch, quit chan lua.LValue) {
	L := lua.NewState()
	defer L.Close()
	L.SetGlobal("ch", lua.LChannel(ch))
	L.SetGlobal("quit", lua.LChannel(quit))
	if err := L.DoString(`
    local exit = false
    while not exit do
      -- 這個 channel 的寫法是固定的 ??
      channel.select(
        {"|<-", ch, function(ok, v)
          if not ok then
            print("channel closed")
            exit = true
          else
            print("received:", v)
          end
        end},
        {"|<-", quit, function(ok, v)
            print("quit")
            exit = true
        end}
      )
    end
  `); err != nil {
		panic(err)
	}
}

func sender(ch, quit chan lua.LValue) {
	L := lua.NewState()
	defer L.Close()
	L.SetGlobal("ch", lua.LChannel(ch))
	L.SetGlobal("quit", lua.LChannel(quit))
	if err := L.DoString(`
    ch:send("1")
    ch:send("2")
  `); err != nil {
		panic(err)
	}
	ch <- lua.LString("3")
	quit <- lua.LTrue
}

func main() {
	ch := make(chan lua.LValue)
	quit := make(chan lua.LValue)
	go receiver(ch, quit)
	go sender(ch, quit)
	time.Sleep(3 * time.Second)
}

執行結果:

received:       1
received:       2
received:       3
quit

4、gopher_lua 性能優化

下麵這些內容,主要來自參考的文章,大家可以點擊當 Go 遇上了 Lua 查看原文。

如果侵權,請聯繫刪除,謝謝。

4.1 提前編譯

在查看上述 DoString(...) 方法的調用鏈後,我們發現每執行一次 DoString(...)DoFile(...) ,都會各執行一次 parse 和 compile 。

func (ls *LState) DoString(source string) error {
    if fn, err := ls.LoadString(source); err != nil {
        return err
    } else {
        ls.Push(fn)
        return ls.PCall(0, MultRet, nil)
    }
}

func (ls *LState) LoadString(source string) (*LFunction, error) {
    return ls.Load(strings.NewReader(source), "<string>")
}

func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {
    chunk, err := parse.Parse(reader, name)
    // ...
    proto, err := Compile(chunk, name)
    // ...
}

從這一點考慮,在同份 Lua 代碼將被執行多次(如在 http server 中,每次請求將執行相同 Lua 代碼)的場景下,如果我們能夠對代碼進行提前編譯,那麼應該能夠減少 parse 和 compile 的開銷(如果這屬於 hotpath 代碼)。根據 Benchmark 結果,提前編譯確實能夠減少不必要的開銷。

package glua_test

import (
    "bufio"
    "os"
    "strings"

    lua "github.com/yuin/gopher-lua"
    "github.com/yuin/gopher-lua/parse"
)

// 編譯 lua 代碼欄位
func CompileString(source string) (*lua.FunctionProto, error) {
    reader := strings.NewReader(source)
    chunk, err := parse.Parse(reader, source)
    if err != nil {
        return nil, err
    }
    proto, err := lua.Compile(chunk, source)
    if err != nil {
        return nil, err
    }
    return proto, nil
}

// 編譯 lua 代碼文件
func CompileFile(filePath string) (*lua.FunctionProto, error) {
    file, err := os.Open(filePath)
    defer file.Close()
    if err != nil {
        return nil, err
    }
    reader := bufio.NewReader(file)
    chunk, err := parse.Parse(reader, filePath)
    if err != nil {
        return nil, err
    }
    proto, err := lua.Compile(chunk, filePath)
    if err != nil {
        return nil, err
    }
    return proto, nil
}

func BenchmarkRunWithoutPreCompiling(b *testing.B) {
    l := lua.NewState()
    for i := 0; i < b.N; i++ {
        _ = l.DoString(`a = 1 + 1`)
    }
    l.Close()
}

func BenchmarkRunWithPreCompiling(b *testing.B) {
    l := lua.NewState()
    proto, _ := CompileString(`a = 1 + 1`)
    lfunc := l.NewFunctionFromProto(proto)
    for i := 0; i < b.N; i++ {
        l.Push(lfunc)
        _ = l.PCall(0, lua.MultRet, nil)
    }
    l.Close()
}

// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPreCompiling-8         100000             19392 ns/op           85626 B/op         67 allocs/op
// BenchmarkRunWithPreCompiling-8           1000000              1162 ns/op            2752 B/op          8 allocs/op
// PASS
// ok      glua    3.328s

4.2 虛擬機實例池

看到這裡的需要註意,官方提醒我們,在每個 goroutine

在同份 Lua 代碼被執行的場景下,除了可使用提前編譯優化性能外,我們還可以引入虛擬機實例池。

因為新建一個 Lua 虛擬機會涉及到大量的記憶體分配操作,如果採用每次運行都重新創建和銷毀的方式的話,將消耗大量的資源。引入虛擬機實例池,能夠復用虛擬機,減少不必要的開銷。

func BenchmarkRunWithoutPool(b *testing.B) {
    for i := 0; i < b.N; i++ {
        l := lua.NewState()
        _ = l.DoString(`a = 1 + 1`)
        l.Close()
    }
}

func BenchmarkRunWithPool(b *testing.B) {
    pool := newVMPool(nil, 100)
    for i := 0; i < b.N; i++ {
        l := pool.get()
        _ = l.DoString(`a = 1 + 1`)
        pool.put(l)
    }
}

// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPool-8          10000            129557 ns/op          262599 B/op        826 allocs/op
// BenchmarkRunWithPool-8            100000             19320 ns/op           85626 B/op         67 allocs/op
// PASS
// ok      glua    3.467s

Benchmark 結果顯示,虛擬機實例池的確能夠減少很多記憶體分配操作。

下麵給出了 README 提供的實例池實現,但註意到該實現在初始狀態時,並未創建足夠多的虛擬機實例(初始時,實例數為 0),以及存在 slice 的動態擴容問題,這都是值得改進的地方。

type lStatePool struct {
    m     sync.Mutex
    saved []*lua.LState
}

func (pl *lStatePool) Get() *lua.LState {
    pl.m.Lock()
    defer pl.m.Unlock()
    n := len(pl.saved)
    if n == 0 {
        return pl.New()
    }
    x := pl.saved[n-1]
    pl.saved = pl.saved[0 : n-1]
    return x
}

func (pl *lStatePool) New() *lua.LState {
    L := lua.NewState()
    // setting the L up here.
    // load scripts, set global variables, share channels, etc...
    return L
}

func (pl *lStatePool) Put(L *lua.LState) {
    pl.m.Lock()
    defer pl.m.Unlock()
    pl.saved = append(pl.saved, L)
}

func (pl *lStatePool) Shutdown() {
    for _, L := range pl.saved {
        L.Close()
    }
}

// Global LState pool
var luaPool = &lStatePool{
    saved: make([]*lua.LState, 0, 4),
}

參考鏈接:

github.com/yuin/gopher-lua

當 Go 遇上了 Lua


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

-Advertisement-
Play Games
更多相關文章
  • 開始 mock一個用於攔截ajax請求,並返回模擬數據的庫。主要讓前端獨立於後端進行開發,通過 pnpm add mockjs 來進行安裝 基礎 初窺門徑 var data = Mock.mock({ // 屬性 list 的值是一個數組,其中含有 1 到 10 個元素 'list|1-10': [ ...
  • 一.概述 分散式系統存在網路,時鐘,以及許多不可預測的故障。分散式事務,一致性與共識問題,迄今為止仍沒有得到很好的解決方案。要想完美地解決分散式系統中的問題不太可能,但是實踐中應對特定問題仍有許多可靠的解決方案。本文不會談及諸如BASE, CAP, ACID 等空泛的理論,只基於實踐中遇到的問題提出 ...
  • Java設計模式【單例模式】 單例模式 單例模式(Singleton Pattern)是一種創建型設計模式,其主要目的是確保一個類只有一個實例,並提供對該實例的唯一訪問點。 優缺點 優點: 提供了對唯一實例的受控訪問。 由於在系統記憶體中只存在一個對象,因此可以節約系統資源。 缺點: 單例類的擴展有很 ...
  • 本文儘可能對GC的情況進行簡要表述,目標是用最少的描述來完整闡明JDK8 Java Hotspot VM目前支持的各種GC方式及其調優方法。 為精簡行文內容,正文中不包含GC觀測手段的介紹,讀者可以參考 [Java Platform, Standard Edition HotSpot 虛擬機垃圾收集... ...
  • _Java 平臺標準版 HotSpot 虛擬機垃圾收集調整指南_ 描述了 Java HotSpot 虛擬機 (Java HotSpot VM) 中包含的垃圾收集方法,並幫助您確定最適合您需要的方法。 本文檔適用於希望提高應用程式性能的應用程式開發人員和系統管理員,尤其是那些處理大量數據、使用多線程... ...
  • 首先值得說明的是,在這個項目幾乎完成之際,筆者才愈發體會到了硬體思維和軟體思維的雲泥之別。不幸的是,在此項目的實現過程中,絕大部分代碼的思維仍然是軟體思維,因此該項目主要模塊的設計部分可能並不能體現硬體操作的獨到之處,不符合硬體工程師的基本設計思維,所以此主題文章僅用於學習交流以及記錄一次FPGA項 ...
  • 剪貼板是個啥就不用多介紹了,最直觀的功能是實現應用程式之間數據共用。就是咱們常說的“複製”、“粘貼”功能。 在 Qt 中,QClipboard 類提供了相關 API 讓應用程式具備讀/寫剪貼板的能力。數據通過 QMimeData 類包裝。該類使用 MIME 類型來標識數據。比如,要包裝的數據是純文本 ...
  • golang支持兩種隨機數生成方式: math/rand // 偽隨機 crypto/rand // 真隨機 math/rand的用法:rand.Intn(100)。這個起始位置是由一個seed決定的,預設是從1開始。為了儘量隨機性,那麼我們可以每次使用不同的seed來啟動程式,就可以保證每次啟動都 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...