Python使用Ctypes與C/C++ DLL文件通信過程介紹及實例分析

来源:https://www.cnblogs.com/yssjun/archive/2018/11/23/9960479.html
-Advertisement-
Play Games

項目中可能會經常用到第三方庫,主要是出於程式效率考慮和節約開發時間避免重覆造輪子。無論第三方庫開源與否,編程語言是否與當前項目一致,我們最終的目的是在當前編程環境中調用庫中的方法並得到結果或者藉助庫中的模塊實現某種功能。這個過程會牽涉到很多東西,本篇文章將簡要的介紹一下該過程的一些問題。 1.背景 ...


項目中可能會經常用到第三方庫,主要是出於程式效率考慮和節約開發時間避免重覆造輪子。無論第三方庫開源與否,編程語言是否與當前項目一致,我們最終的目的是在當前編程環境中調用庫中的方法並得到結果或者藉助庫中的模塊實現某種功能。這個過程會牽涉到很多東西,本篇文章將簡要的介紹一下該過程的一些問題。

1.背景

多語言混合編程可以彌補某一種編程語言在性能表現或者是功能等方面的不足。雖然所有的高級語言都會最終轉換成彙編指令或者最底層的機器指令,但是語言本身之間的千差萬別很難一言以蔽之,這對不同語言之間相互通信造成很大的障礙。

工作中需要用python完成一項功能,但是所有現有的python庫都不滿足需求。最終找到了一個開源的C++庫,編譯得到動態庫被python調用才完成工作需求。雖然整個過程耗時不多,但是期間碰到很多的問題,而且這些問題都很有思考價值。

除了這篇博文外,後續還將有一到兩篇文章通過具體的實例講解一下跨語言調用。

2.問題思考

在進行具體的介紹之前,先來思考一下調用外部庫或者自己實現庫所牽涉的一些一般性的問題。這樣或許實際中操作使用時會理解的更加深刻,遇到問題也能夠逐項的排查。

如果用C語言寫的庫調用了Linux的system call,縱使C本身是跨平臺的,那麼該庫也不可能在Window上被使用,即便我們能拿到源碼。這裡有兩個核心問題:

  • 是否開源
  • 是否跨平臺

如果庫的實現不依賴平臺,且開源,那就意味著很大可能能在當前項目中使用。為什麼是可能,因為即使庫的實現語言和當前項目語言一致,也可能因為語言版本差異或者標準迭代導致不相容。

 最差的情況就是只能拿到編譯後的庫文件,且需在特定的平臺運行。

作為庫的開發者,最好是能夠開源且庫的實現不依賴於特定的平臺,這樣才能最大限度的被使用。

作為庫的使用者,最不理想的情況是庫可以在當前平臺使用,但是只能拿到靜態庫或者動態庫,且庫的實現語言和當前項目語言不一致。

多數情況是第三方庫是跨平臺的且能夠拿到源代碼。這樣的話如果兩者的實現語言一致,我們可以直接將第三方庫的代碼移植到當前的項目中;如果實現語言不一致,需要在當前平臺上將庫的源碼編譯出當前平臺上可用的庫文件,然後在當前項目中引用編譯生成的庫文件。

本文將先簡單的介紹在window平臺上,使用python 2.7 自帶的ctypes庫引用標準的C動態庫msvcrt.dll。這裡可以先思考以下幾個問題:

  1. python可不可以引用靜態庫?
  2. python中怎麼拿到DLL導出的函數?
  3. python和C/C++之間的變數的類型怎樣轉換,如果是自定義的類型呢?
  4. 怎麼處理函數調用約定(calling convention,eg:__cdecl,__stdcall,__thiscall,__fastcall)可能不同的問題?
  5. 如果調用DLL庫的過程中出現問題,是我們調用的問題還是庫本身的問題?應該怎樣快速排查和定位問題?
  6. 有沒有什麼現有的框架能夠幫我們處理python中引用第三方庫的問題呢?
  7. 對於自定義的類型(class 和 struct)是否能在python中被引用。

關於函數調用約定,有必要簡單的提一下:

Calling Convention和具體的編程語言無關,是由編譯器、連接器和操作系統平臺這些因素共同決定的。

The Visual C++ compilers allow you to specify conventions for passing arguments and return values between functions and callers. Not all conventions are available on all supported platforms, and some conventions use platform-specific implementations. In most cases, keywords or compiler switches that specify an unsupported convention on a particular platform are ignored, and the platform default convention is used.

這是MS的官方解釋。註意最後一句話,表示對於函數調用,在平臺不支持的情況下,語言中指定關鍵字或者編譯器轉換均可能無效。

接下的介紹中來我們將一一回答上面的問題。

3.導入C標準動態庫

先來簡單看一下python中如何引用C的標準動態庫。

 1 import ctypes, platform, time
 2 if platform.system() == 'Windows':
 3     libc = ctypes.cdll.LoadLibrary('msvcrt.dll')
 4 elif platform.system() == 'Linux':
 5     libc = ctypes.cdll.LoadLibrary('libc.so.6')
 6 print libc
 7 # Example 1
 8 libc.printf('%s\n', 'lib c printf function')
 9 libc.printf('%s\n', ctypes.c_char_p('lib c printf function with c_char_p'))
10 libc.printf('%ls\n', ctypes.c_wchar_p(u'lib c printf function with c_wchar_p'))
11 libc.printf('%d\n', 12)
12 libc.printf('%f\n', ctypes.c_double(1.2))
13 # Example 2
14 libc.sin.restype = ctypes.c_double
15 print libc.sin(ctypes.c_double(30 * 3.14 / 180))
16 # Example 3
17 libc.pow.restype = ctypes.c_double
18 print libc.pow(ctypes.c_double(2), ctypes.c_double(10))
19 # Example 4
20 print libc.time(), time.time()
21 # Example 5
22 libc.strcpy.restype = ctypes.c_char_p
23 res = 'Hello'
24 print libc.strcpy(ctypes.c_char_p(res), ctypes.c_char_p('World'))
25 print res

接下來我們一一分析上面的這段代碼。

3.1 載入庫的方式

根據當前平臺分別載入Windows和Linux上的C的標準動態庫msvcrt.dll和libc.so.6。

 註意這裡我們使用的ctypes.cdll來load動態庫,實際上ctypes中總共有以下四種方式載入動態庫:

  1. class ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
  2. class ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
  3. class ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
  4. class ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=None)

關於這幾個載入動態庫的方式區別細節可以參考一下官網的說明,這裡僅簡要說明一下。

 除了PyDll用於直接調用Python C api函數之外,其他的三個主要區別在於

  • 使用的平臺;
  • 被載入動態庫中函數的調用約定(calling convention);
  • 庫中函數假定的預設返回值。

 也就是平臺和被載入動態庫中函數的調用約定決定了我們應該使用哪種方式載入動態庫。

本例中我們在windows平臺上使用的是CDLL而不是WinDll,原因是msvcrt.dll中函數調用約定是C/C++預設的調用約定__cdecl。

而WinDll雖然是可以應用於windows平臺上,但是其只能載入標準函數調用約定為__stdcall的動態庫。因此這裡只能使用CDLL方式。

可以將上面的CDLL換成WinDll看一下會不會有問題。這裡應該能夠對函數調用理解的更加深刻一些了,同時也回答了上面第一小節中我們提問的問題4。

3.2 跨語言類型轉換

 這裡主要針對第一節提出的問題3。

我們是在python中調用C的函數,函數實參是python類型的變數,函數形參則是C類型的變數,顯然我們將python類型的變數直接賦值給C類型的變數肯定會有問題的。

因此這裡需要兩種語言變數類型之間有一一轉換的必要。這裡僅僅列出部分對應關係(由於博客園的表格顯示會有問題,因此這樣列出,請見諒):

Python type        Ctypes type          C type

int/long             c_int             int

float             c_double           double

string or None        c_char_p           char * (NUL terminated)

unicode or None       c_wchar_p          wchar_t * (NUL terminated)

 通過Ctypes type中提供類型,我們建立了一種python類型到c類型的一種轉換關係。

在看一下上面的例子Example 1。在調用C的函數時,我們傳給C函數的實參需要經過Ctypes轉換成C類型之後才能正確的調用C的函數。

3.3 設定C函數的返回類型

看一下上面的例子Example 2.

libc.sin.restype = ctypes.c_double

我們通過restype的方式指定了C(math 模塊)函數sin的返回類型為double,對應到python即為float。顯然函數的返回類型在DLL中是無法獲取的。

開發人員也只能從庫的說明文檔或者頭文件中獲取到函數的聲明,進而指定函數返回值的類型。

double sin (double x);
float sin (float x);
long double sin (long double x);
double sin (T x);           // additional overloads for integral types

上面是C++11中cmath中sin函數的聲明。這裡幾個sin函數是C++中的函數重載。

libc.sin(ctypes.c_double(30 * 3.14 / 180))

由於調用之前指定了sin函數的返回類型ctypes.c_double,因此sin的調用結果在python中最終會轉換為float類型。

3.4 假定的函數返回類型

由於我們在動態庫中獲取的函數並不知道其返回類型,因為我們只得到了函數的實現,並沒有函數的聲明。

在沒有指定庫函數返回類型的情況下,ctypes.CDLL和ctyps.WinDll均假定函數返回類型是int,而ctypes.oleDll則假定函數返回值是Windows HRESULT。

那如果函數實際的返回值不是int,便會按照int返回值處理。如果返回類型能轉為int類型是可以的,如果不支持那函數調用的結果會是一個莫名其妙的數字。

time_t time (time_t* timer);

  上面的例子Example 4則預設將C類型time_t轉為了python 的int類型,結果是正確的。

對於Example 3中我們不僅要指定函數pow的返回類型,還要轉換函數的實參(這裡很容易疏忽)。

因此在調用動態庫之前一定要看下函數聲明,指定函數返回類型。

到這裡很容易想到可以指定函數的返回值類型,那能不能指定函數形參的類型呢?答案是肯定的,argtypes 。

printf.argtypes = [c_char_p, c_char_p, c_int, c_double]

3.5 可變string buffer

 上面的例子Exapmle 5中我們調用了C中的一個字元串拷貝函數strcpy,這裡函數的返回值和被拷貝的對象均為正確的。

但是這裡是故意這樣寫的,因為這裡會有一個問題。

如果res = 'Hello'改為res = 'He'和res = 'HelloWorld',那麼實際上res的結果會是‘Wo’和'World\x00orld'。

str_buf = ctypes.create_string_buffer(10)
print ctypes.sizeof(str_buf)                       # 10
print repr(str_buf.raw)                            # '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
str_buf.raw = 'Cnblogs'
print repr(str_buf.raw)                            # 'Cnblogs\x00\x00\x00'
print repr(str_buf.value)                          # 'Cnblogs'

這裡我們可以通過ctypes.create_string_buffer來指定一個字元串緩存區。

使用string buffer改寫Example 5:

libc.strcpy.restype = ctypes.c_char_p
res = ctypes.create_string_buffer(len('World') + 1)
print libc.strcpy(res, ctypes.c_char_p('World'))
print repr(res.raw), res.value                     # 'World\x00' 'World'

註意上面的res的類型是c_char_Array_xxx。這裡只是為了介紹string buffer,實際上不會這麼用。

3.6 小節

這裡簡單的介紹了一下ctypes如何和動態庫打交道。限於篇幅還有指針,引用類型和數組等的傳遞,以及自定義類型等沒有介紹。但是這一小結應該能對python引用動態庫過程有一個大致的認識。

更加詳細信息可以參考官網:ctypes

4. 自定義DLL文件導入

為了更好的理解python調用DLL的過程,有必要瞭解一下DLL的定義文件。

4.1 C/C++引用DLL

首先,作為對比我們看一下C/C++如何引用DLL文件的。下麵的文件是 ./Project2/Source2.cpp

工程配置為:Conguration Properties>General>Configuration Types: Dynamic Library (.dll)

輸出路徑:./Debug/Project2.dll

 1 #include <stdio.h>
 2 #include <math.h>
 3 #include <string.h>
 4 
 5 #ifdef _MSC_VER
 6 #define DLL_EXPORT extern "C" __declspec( dllexport )
 7 #else
 8 #define DLL_EXPORT
 9 #endif
10 
11 __declspec(dllexport) char* gl = "gl_str";
12 
13 DLL_EXPORT void __stdcall hello_world(void) {
14     printf("%s Hello world!\n", gl);
15 }
16 
17 DLL_EXPORT int my_add(int a, int b) {
18     printf("calling my_add@int func\n");
19     return a + b;
20 }
21 
22 //DLL_EXPORT double my_add(double a, double b) {
23 //    printf("calling my_add@double func\n");
24 //    return a + b;
25 //}
26 
27 DLL_EXPORT int my_mod(int m, int n) {
28     return m % n;
29 }
30 
31 DLL_EXPORT bool is_equal(double a, double b) {
32     return fabs(a - b) < 1e-3;
33 }
34 
35 DLL_EXPORT void my_swap(int *p, int *q) {
36     int tmp = *p;
37     *p = *q;
38     *q = tmp;
39 }
40 
41 inline void swap_char(char *p, char *q) {
42     char tmp = *p;
43     *p = *q;
44     *q = tmp;
45 }
46 
47 DLL_EXPORT void reverse_string(char *const p) {
48     if (p != nullptr) {
49         for (int i = 0, j = strlen(p) - 1; i < j; ++i, --j)
50             swap_char(p + i, p + j);
51             //swap_char(&p[i], &p[j]);
52     }
53 }

 下麵的文件是 ./Project1/Source1.cpp

工程配置為:Conguration Properties>General>Configuration Types: Application (.exe)

輸出路徑:./Debug/Project1.exe

 1 #include "stdio.h"
 2 #include "cstdlib"
 3 #pragma comment(lib, "../Debug/Project2.lib")
 4 
 5 #ifdef _MSC_VER
 6 #define DLL_IMPORT extern "C" __declspec( dllimport )
 7 #else
 8 #define DLL_IMPORT
 9 #endif
10 
11 DLL_IMPORT void __stdcall hello_world(void);
12 DLL_IMPORT int my_add(int, int);
13 DLL_IMPORT int my_mod(int, int);
14 DLL_IMPORT bool is_equal(double, double);
15 DLL_IMPORT void my_swap(int*, int*); 
16 DLL_IMPORT void reverse_string(char* const);
17 
18 __declspec(dllimport) char* gl;
19 
20 int main() {
21     int a = 0, b = 1;
22     char s[] = "123456";
23     hello_world();
24     my_swap(&a, &b);
25     reverse_string(s);
26     printf("DLL str gl: %s \n", gl);
27     printf("DLL func my_add: %d\n", my_add(1,2));
28     printf("DLL func my_mod: %d\n", my_mod(9, 8));
29     printf("DLL func my_comp: %s\n", is_equal(1, 1.0001) ? "true":"false");
30     printf("DLL func my_swap: (%d, %d)\n", a, b);
31     printf("DLL func reverse_string: %s\n", s);
32     system("pause");
33 }

 上面的這個例子已經清楚的展示了C/C++如何導出和引用DLL文件。有以下幾點需要註意:

  1. 上面#pragma comment(lib, "../Debug/Project2.lib")中引用的是生成Project2.dll過程中產生的導出庫,並非靜態庫。
  2. __declspec聲明只在Windows平臺用,若是引用靜態庫,則不需要__declspec聲明。
  3. 不管動態庫還是靜態庫,除了用#pragma comment引用lib文件外,還可以在Conguration Properties>Linker>Input>Additional Dependencies中添加lib文件。
  4. 上面例子中我們導出和引用均聲明瞭extern "C",表示讓編譯器以C的方式編譯和鏈接文件。意味著導出的函數不支持重載,且函數調用約定為C和C++的預設調用約定__cdecl。
  5. DLL_EXPORT void __stdcall hello_world(void)指定了函數使用__stdcall的Calling Convention,該方式聲明優先於編譯器預設的__cdecl方式。
  6. 不同的調用約定不僅會影響實際的函數調用過程,還會影響編譯輸出函數的命名。比如函數hello_world以__cdecl方式和__stdcall方式輸出到DLL中的函數分別為hello_world和_hello_world@0。

4.2 python引用DLL

先使用VS自帶的dumpbin工具看一下Project2.dll文件部分內容:

dumpbin -exports "./Debug/project2.dll"

ordinal hint RVA      name

1    0 00018000 ?gl@@3PADA
2    1 00011217 _hello_world@0
3    2 00011046 is_equal
4    3 0001109B my_add
5    4 000112D0 my_mod
6    5 00011005 my_swap
7    6 0001118B reverse_string

 話不多說,先上代碼:

 1 import ctypes, platform, time
 2 if platform.system() == 'Windows':
 3     my_lib = ctypes.cdll.LoadLibrary(r'.\Debug\Project2.dll')
 4     # my_lib = ctypes.CDLL(r'.\Debug\Project2.dll')
 5 elif platform.system() == 'Linux':
 6     my_lib = ctypes.cdll.LoadLibrary('libc.so.6')
 7 
 8 # [C++] __declspec(dllexport) char* gl = "gl_str";
 9 print ctypes.c_char_p.in_dll(my_lib, '?gl@@3PADA').value    # result: gl_str
10 
11 # [C++] DLL_IMPORT void __stdcall hello_world(void);
12 getattr(my_lib, '_hello_world@0')()    # result: gl_str Hello world!
13 
14 # [C++] DLL_IMPORT int my_add(int, int);
15 print my_lib.my_add(1, 2)         # result: 3                 
16 
17 # [C++] DLL_IMPORT int my_mod(int, int);
18 print my_lib.my_mod(123, 200)    # result: 123
19 
20 # [C++] DLL_IMPORT void my_swap(int*, int*); 
21 a, b = 111, 222
22 pa, pb = ctypes.pointer(ctypes.c_int(a)), ctypes.pointer(ctypes.c_int(b))
23 my_lib.my_swap(pa, pb)
24 print pa.contents.value, pb.contents.value  # result: 222, 111
25 print a, b    # result: 111, 222
26 
27 # [C++] DLL_IMPORT bool is_equal(double, double);
28 my_lib.is_equal.restype = ctypes.c_bool
29 my_lib.is_equal.argtypes = [ctypes.c_double, ctypes.c_double]
30 # print my_lib.is_equal(ctypes.c_double(1.0), ctypes.c_double(1.0001))
31 print my_lib.is_equal(1.0, 1.0001)    # result: True
32 print my_lib.is_equal(1.0, 1.0100)    # result: False
33 
34 # [C++] DLL_IMPORT void reverse_string(char *const);
35 s = "123456"
36 ps = ctypes.pointer(ctypes.c_char_p(s))
37 print ps.contents    # result: c_char_p('123456')
38 my_lib.reverse_string(ctypes.c_char_p(s))
39 print ps.contents, s  # result: c_char_p('654321') 654321

 上面的代碼加上註釋和結果已經很詳細的說明瞭python引用DLL的過程,限於篇幅,這裡就不在贅述。

有一點需要強調,我們使用__stdcall方式聲明函數hello_world方式,並且用CDLL方式引入。導致無法直接用lib.func_name的方式訪問函數hello_world。

如果想要使用my_lib.hello_world的方式調用該函數,只需要使用windll的方式引入DLL,或者使用預設的__cdecl方式聲明hello_world。

5 總結

先來看一下開始提問的問題,部分問題已經在文中說明。

1.python可不可以引用靜態庫?

首先,靜態庫是會在鏈接的過程組裝到可執行文件中的,靜態庫是C/C++代碼。

其次,python是一種解釋性語言,非靜態語言,不需要編譯鏈接。

最後,官網好像沒有提供對應的對接模塊。

5.如果調用DLL庫的過程中出現問題,是我們調用的問題還是庫本身的問題?應該怎樣快速排查和定位問題?

python中怎麼定位問題這個不多說。

DLL中的問題可以使用VS的attach to process功能,將VS Attach 到當前運行的python程式,然後調用到DLL,加斷點。

6.有沒有什麼現有的框架能夠幫我們處理python中引用第三方庫的問題呢?

常用的有ctypes,swig, cython, boost.python等

7.對於自定義的類型(class 和 struct)是否能在python中被引用。

至少ctypes中沒有相關的操作。

其實也沒必要,因為不僅python中沒有對應的類型,而且完全可以通過將自定義的類或者結構體封裝在DLL輸出的函數介面中進行訪問等操作。

總結:

本文使用python自帶的庫ctypes介紹瞭如果引用動態庫DLL文件,相對於其他的第三方庫,這是一個相對比較低級的DLL包裝庫。但正是因為這樣我們才能看清楚調用DLL過程的一些細節。使用ctypes過程遇到的每一個錯誤都可能是一個我們未知的知識點,因此建議先熟悉該庫,儘可能深入的瞭解一下python調用動態庫的過程。其他的庫原理是一樣的,只不過進行了更高級的封裝而已。


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

-Advertisement-
Play Games
更多相關文章
  • 內置函數 絕對值函數 x=100的絕對值為:100 y= 20的絕對值為:20 求最大值、最小值、求和函數 (1, 2, 3, 4)中最大max的元素為:4 (1, 2, 3, 4)中最小min的元素為:1 (1, 2, 3, 4)中最元素累加和sum為:10 模塊中的函數 char_set長度36 ...
  • Python基礎知識(11):高級特性 一、分片(切片) 通過索引來獲取一定範圍內的元素 二、迭代 給定一個元組或列表,通過for迴圈遍歷,這種遍歷稱為迭代 結果: Alice 通過collections模塊的Iterator判斷一個對象是否是可迭代對象 enumerate函數可以把一個list變成 ...
  • [TOC] 1. static概括 當在定義類的時候,類中都會有相應的屬性和方法。而屬性和方法都是通過創建本類對象調用的。當在調用對象的某個方法時,這個方法沒有訪問到對象的特有數據時, 方法創建這個對象有些多餘 ,我們可以通過static關鍵字來實現。static它是靜態修飾符,一般用來修飾類中的成 ...
  • 1.什麼是粘包 寫在前面:只有TCP有粘包現象,UDP永遠不會粘包 1.TCP下的粘包 因為TCP協議是面向連接、面向流的,收發兩端(客戶端和伺服器端)都要有成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且數據量小的數據, ...
  • 前面介紹了數值包裝類型,因為不管是整數還是小數,它們的運算操作都是類似的,所以只要學會了Integer的用法,其它數值包裝類型即可一併掌握。但是對於布爾類型boolean來說,該類型定義的是“true”和“false”的布爾值,並非123之類的數字,因此還需專門的包裝類型Boolean來包裝bool ...
  • 作為當前最流行的NIO框架,Netty在互聯網領域、大數據分散式計算領域、游戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。 ...
  • opencv庫在3.0以後分為opencv庫和opencv_contrib庫兩部分,其中opencv_contrib庫是一個擴展庫,如果需要使用SIFT和SURF演算法就需要安裝這個擴展庫,否則只用安裝opencv庫即可。 對於vs2015、vs2017這樣的高版本vs有現成的編譯好的opencv庫, ...
  • 惰性數值生成器是指在需要的時候才生成下一個數值,不需要的時候就卡在那。這和python的列表推導表達式類似。惰性生成器的好處是不會一次性將全部結果返回或放進記憶體,而是每次只返回一個,這樣不會在某一時刻大量占用記憶體和其它資源。 比如,要生成10W個數值,如果要迭代這10W個數值,有兩種方法。第一種方法 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...