PG14:adminpack 插件源碼分析

来源:https://www.cnblogs.com/zardfans/p/18062886
-Advertisement-
Play Games

adminpack 提供了大量支持功能,pgAdmin 和其他管理工具可以使用這些功能提供額外功能,例如遠程管理伺服器日誌文件。預設情況下,只有資料庫超級用戶才能使用所有這些功能,但其他用戶也可以使用 GRANT 命令使用這些功能。 我們先來看一下他支持的函數,可以通過 \dx+ adminpack ...


adminpack 提供了大量支持功能,pgAdmin 和其他管理工具可以使用這些功能提供額外功能,例如遠程管理伺服器日誌文件。預設情況下,只有資料庫超級用戶才能使用所有這些功能,但其他用戶也可以使用 GRANT 命令使用這些功能。

我們先來看一下他支持的函數,可以通過 \dx+ adminpack 來進行查看

  • function pg_file_rename(text,text) 重命名文件
  • function pg_file_rename(text,text,text) 重命名文件,如果新文件存在,將將其命名為第三個參數的名字
  • function pg_file_sync(text) 文件刷入磁碟
  • function pg_file_unlink(text) 刪除文件
  • function pg_file_write(text,text,boolean) 寫文件
  • function pg_logdir_ls() 列出日誌目錄下的文件

pg_file_rename(text,text)

用於重命名文件,我們看一下 sql 代碼

CREATE FUNCTION pg_catalog.pg_file_rename(text, text)
RETURNS bool
AS 'SELECT pg_catalog.pg_file_rename($1, $2, NULL::pg_catalog.text);'
LANGUAGE SQL VOLATILE STRICT;

這裡我們看到兩個參數版本的 pg_file_rename 直接調用來三參數版本的 pg_file_rename, 因此我們直接查看三參數版本的 SQL 代碼

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_rename(text, text, text)
RETURNS bool
AS 'MODULE_PATHNAME', 'pg_file_rename_v1_1'
LANGUAGE C VOLATILE;

這個 SQL 代碼直接調用來 C 函數 pg_file_rename_v1_1 來實現文件重命名。

現在我們來看一下 C 函數 pg_file_rename_v1_1

Datum
pg_file_rename_v1_1(PG_FUNCTION_ARGS)
{
	text	   *file1;
	text	   *file2;
	text	   *file3;
	bool		result;

	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
		PG_RETURN_NULL();

	file1 = PG_GETARG_TEXT_PP(0);
	file2 = PG_GETARG_TEXT_PP(1);

	if (PG_ARGISNULL(2))
		file3 = NULL;
	else
		file3 = PG_GETARG_TEXT_PP(2);

	result = pg_file_rename_internal(file1, file2, file3);

	PG_RETURN_BOOL(result);
}

這個代碼中僅僅是判斷參數是否為空,如果不為空,則獲取參數,然後調用 pg_file_rename_internal 這個函數

static bool
pg_file_rename_internal(text *file1, text *file2, text *file3)
{
	char	   *fn1,
			   *fn2,
			   *fn3;
	int			rc;

	fn1 = convert_and_check_filename(file1);
	fn2 = convert_and_check_filename(file2);

	if (file3 == NULL)
		fn3 = NULL;
	else
		fn3 = convert_and_check_filename(file3);

	if (access(fn1, W_OK) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("file \"%s\" is not accessible: %m", fn1)));

		return false;
	}

	if (fn3 && access(fn2, W_OK) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("file \"%s\" is not accessible: %m", fn2)));

		return false;
	}

	rc = access(fn3 ? fn3 : fn2, W_OK);
	if (rc >= 0 || errno != ENOENT)
	{
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_FILE),
				 errmsg("cannot rename to target file \"%s\"",
						fn3 ? fn3 : fn2)));
	}

	if (fn3)
	{
		if (rename(fn2, fn3) != 0)
		{
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not rename \"%s\" to \"%s\": %m",
							fn2, fn3)));
		}
		if (rename(fn1, fn2) != 0)
		{
			ereport(WARNING,
					(errcode_for_file_access(),
					 errmsg("could not rename \"%s\" to \"%s\": %m",
							fn1, fn2)));

			if (rename(fn3, fn2) != 0)
			{
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not rename \"%s\" back to \"%s\": %m",
								fn3, fn2)));
			}
			else
			{
				ereport(ERROR,
						(errcode(ERRCODE_UNDEFINED_FILE),
						 errmsg("renaming \"%s\" to \"%s\" was reverted",
								fn2, fn3)));
			}
		}
	}
	else if (rename(fn1, fn2) != 0)
	{
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not rename \"%s\" to \"%s\": %m", fn1, fn2)));
	}

	return true;
}

這個函數的整體邏輯為,先將 text* 類型的數據轉換為 char* 類型的數據,會在這個轉換的過程中處理一些路徑相關和許可權驗證的問題。

然後先判斷一下 fn1 是不是存在,如果不存在那肯定是沒法重命名的,然後判斷一下 fn3 是不是為空,並且 fn2 是不是存在,如果不存在,那麼將 fn2 重命名為 fn3 也會失敗。

然後判斷一下 fn3 是不是存在,如果存在,那麼說明文件已經存在,肯定不能重命名,也會報一個 DUPLICATE 的錯誤。如果 fn3 為空,那麼就判斷 fn2 文件是不是存在,如果存在那也是不能重命名的。

接下來,我們就可以將 fn2 重命名成 fn3, 然後將 fn1 重命名為 fn2,

如果 fn1 重命名為 fn2 出錯,則將 fn3 重名名為 fn2 ,即撤消之前的修改操作。

那如果 fn3 為空,直接將 fn1 重命名為 fn2 就可以了,

pg_file_sync

我們先來看一下 SQL 代碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_sync(text)
RETURNS void
AS 'MODULE_PATHNAME', 'pg_file_sync'
LANGUAGE C VOLATILE STRICT;

可以看到他調用的是 C 函數 pg_file_sync ,我們來看一下這個 C 代碼:

Datum
pg_file_sync(PG_FUNCTION_ARGS)
{
	char	   *filename;
	struct stat fst;

	filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));

	if (stat(filename, &fst) < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not stat file \"%s\": %m", filename)));

	fsync_fname_ext(filename, S_ISDIR(fst.st_mode), false, ERROR);

	PG_RETURN_VOID();
}

可以看到這個僅僅是將 text 類型的數據轉換為 char * 類型的數據。然後調用 storage 的函數實現的功能。(src/backend/storage/file/fd.c)

我們先來看一下 SQL 代碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_unlink(text)
RETURNS bool
AS 'MODULE_PATHNAME', 'pg_file_unlink_v1_1'
LANGUAGE C VOLATILE STRICT;

發現它調用了 C 函數 pg_file_unlink_v1_1. 我們來看一下這個函數:

Datum
pg_file_unlink_v1_1(PG_FUNCTION_ARGS)
{
	char	   *filename;

	filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));

	if (access(filename, W_OK) < 0)
	{
		if (errno == ENOENT)
			PG_RETURN_BOOL(false);
		else
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("file \"%s\" is not accessible: %m", filename)));
	}

	if (unlink(filename) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("could not unlink file \"%s\": %m", filename)));

		PG_RETURN_BOOL(false);
	}
	PG_RETURN_BOOL(true);
}

這個函數的整體邏輯是將 text* 類型的數據轉換為 char* 類型的數據,並處理路徑相關的問題,然後判斷一下文件是不是可訪問的。然後調用 unlink 對文件進行刪除。

pg_file_write

看一下 SQL 代碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_write(text, text, bool)
RETURNS bigint
AS 'MODULE_PATHNAME', 'pg_file_write_v1_1'
LANGUAGE C VOLATILE STRICT;

發現它調用的是 C 函數 pg_file_write_v1_1

Datum
pg_file_write_v1_1(PG_FUNCTION_ARGS)
{
	text	   *file = PG_GETARG_TEXT_PP(0);
	text	   *data = PG_GETARG_TEXT_PP(1);
	bool		replace = PG_GETARG_BOOL(2);
	int64		count = 0;

	count = pg_file_write_internal(file, data, replace);

	PG_RETURN_INT64(count);
}

/* ------------------------------------
 * pg_file_write_internal - Workhorse for pg_file_write functions.
 *
 * This handles the actual work for pg_file_write.
 */
static int64
pg_file_write_internal(text *file, text *data, bool replace)
{
	FILE	   *f;
	char	   *filename;
	int64		count = 0;

	filename = convert_and_check_filename(file);

	if (!replace)
	{
		struct stat fst;

		if (stat(filename, &fst) >= 0)
			ereport(ERROR,
					(errcode(ERRCODE_DUPLICATE_FILE),
					 errmsg("file \"%s\" exists", filename)));

		f = AllocateFile(filename, "wb");
	}
	else
		f = AllocateFile(filename, "ab");

	if (!f)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\" for writing: %m",
						filename)));

	count = fwrite(VARDATA_ANY(data), 1, VARSIZE_ANY_EXHDR(data), f);
	if (count != VARSIZE_ANY_EXHDR(data) || FreeFile(f))
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write file \"%s\": %m", filename)));

	return (count);
}

我們可以看到 pg_file_write_v1_1 僅僅是獲取了參數,然後就調用了 pg_file_write_internal 函數。
這個函數的主要邏輯是將 text* 的數據轉換為 char* 的數據 。然後判斷一下 replace 參數,如果為 false,則文件不能存在,然後使用 AllocateFile 創建一個文件。
然後使用 fwrite 將數據寫入文件,VARDATA_ANY 巨集的作用是獲取實際的數據指針,VARSIZE_ANY_EXHDR 的作用是獲取數據的長度。最後返回寫入的長度。

pg_logdir_ls

我們看一下 SQL 代碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_logdir_ls()
RETURNS setof record
AS 'MODULE_PATHNAME', 'pg_logdir_ls_v1_1'
LANGUAGE C VOLATILE STRICT;

這裡它調用了 C 函數 pg_logidr_ls_v1_1,我們看一下這個 C 函數:

Datum
pg_logdir_ls_v1_1(PG_FUNCTION_ARGS)
{
	return (pg_logdir_ls_internal(fcinfo));
}

static Datum
pg_logdir_ls_internal(FunctionCallInfo fcinfo)
{
	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
	bool		randomAccess;
	TupleDesc	tupdesc;
	Tuplestorestate *tupstore;
	AttInMetadata *attinmeta;
	DIR		   *dirdesc;
	struct dirent *de;
	MemoryContext oldcontext;

	if (strcmp(Log_filename, "postgresql-%Y-%m-%d_%H%M%S.log") != 0)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("the log_filename parameter must equal 'postgresql-%%Y-%%m-%%d_%%H%%M%%S.log'")));

	/* check to see if caller supports us returning a tuplestore */
	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("set-valued function called in context that cannot accept a set")));
	if (!(rsinfo->allowedModes & SFRM_Materialize))
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("materialize mode required, but it is not allowed in this context")));

	/* The tupdesc and tuplestore must be created in ecxt_per_query_memory */
	oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);

	tupdesc = CreateTemplateTupleDesc(2);
	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "starttime",
					   TIMESTAMPOID, -1, 0);
	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "filename",
					   TEXTOID, -1, 0);

	randomAccess = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0;
	tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
	rsinfo->returnMode = SFRM_Materialize;
	rsinfo->setResult = tupstore;
	rsinfo->setDesc = tupdesc;

	MemoryContextSwitchTo(oldcontext);

	attinmeta = TupleDescGetAttInMetadata(tupdesc);

	dirdesc = AllocateDir(Log_directory);
	while ((de = ReadDir(dirdesc, Log_directory)) != NULL)
	{
		char	   *values[2];
		HeapTuple	tuple;
		char		timestampbuf[32];
		char	   *field[MAXDATEFIELDS];
		char		lowstr[MAXDATELEN + 1];
		int			dtype;
		int			nf,
					ftype[MAXDATEFIELDS];
		fsec_t		fsec;
		int			tz = 0;
		struct pg_tm date;

		/*
		 * Default format: postgresql-YYYY-MM-DD_HHMMSS.log
		 */
		if (strlen(de->d_name) != 32
			|| strncmp(de->d_name, "postgresql-", 11) != 0
			|| de->d_name[21] != '_'
			|| strcmp(de->d_name + 28, ".log") != 0)
			continue;

		/* extract timestamp portion of filename */
		strcpy(timestampbuf, de->d_name + 11);
		timestampbuf[17] = '\0';

		/* parse and decode expected timestamp to verify it's OK format */
		if (ParseDateTime(timestampbuf, lowstr, MAXDATELEN, field, ftype, MAXDATEFIELDS, &nf))
			continue;

		if (DecodeDateTime(field, ftype, nf, &dtype, &date, &fsec, &tz))
			continue;

		/* Seems the timestamp is OK; prepare and return tuple */

		values[0] = timestampbuf;
		values[1] = psprintf("%s/%s", Log_directory, de->d_name);

		tuple = BuildTupleFromCStrings(attinmeta, values);

		tuplestore_puttuple(tupstore, tuple);
	}

	FreeDir(dirdesc);
	return (Datum) 0;
}

這裡它直接調用了 pg_logdir_ls_internal 函數,其中的 fcinfo 是這個巨集展開的:

#define PG_FUNCTION_ARGS FunctionCallInfo fcinfo

我們來看一下 pg_logdir_ls_internal 函數:

首先我們看到了它從 fcinfo 中獲取了一個 resultinfo 的欄位,然後將其轉換為 ReturnSetInfo 這個結構體指針。我們來看一下這個結構體

/*
 * When calling a function that might return a set (multiple rows),
 * a node of this type is passed as fcinfo->resultinfo to allow
 * return status to be passed back.  A function returning set should
 * raise an error if no such resultinfo is provided.
 */
typedef struct ReturnSetInfo
{
	NodeTag		type;
	/* values set by caller: */
	ExprContext *econtext;		/* context function is being called in */
	TupleDesc	expectedDesc;	/* tuple descriptor expected by caller */
	int			allowedModes;	/* bitmask: return modes caller can handle */
	/* result status from function (but pre-initialized by caller): */
	SetFunctionReturnMode returnMode;	/* actual return mode */
	ExprDoneCond isDone;		/* status for ValuePerCall mode */
	/* fields filled by function in Materialize return mode: */
	Tuplestorestate *setResult; /* holds the complete returned tuple set */
	TupleDesc	setDesc;		/* actual descriptor for returned tuples */
} ReturnSetInfo;

從這個註釋中我們可以看到,當我們調用的函數需要返回一個集合,即多行數據的時候,我們就可以使用這個結構體,而 resultinfo 的實際類型是一個 fmNodePtr ,實際上就是一個 Node 節點。而 ReturnSetInfo 也是一個 Node 節點。

接下來我們定義來一些用於返回結果需要的輔助數據結構。

第一個判斷我們的 Log_filename 的格式是不是符合 postgresql-%Y-%m-%d_%H%M%S.log 這個規則,不符合這個規則是無法使用這個函數的。Log_filename 是一個 GUC 參數,你可以在 postgresql.conf 中修改,也可以使用 show log_filename 命令來查看當前的值。

然後我們檢查一下 rsinfo 變數的類型是不是一個 ReturnSetInfo,實際上就是通過 NodeTag 進行比較的。

接下來我們檢查一下 rsinfo 中的 allowedMode 返回模式是否是 SFRM_Materialize 這個模式可以讓我們將要返回的數據都存儲在 Tuplestore 中。

接下來我們需要將記憶體上下文切換到 rsinfo->econtext->ecxt_per_query_memory ,在這個記憶體上下文中我們保存要返回的結果,即這裡的 tuple.

然後我們使用 CreateTemplateTupleDesc 來創建一個 tuple 的描述,其實我感覺可以理解為 DDL ,即這個表各個欄位是什麼樣的數據。這裡我們添加了兩個欄位,其數據類型分別為 TIMESTAMPTEXT.

然後我們使用 tuplestore_begin_heap 函數創建一個 tuplestore ,我們最終的結果也將存放在這個數據結構中。

attinmeta = TupleDescGetAttInMetadata(tupdesc); 這個語句把 tuple 的屬性信息存儲到 attinmeta 這個會在我們後續構造 tuple 的時候要用到。

接下來,我們打開目錄,這個 Log_directory 也是一個 GUC 參數可以設置和查看。
這個目錄是相對於 data 目錄來說的。

接下來,我們遍歷目錄中的文件,查看文件的名字是否滿足 postgresql- 這種形式,然後將文件名中包含的日期信息提取出來,然後將其變成好看的日期格式,最後我們將日期和文件名寫入到一個 tuple 里

values[0] = timestampbuf;
values[1] = psprintf("%s/%s", Log_directory, de->d_name);
tuple = BuildTupleFromCStrings(attinmeta, values);

其中的 attinmeta 就是我們之前準備好的 tuple 描述信息,構造好一個 tuple 以後,我們就可以把它放在 tuplestore 裡邊了。

tuplestore_puttuple(tupstore, tuple);

最後關閉目錄。


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

-Advertisement-
Play Games
更多相關文章
  • 一:新建虛擬主機 1. 在tomcat里新建文件夾myapps,在裡面添加ROOT文件,放入網站的首頁文件 新建文本文檔,輸入你想要的內容我這裡的內容是TOM.AI,把文本文檔的名字改成index.htm 2. server.xml下每個host節點就代表一個主機,相當於一個網站。 用記事本打開to ...
  • 哈嘍大家好,我是鹹魚。 今天收到了一個告警,說有台伺服器上的 swap 過高,已經用了 50% 以上了。 登錄機器查看一下記憶體以及 swap 的使用情況。 [root@localhost ~]# free -h total used free shared buff/cache available ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是使用恩智浦GUI Guider快速創建全新LCD屏示例工程的步驟。 在痞子衡舊文 《在i.MXRT1170上快速點亮一款全新LCD屏的方法與步驟》 里,痞子衡介紹了在官方 SDK 裸機驅動 elcdif 示例工程基礎上做修改以支持一款全 ...
  • Linux系統提供了許多命令來分析系統性能。以下是一些常用的Linux系統性能分析命令: top:實時監視系統的運行狀態和進程信息,包括CPU使用率、記憶體使用情況、進程狀態等。 實例:直接在終端中輸入top,即可實時查看系統進程狀態及資源占用情況。 htop:類似於top,但提供更友好的界面和交互, ...
  • 一、下載Nginx安裝包 Nginx官網下載地址 根據需求選擇自己需要的版本下載後上傳至伺服器(路徑自行決定)。 如果伺服器有外網,可以直接在伺服器上下載。 wget -c https://nginx.org/download/nginx-1.24.0.tar.gz 二、安裝Nginx 解壓安裝包 ...
  • 引言 關係資料庫中的關係滿足一定要求的,滿足不同程度要求的為不同的範式,共有6種範式。 滿足最低要求的叫第一範式,簡稱 1NF;在第一範式的基礎上滿足進一步要求的稱為第二範式,簡稱 2NF; 其餘範式以此類推。 對於各種範式之間有如下關係: 5NF ∈ 4NF ∈ BCNF ∈ 3NF ∈ 2NF ...
  • auth_delay 讓伺服器在報告身份驗證失敗前短暫暫停,以增加對資料庫密碼進行暴力破解的難度。需要註意的是,這對阻止拒絕服務攻擊毫無幫助,甚至可能加劇攻擊,因為在報告身份驗證失敗前等待的進程仍會占用連接。 要使用這個模塊必須要在 postgresql.conf 中配置參數 shared_prel ...
  • 本文分享自華為雲社區《GaussDB資料庫的索引管理》,作者: Gauss松鼠會小助手2。 一、引言 GaussDB資料庫是華為公司傾力打造的自研企業級分散式關係型資料庫,索引的設計和管理對於提高查詢性能至關重要。下麵將通過實際例子深入研究GaussDB資料庫的索引管理。 二、GaussDB資料庫中 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...