【eBPF-02】入門:基於 BCC 框架的程式進階

来源:https://www.cnblogs.com/lianyihong/archive/2023/12/28/17932675.html
-Advertisement-
Play Games

本文在前一篇文章的基礎上,對進程執行監控工具(execsnoop)進行了升級,實時列印進程執行時傳入的參數列表;並通過 `kprobe` 和 `tracepoint` 兩種方式,綁定 eBPF 程式,給出了代碼實現。同時,對這兩種 eBPF 事件類型進行了簡單比較。顯然,在你手動開發一個 eBPF ... ...


本文是 eBPF 系列的第二篇文章,我們來學習 eBPF BCC 框架的進階用法,對上一篇文章中的代碼進行升級,動態輸出進程運行時的參數情況。

主要內容包括:

  1. 通過 kprobe 掛載內核事件的 eBPF 程式要如何編寫?
  2. 通過 tracepoint 掛載內核事件的 eBPF 程式要如何編寫?
  3. eBPF 的程式事件類型有哪些?

在開始之前,我們來回顧一下前一篇文章的內容。

前一篇文章介紹瞭如何通過 BCC 框架來編寫一個簡單的 eBPF 程式。在內核空間,使用 c 程式實現 eBPF 的核心邏輯;在用戶空間,使用 python 腳本作為 eBPF 程式的控制、載入和展示。其中,內核態通過若幹 eBPF helper 函數,獲取內核觀測數據,並通過 PERF 區域,將這些數據傳遞到用戶空間;用戶態使用attach_kprobe() 將內核 eBPF 函數綁定到某個內核事件上。

整個流程如下圖所示:

image

在上面的實現過程中,用戶態通過 kprobe 的方式,為某個內核事件掛載自定義處理邏輯(圖中是指定了內核中 do_execve 函數)。通過這種方式,我們能夠監測絕大部分的內核函數,這正是 eBPF 技術牛逼的原因。

對於這種 kprobe 類型的 eBPF 程式,我們再來看一個例子(改編自 Brendan Gregg 大神的 execsnoop 工具:https://github.com/iovisor/bcc/blob/master/tools/execsnoop.py

1 進程執行參數的監控

接下來,我們要對上圖中的工具再次進行功能升級,我希望這個工具在運行時,能夠輸出當前執行進程的參數信息。

如果將 eBPF 程式等同於 C 程式來看,這個問題似乎沒那麼困難。何以見得?

1.1 分析

sys_execve 系統調用的函數簽名為:int execve(const char *filename, char *const argv[], char *const envp[]), 其中,argv[]便記錄了進程執行的參數。我們大可以像提取 filename 的方式那樣,提取 argv[],並將其傳入到用戶空間中。

但實際上,eBPF 程式與 C 程式並不等同。eBPF 編程中有 “兩座大山” 般的限制,分別是:

限制一:eBPF 程式運行棧僅有 512 位元組。

限制二:eBPF 程式可以調用的介面極其有限。

因此,如果我們想嘗試在 512 位元組的 eBPF 運行棧中完整拼接整理不定長的 argv[] 參數列表,是根本不可能的。

基於以上分析,本文給出一個比較合理的解決方案:

Q:如何防止運行棧爆棧?

1)既然運行棧有大小限制,不如直接將拼接操作轉移到用戶態完成。eBPF 程式只需要將 argv[] 數組中每個 argv 傳輸到用戶態程式中。
2)對於長度過長的 argv,沒辦法了,只能手動截斷了。

Q:用戶態何時進行參數拼接?何時進行參數展示?

1)既然需要用戶態完成拼接,那麼,可以分為兩個階段。STEP-1,僅專註字元串的拼接;STEP-2,僅專註字元串展示。
2)對於 execve 系統調用,我們可以在 enter 時執行 STEP-1 操作,在 exit 是執行 STEP-2 操作。

接下來更新代碼。

1.2 定義

首先,對於用於交互的結構體,增加兩個個欄位,其一用於記錄 execve 調用的每個參數,其二用於記錄 eBPF 執行的階段;同時,去掉冗餘欄位 fname

#define ARGSIZE     128
#define MAXARG      60

enum event_step {
	STEP_1,		// STEP 1: 執行 argv 拼接
	STEP_2,		// STEP 2: 執行 argv 展示
};

struct data_t {
	u32		pid;
	enum		event_step step;	// 記錄 eBPF 執行階段
	char		comm[TASK_COMM_LEN];
	char		argv[ARGSIZE];		// 記錄每一個參數
};

定義 BPF_PERF_OUTPUT

BPF_PERF_OUTPUT(events);

1.3 處理

實現 execve 系統調用 enterexit 回調函數:

// exter execve
int syscall__execve(struct pt_regs *ctx, const char __user *filename,
			const char __user *const __user *__argv,
			const char __user *const __user *__envp) {
	struct data_t data = {};
	// 設置 step = STEP 1
	data.step = STEP_1;
	// 設置 pid
	data.pid = bpf_get_current_pid_tgid() >> 32;
	// 設置 comm
	bpf_get_current_comm(&data.comm, sizeof(data.comm));
	// 設置每一個 argv,並導出
	...
	
	return 0;
}


// exit execve
int do_ret_sys_execve(struct pt_regs *ctx) {
	struct data_t data = {};
	// 設置 step = STEP 1
	data.step = STEP_2;
	// 設置 pid
	data.pid = bpf_get_current_pid_tgid() >> 32;
	// 設置 comm
	bpf_get_current_comm(&data.comm, sizeof(data.comm));
	// 提交 perf
	events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

註意,這裡 bpf_get_current_pid_tgid() 輔助函數返回值高 32 為內核視角下的 process ID(用戶視角下為 TID),低 32 位為內核視角下的 thread group ID(用戶視角下的 PID)。這裡右移 32 位,是獲取用戶視角的 PID。

1.4 綁定

用戶態綁定 kprobe 事件:

b = BPF(src_file="execsnoop.c")
execve_fnname = b.get_syscall_fnname("execve")
# enter 事件
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
# exit 事件
b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve")

1.5 難點

內核態如何設置並導出每一個 argv[]

// 字元串提交
static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
	// 提交 perf 之前,需要拷貝到用戶態變數中
	bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);
	// 將這個 argv 提交
	events.perf_submit(ctx, data, sizeof(struct data_t));
	return 1;
}
// 字元串控制
static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
	const char *argp = NULL;
	bpf_probe_read_user(&argp, sizeof(argp), ptr);
	// 是否到達末尾字元串
	if (argp) {
		return __submit_arg(ctx, (void *)(argp), data);
	}
	return 0;
}

int syscall__execve(struct pt_regs *ctx, const char __user *filename,
			const char __user *const __user *__argv,
			const char __user *const __user *__envp) {
	// 設置過程
	...
	// (A) 設置每一個 argv,並導出
	#pragma unroll
	for (int i = 1; i < MAXARG; i++) {
		if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)
			goto out;
	}
	// (B) 如果當前的 argv[] 太長了,進行截斷操作
	char ellipsis[] = "...";
	__submit_arg(ctx, (void *)ellipsis, &data);
out:
	return 0;
}

關註核心的兩個步驟:

(A) MAXARG 代表一個 argv[] 的最大監測數量。首先要遍歷這個 argv[] 的每一個字元串,如果這個字元不為 NULL(說明沒有到當前 argv[] 結尾)或不超過最大值 MAXARG,那麼將每個字元串提交到 PERF 區域。

註意:
低版本(5.3 以前)的 eBPF 程式不支持迴圈。5.3 版本後也僅支持有界迴圈。在低版本的 eBPF 中使用迴圈有一個小技巧,那就是通過 #pragma unroll 進行編譯器迴圈展開預處理。

(B) 如果超過了這個最大數量 MAXARG,後面及時再有參數,也進行截斷處理。

1.6 拼接

用戶態獲取和拼接參數列表是基於 eBPF 階段的。

from collections import defaultdict

argv = defaultdict(list)
class EventStep(object):
	STEP_1 = 0
	STEP_2 = 1

# PERF 事件回調處理
def print_event(cpu, data, size):
	event = b["events"].event(data)
	# STEP 1:拼接
	if event.step == EventStep.STEP_1:
		argv[event.pid].append(event.argv)
	# STEP 2:顯示
	elif event.step == EventStep.STEP_2:
		argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
		printb(b"%-16s %-7d %s" % (event.comm, event.pid, argv_text))
		try:
			del(argv[event.pid])
		except Exception:
			pass

# 綁定 PERF 事件回調處理
b["events"].open_perf_buffer(print_event)
while 1:
	try:
		b.perf_buffer_poll()
	except KeyboardInterrupt:
		exit()

用戶態程式需要註意:event 事件通過 PERF 獲取的結構數據為 Byte 類型,需要通過 decode('utf-8')/encode()str 類型進行轉換。

1.7 完整代碼和運行效果

// execsnoop.c
#include <linux/sched.h>
#include <linux/fs.h>

#define ARGSIZE     128
#define MAXARG      60
enum event_step {
	STEP_1,
	STEP_2,
};
struct data_t {
	u32		pid;
	enum		event_step step;
	char		comm[TASK_COMM_LEN];
	char		argv[ARGSIZE];
};
BPF_PERF_OUTPUT(events);

static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
	bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);
	events.perf_submit(ctx, data, sizeof(struct data_t));
	return 1;
}
static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
	const char *argp = NULL;
	bpf_probe_read_user(&argp, sizeof(argp), ptr);
	if (argp) {
		return __submit_arg(ctx, (void *)(argp), data);
	}
	return 0;
}
// exter execve
int syscall__execve(struct pt_regs *ctx, const char __user *filename,
			const char __user *const __user *__argv,
			const char __user *const __user *__envp) {
	struct data_t data = {};
	data.step = STEP_1;
	data.pid = bpf_get_current_pid_tgid() >> 32;
	bpf_get_current_comm(&data.comm, sizeof(data.comm));
	#pragma unroll
	for (int i = 1; i < MAXARG; i++) {
		if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)
			goto out;
	}
	char ellipsis[] = "...";
	__submit_arg(ctx, (void *)ellipsis, &data);
out:
	return 0;
}
// exit execve
int do_ret_sys_execve(struct pt_regs *ctx) {
	struct data_t data = {};
	data.step = STEP_2;
	data.pid = bpf_get_current_pid_tgid() >> 32;
	bpf_get_current_comm(&data.comm, sizeof(data.comm));
	events.perf_submit(ctx, &data, sizeof(data));
	return 0;
}
# execsnoop.py
#!/usr/bin/python3
from bcc import BPF
from bcc.utils import printb
from collections import defaultdict

argv = defaultdict(list)
class EventStep(object):
	STEP_1 = 0
	STEP_2 = 1

b = BPF(src_file="execsnoop.c")
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve")

print("%-7s %-16s %s" % ("PID", "PCOMM", "ARGS"))

# process event
def print_event(cpu, data, size):
	event = b["events"].event(data)
	fname = ""
	if event.step == EventStep.STEP_1:
		argv[event.pid].append(event.argv)
	elif event.step == EventStep.STEP_2:
		argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
		printb(b"%-7d %-16s %s" % (event.pid, event.comm, argv_text))
		try:
			del(argv[event.pid])
		except Exception:
			pass

# loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
	try:
		b.perf_buffer_poll()
	except KeyboardInterrupt:
		exit()

運行效果:

image

2 Tracepoint 追蹤點

前文提到過,kprobe 方式,幾乎可以使 eBPF 掛載到內核中任意一個函數事件上,隨著內核函數的執行而觸發。但是,由於不同的內核版本,其某個具體函數的定義、參數和實現可能會有所不同(kprobe 實現的事件處理函數要求和掛載點函數擁有相同的參數)。因此,使用 kprobe 方式實現的 eBPF 程式可能無法在其他內核的主機上運行。此外,kprobe 無法掛載到靜態函數或內聯函數上。而出於性能考慮,大部分網路相關的內層函數都是內聯或者靜態的,因此,kprobe 方式在這些領域也只能望洋興嘆了。

上述兩點,均為 kprobe 方式的局限性,它並不具備很好的可移植性。於是,從 Linux 內核 4.7 開始,能讓 eBPF 使用的 tracepoint 出現了(官方文檔)。tracepoint 是由內核開發人員在代碼中設置的靜態 hook 點,具有穩定的 API 介面,不會隨著內核版本的變化而變化。但由於 tracepoint 是需要內核研發人員參數編寫,其數量有限,並不是所有的內核函數中都具有類似的跟蹤點,所以從靈活性上不如 kprobes 這種方式。

2.1 kprobe 和 tracepoint 對比

在 3.10 內核中,kprobetracepoint 方式對比如下:

內容 kprobe tracepoint
追蹤類型 動態 靜態
Hook 點數量 100000+ 1200+
穩定的 API

可以使用以下命令查看系統支持的 tracepoint,支持 grep 檢索。

perf list
perf list | grep execve

image

上面的執行結果可以看到,execve系統調用具有兩個 syscalls 類型的靜態跟蹤點,並且,tracepoint 已經對 enter 和 exit 做了區分,其功能基本等同於 kprobe/kretprobe

在使用 tracepoint 之前,我們需要瞭解 tracepoint 相關參數的格式。syscalls:sys_enter_execve 格式定義在 /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format 文件中。

# 查看 syscalls:sys_enter_execve 參數
cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format

2.2 重構代碼

接下來,使用 tracepoint 方式重構第 1 節的代碼,如下:

// execsnoop.c
#include <linux/sched.h>
#include <linux/fs.h>

#define ARGSIZE         128
#define MAXARG          60

enum event_step {
	STEP_1,
	STEP_2,
};
struct data_t {
	u32 pid;
	char comm[TASK_COMM_LEN];
	enum event_step step;
	char argv[ARGSIZE];
};
BPF_PERF_OUTPUT(events);
static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
	bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);
	events.perf_submit(ctx, data, sizeof(struct data_t));
	return 1;
}
static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
	const char *argp = NULL;
	bpf_probe_read_user(&argp, sizeof(argp), ptr);
	if (argp) {
		return __submit_arg(ctx, (void *)(argp), data);
	}
	return 0;
}
// (A) sys_enter_execve tracepoint
TRACEPOINT_PROBE(syscalls, sys_enter_execve) {
	struct data_t data = {};
	const char **argv = (const char **) (args->argv);
	
	data.step = STEP_1;
	data.pid = bpf_get_current_pid_tgid() >> 32;
	bpf_get_current_comm(&data.comm, sizeof(data.comm));

	#pragma unroll
	for (int i = 1; i < MAXARG; i++) {
		// (B) args 強制轉換為 ctx
		if (submit_arg((struct pt_regs *)args, (void *)&argv[i], &data) == 0)
			goto out;
	}
	char ellipsis[] = "...";
	__submit_arg((struct pt_regs *)args, (void *)ellipsis, &data);
out:
	return 0;
}
// sys_exit_execve tracepoint
TRACEPOINT_PROBE(syscalls, sys_exit_execve) {
	struct data_t data = {};
	data.step = STEP_2;
	data.pid = bpf_get_current_pid_tgid() >> 32;
	bpf_get_current_comm(&data.comm, sizeof(data.comm));

	events.perf_submit(args, &data, sizeof(data));
	return 0;
}
# execsnoop.py
#!/usr/bin/python3
from bcc import BPF
from bcc.utils import printb
from collections import defaultdict

argv = defaultdict(list)
class EventStep(object):
	STEP_1 = 0
	STEP_2 = 1

# (C) 不再通過 kprobe 綁定 
b = BPF(src_file="execsnoop.c")
print("%-7s %-16s %s" % ("PID", "PCOMM", "ARGS"))

# process event
def print_event(cpu, data, size):
	event = b["events"].event(data)
	if event.step == EventStep.STEP_1:
		argv[event.pid].append(event.argv)
	elif event.step == EventStep.STEP_2:
		argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
		printb(b"%-7d %-16s %s" % (event.pid, event.comm, argv_text))
		try:
			del(argv[event.pid])
		except Exception:
			pass

# loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
	try:
		b.perf_buffer_poll()
	except KeyboardInterrupt:
		exit()

註意:

A)一個 tracepoint 定義接收兩個參數,TRACEPOINT_PROBE(syscalls, sys_enter_execve) 第一個為子系統名稱,第二個為事件名稱。

B)tracepoint 中的所有參數都會包含在一個固定名稱的 args 的結構體中。args 類型為 struct tracepoint__syscalls__sys_enter_open,其第一個欄位為 u64 __do_not_use__;,該欄位為 ctx 的保留位置。因此,args 可以被強制轉換為 ctx

ctx 是啥?

在《Linux 內核觀測技術 BPF》一書中,ctx被稱為“上下文”,提供了訪問內核正在處理的信息。我們可以通過 PT_REGS_RC(ctx) 來獲取當前函數的返回值。

C)用戶態代碼不再需要 attach_kprobe 手動綁定。

3 eBPF 程式事件類型

像是 kprobetracepoint 將 eBPF 程式掛載到內核事件的方式,可以暫且被稱為 eBPF 事件類型。事實上,除了以上列出的兩種,eBPF 事件類型還有很多,選取其中一些列舉如下:

  • kprobes/kretprobes:內核函數事件。不再贅述。
  • tracepoint:內核跟蹤點事件。不再贅述。
  • uprobes/uretprobes:用戶空間函數事件,可以綁定監聽一個用戶空間的函數。
  • USDT probes:用戶自定義的靜態追蹤點。用戶可以在用戶空間的程式中插入靜態追蹤點,用於掛載 eBPF。
  • LSM Probes:LSM Hook 掛載點。需要內核版本 5.7 以上。

由於篇幅限制,不再列舉其他 eBPF 事件類型了,後面如果有精力,再補一篇文章。

4 總結

本文在前一篇文章的基礎上,對進程執行監控工具(execsnoop)進行了升級,實時列印進程執行時傳入的參數列表;並通過 kprobetracepoint 兩種方式,綁定 eBPF 程式,給出了代碼實現。同時,對這兩種 eBPF 事件類型進行了簡單比較。顯然,在你手動開發一個 eBPF 程式時,建議使用 tracepoint,以追求更好的穩定性和可移植性。文章的最後,簡單列出了一些支持的 eBPF 事件類型。

以上拋磚引玉,如有不正確指出,請大家及時斧正。如果你喜歡這篇文章,請點個推薦吧!


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

-Advertisement-
Play Games
更多相關文章
  • 在C++中,由於該語言本身不提供直接處理Excel文件的標準庫,常常需要藉助第三方類庫。以下是一些在C++中用於處理Excel的熱門開源類庫: SimpleXlsxWriter: 功能: SimpleXlsxWriter是一個輕量級的C++庫,用於生成Microsoft Excel 2007+ xl ...
  • 在日常應用中,當滑鼠放置在某些控制項上時,都會有相應的信息提示,從軟體易用性上來說,這是一個非常友好的功能設計。那在WPF中,如何進行控制項信息提示呢?這就是本文需要介紹的ToolTip【工具提示】內容,本文以一些簡單的小例子,簡述如何在WPF開發中,應用工具提示,僅供學習分享使用,如有不足之處,還請指... ...
  • 問題 Can not create proxy for type xxx because type xxx is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsV ...
  • 前言: 繼上篇:Taurus .Net Core 微服務開源框架:Admin 插件【4-6】 - 配置管理-Mvc【Plugin-Doc 介面測試及文檔】 本篇繼續介紹下一個內容: 系統配置節點:Mvc - Plugin - Metric 介面調用次數統計: 配置界面如下: 1、Metric.IsE ...
  • 在Winform系統開發中,為了對系統的工具欄/菜單進行動態的控制,我們對系統的工具欄/菜單進行動態配置,這樣可以把系統的功能彈性發揮到極致。通過動態工具欄/菜單的配置方式,我們可以很容易的為系統新增所需的功能,通過許可權分配的方式,可以更有效的管理系統的菜單分配到不同的角色用戶,也就是插件化的處理方... ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT1170內部RAM的ECC初始化工作可全部由ROM完成。 痞子衡之前寫了三篇文章 《M7 FlexRAM ECC》、《M4 L-MEM ECC》、《MECC64》 分別介紹了 i.MXRT1170 片上 2MB RAM 的不 ...
  • 一.安裝anaconda3 前往清華園鏡像下載anaconda3的安裝包 https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/?C=M&O=D 選擇最新鏡像Anaconda3-2023.09-0-Linux-x86_64.sh 安裝 1 bas ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT1170 MECC64功能特點及其保護片內OCRAM1,2之道。 ECC是 “Error Correcting Code” 的簡寫,ECC 能夠實現錯誤檢查和糾正,含有 ECC 功能的記憶體一般稱為 ECC 記憶體,使用了 EC ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...