TinyShell(CSAPP實驗)

来源:https://www.cnblogs.com/Az1r/archive/2022/12/05/16951915.html
-Advertisement-
Play Games

簡介 CSAPP實驗介紹 學生實現他們自己的帶有作業控制的Unix Shell程式,包括Ctrl + C和Ctrl + Z按鍵,fg,bg,和 jobs命令。這是學生第一次接觸併發,並且讓他們對Unix的進程式控制制、信號和信號處理有清晰的瞭解。 什麼是Shell? ​ Shell就是用戶與操作系統內核 ...


簡介

CSAPP實驗介紹

學生實現他們自己的帶有作業控制的Unix Shell程式,包括Ctrl + C和Ctrl + Z按鍵,fg,bg,和 jobs命令。這是學生第一次接觸併發,並且讓他們對Unix的進程式控制制、信號和信號處理有清晰的瞭解。

什麼是Shell?

​ Shell就是用戶與操作系統內核之間的介面,起著協調用戶與系統的一致性和在用戶與系統之間進行交互的作用。

​ Shell最重要的功能是命令解釋,從這種意義上說,Shell是一個命令解釋器。

​ Linux系統上的所有可執行文件都可以作為Shell命令來執行。當用戶提交了一個命令後,Shell首先判斷它是否為內置命令,如果是就通過Shell內部的解釋器將其解釋為系統功能調用並轉交給內核執行;若是外部命令或實用程式就試圖在硬碟中查找該命令並將其調入記憶體,再將其解釋為系統功能調用並轉交給內核執行。在查找該命令時分為兩種情況:

(1)用戶給出了命令的路徑,Shell就沿著用戶給出的路徑進行查找,若找到則調入記憶體,若沒找到則輸出提示信息;

(2)用戶沒有給出命令的路徑,Shell就在環境變數Path所制定的路徑中依次進行查找,若找到則調入記憶體,若沒找到則輸出提示信息。

關於本次實驗

本次實驗需要我們熟讀CSAPP第八章異常控制流

需要設計和實現的函數:

  • eval 函數:解析命令行。

Evaluate the command line that the user has just typed in.

  • builtin_cmd:判斷是否為內置 shell 命令

If the user has typed a built-in command then execute it immediately.

  • do_bgfg:實現內置命令bgfg

Execute the builtin bg and fg commands.

  • waitfg:等待前臺作業完成。

Block until process pid is no longer the foreground process

  • sigchld_handler:捕獲SIGCHLD信號。
  • sigint_handler:捕獲SIGINT信號。
  • sigtstp_handler:捕獲SIGTSTP信號。

TinyShell輔助函數:

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); //解析命令行參數
void sigquit_handler(int sig);//退出的處理函數
/*jobs是全局變數,存儲每一個進程的信息。*/
/*jid為job編號ID,pid為進程ID*/
void clearjob(struct job_t *job);//清除所有工作
void initjobs(struct job_t *jobs);//初始化工作結構體
int maxjid(struct job_t *jobs); //返回jobs中jid的最大值
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//添加job
int deletejob(struct job_t *jobs, pid_t pid); //刪除job
pid_t fgpid(struct job_t *jobs);//返回前臺運行job的pid
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);//返回對應pid的job
struct job_t *getjobjid(struct job_t *jobs, int jid); //返回jid對應的job
int pid2jid(pid_t pid); //pid轉jid
void listjobs(struct job_t *jobs);//遍歷
void usage(void);//幫助信息
void unix_error(char *msg);//報錯unix-style error routine
void app_error(char *msg);//報錯application-style error routine
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);//信號設置

實驗要求

  • tsh的提示符:tsh>

  • 用戶輸入的命令行應該包括一個名字、0或多個參數,並用一個或多個空格分隔。

  • 如果名字是內置命令,tsh立即處理並等待用戶輸入下一個命令行。否則,假定這個名字是一個可執行文件的路徑,tsh在初始子進程的上下文中載入和運行它。

  • tsh不需要支持管(|)或I/O重定向(<>)。

  • 鍵入ctrl-cctrl-z)應該導致SIGINTSIGTSTP)信號被髮送到當前的前臺作業,及其該作業的子孫作業(例如,它創建的任何子進程)。如果沒有前臺工作,那麼信號應該沒有效果。

  • 如果命令行以&結尾,則tsh在後臺運行該作業;否則,在前臺運行該作業

  • 可以用進程ID(PID)tsh賦予的正整數作業IDjob IDJID)標識一個作業。JID用首碼%,例如%5標識作業ID5的作業,5表示PID5的作業。

  • 已經提供了處理作業列表所需的所有函數

  • tsh支持以下內置命令:

    • quit:終止tsh程式
    • jobs:列出所有後臺job
    • bg:後臺運行程式
    • fg:前臺運行程式

回顧

fork

pid_t fork(void)

在函數調用處創建子進程。

父進程函數返回子進程的PID

子進程函數返回0

waitpid

一個進程可以通過waitpid函數來等待它的子進程終止或者停止。

pid_t waitpid(pid_t pid, int *statusp, int options);

pid:判定等待集合的成員

  • pid > 0時,waitpid等待進程ID為pid的進程;
  • pid = -1時,waitpid等待所有它的子進程。

options:修改預設行為

options中有如下選項:

  1. WNOHANG:若當前沒有等待集合中的子進程終止,則立即返回0
  2. WUNTRACED:等待直到某個等待集合中的子進程停止或返回,並返回這個子進程的pid。
  3. WCONTINUED:等待直到某個等待集合中的子進程重新開始執行或終止。
  4. 組合WNOHANG | WUNTRACED:立即返回,如果等待集合中的子進程都沒有被停止或終止,則返回0。如果有,則返回PID。

statusp:檢查已回收子進程的退出狀態

如果statusp參數非空,那麼waitpid就會在status中放入關於導致返回的子進程的狀態信息,status是statusp指向的值。

  • WIFEXITED(status):如果子進程通過調用exit或者返回(return)正常終止,就返回真。
  • ········

kill函數

int kill(pid_t pid, int signo);

  • pid > 0,信號發送給pid進程;
  • pid == 0,把信號發送給本進程(自己)所在的進程組中所有進程,不包括系統進程;
  • pid < 0,把信號發送給組id 為 -pid 的進程組中所有進程;
  • pid == -1,把信號發送給所有進程,除系統進程外(有些進程不接受9和19號信號)

安全的信號處理

目的

讓信號處理程式和主程式它們可以安全地,無錯誤地,按照我們預期地併發地運行。

方法

  1. 處理程式儘可能簡單。

  2. 在處理程式只調用非同步信號安全的函數。

    1. 可重入的(只訪問局部變數)。
    2. 不能被信號處理程式中斷。
  3. 保存和恢復errno。避免干擾其他依賴於errno的部分。解決方法是用局部變數存儲,再恢復。

void Example(int sig) 
{
	int olderrno = errno;
    /*
    this is your code
    */
    errno = olderrno;
}
  1. 阻塞所有信號,保護對共用全局變數數據結構的訪問。
  2. volatile聲明全局變數。
  3. sig_atomic_t聲明標誌。

例:在添加job時,阻塞信號,因為jobs是全局變數。

This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP signals until we can add the job to the job list. This eliminates some nasty races between adding a job to the job list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.

註意

  1. 不可以用信號來對其他進位中發生的事件計數。
  2. 使用原子(atomic)函數如sigsuspend函數消除潛在的競爭並提高效率。

實驗

eval

要點分析:

  1. 創建子進程前需要阻塞信號,防止競爭。

  2. 將子進程加入到jobs後,需要恢復,即解除阻塞。

  3. 創建子進程時,為子進程創建一個新的進程組。

/* 
 * eval - Evaluate the command line that the user has just typed in
 * 
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard.  
*/
void eval(char *cmdline) 
{
    /* $begin handout */
    char *argv[MAXARGS]; /* argv for execve() */
    int bg;              /* should the job run in bg or fg? */
    pid_t pid;           /* process id */
    sigset_t mask;       /* signal mask */

    /* Parse command line */
    bg = parseline(cmdline, argv); 
    if (argv[0] == NULL)  
		return;   /* ignore empty lines */

    if (!builtin_cmd(argv)) { 

			/* 
		* This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP
		* signals until we can add the job to the job list. This
		* eliminates some nasty races between adding a job to the job
		* list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.  
		*/

		if (sigemptyset(&mask) < 0)
			unix_error("sigemptyset error");
		if (sigaddset(&mask, SIGCHLD)) 
			unix_error("sigaddset error");
		if (sigaddset(&mask, SIGINT)) 
			unix_error("sigaddset error");
		if (sigaddset(&mask, SIGTSTP)) 
			unix_error("sigaddset error");
		if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
			unix_error("sigprocmask error");

		/* Create a child process */
		if ((pid = fork()) < 0)
			unix_error("fork error");
		
		/* 
		* Child  process 
		*/

		if (pid == 0) {
			/* Child unblocks signals */
			sigprocmask(SIG_UNBLOCK, &mask, NULL);

			/* Each new job must get a new process group ID 
			so that the kernel doesn't send ctrl-c and ctrl-z
			signals to all of the shell's jobs */
			if (setpgid(0, 0) < 0) 
				unix_error("setpgid error"); 

			/* Now load and run the program in the new job */
			if (execve(argv[0], argv, environ) < 0) {
				printf("%s: Command not found\n", argv[0]);
				exit(0);
			}
	}

	/* 
	 * Parent process
	 */

	/* Parent adds the job, and then unblocks signals so that
	   the signals handlers can run again */
	addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
	sigprocmask(SIG_UNBLOCK, &mask, NULL);

	if (!bg) 
	    waitfg(pid);
	else
	    printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
    }
    /* $end handout */
    return;
}

builtin_cmd

要點分析:

調用listjobs時,屬於訪問全局變數,需要阻塞和解除阻塞。

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
	if(*argv == NULL)
	{
		return 0;
	}
	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");
	}
	if(! strcmp(argv[0], "quit"))
	{

		if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//訪問全局變數需要阻塞
		{
			unix_error("sigprocmask error!");
		}
		int i;
		for(i = 0; i < MAXJOBS; i ++ )//退出時終止所有所有的子進程
		{
			if(jobs[i].pid)
			{
				kill(- jobs[i].pid, SIGINT);
			}
		}
		if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
		{
			unix_error("sigprocmask error!");
		}
		exit(0);//Shell exit
	}else if(! strcmp(argv[0], "jobs"))
	{
		if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//同理,訪問全局變數
		{
			unix_error("sigprocmask error!");
		}
		listjobs(jobs);//遍歷
		if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
		{
			unix_error("sigprocmask error!");
		}
		return 1;
	}else if(! strcmp(argv[0], "&"))
	{
		return 1;// &也是內置命令,需要返回1
	}else if(! strcmp(argv[0], "fg") || ! strcmp(argv[0], "bg"))
	{
		do_bgfg(argv);
		return 1;
	}
    return 0;     /* not a builtin command */
}

do_bgfg

要點分析:

  1. 需要保證參數正確,即將不正確的情況排除。

  2. 區別jid和pid。

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    /* $begin handout */
    struct job_t *jobp = NULL;
    
    /* Ignore command if no argument */
    if (argv[1] == NULL) {
		printf("%s command requires PID or %%jobid argument\n", argv[0]);
		return;
    }
    
    /* Parse the required PID or %JID arg */
    if (isdigit(argv[1][0])) {
		pid_t pid = atoi(argv[1]);
		if (!(jobp = getjobpid(jobs, pid))) {
	    	printf("(%d): No such process\n", pid);
	    	return;
		}
    }
    else if (argv[1][0] == '%') {
		int jid = atoi(&argv[1][1]);
		if (!(jobp = getjobjid(jobs, jid))) {
	    	printf("%s: No such job\n", argv[1]);
	    	return;
		}
    }	    
    else {
		printf("%s: argument must be a PID or %%jobid\n", argv[0]);
		return;
    }

    /* bg command */
    if (!strcmp(argv[0], "bg")) { 
		if (kill(-(jobp->pid), SIGCONT) < 0)
	    	unix_error("kill (bg) error");
		jobp->state = BG;
		printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
    }

    /* fg command */
    else if (!strcmp(argv[0], "fg")) { 
		if (kill(-(jobp->pid), SIGCONT) < 0)
	    		unix_error("kill (fg) error");
		jobp->state = FG;
		waitfg(jobp->pid);
    }
    else {
		printf("do_bgfg: Internal error\n");
		exit(0);
    }
    /* $end handout */
    return;
}

waitfg

要點分析:

  1. 在等待的迴圈不使用可能會無限休眠的pause,也不使用太慢的sleep。

  2. 在等待的迴圈中使用sigsuspend函數,因為它是原子的。

  3. 在等待前,需阻塞chld信號。

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
	sigset_t mask, prev_mask;
	if(sigemptyset(&mask))
	{
		unix_error("sigempty error!");
	}
	if(sigaddset(&mask, SIGCHLD))
	{
		unix_error("sigaddset error!");
	}
	if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//訪問jobs先阻塞chld信號
	{
		unix_error("sigprocmask error!");
	}


	while(fgpid(jobs) == pid)
	{
		sigsuspend(&prev_mask);//消除競爭
	}
	//
	if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
	{
		unix_error("sigprocmask error!");
	}
	
    return;
}

sigchld_handler

要點分析:

  1. 刪除作業信息時,屬於訪問全局變數,需要阻塞全部信號。

  2. 保存恢復errno。

/* 
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate.  
 */
void sigchld_handler(int sig) 
{
	int olderrno = errno;
	
	pid_t pid;
	int status;

	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");
	}
	

	while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
	{
		if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//訪問全局變數前阻塞所有信號
		{
			unix_error("sigprocmask error!");
		}
		struct job_t *temp = getjobpid(jobs, pid);
		if(WIFEXITED(status))//正常結束
		{
			deletejob(jobs, pid);
		}else if(WIFSIGNALED(status))//被未捕獲的信號終止
		{
			
			int jid = pid2jid(pid);

			printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
			deletejob(jobs, pid);
		}else if(WIFSTOPPED(status))//停止的信號
		{
			temp->state = ST;
			int jid = pid2jid(pid);
			printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status));
		}
		fflush(stdout);//之前printf輸出,所以刷新
		if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
		{
			unix_error("sigprocmask error!");
		}
	}


	errno = olderrno; 
    return;
}

sigint_handler

/* 
 * sigint_handler - The kernel sends a SIGINT to the shell whenver the
 *    user types ctrl-c at the keyboard.  Catch it and send it along
 *    to the foreground job.  
 */
void sigint_handler(int sig) 
{
	int olderrno = errno;//保存和恢復errno

	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");//阻塞所有信號
	}
	if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
	{
		unix_error("sigprocmask error!");
	}

    	pid_t pid = fgpid(jobs);
	if(pid != 0)//對進程組發送SIGINT
		kill(-pid, sig);

	if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
	{
		unix_error("sigprocmask error!");
	}
	errno = olderrno;
    return;
}

sigtstp_handler

/*
 * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
 *     the user types ctrl-z at the keyboard. Catch it and suspend the
 *     foreground job by sending it a SIGTSTP.  
 */
void sigtstp_handler(int sig) 
{
	int olderrno = errno;
	sigset_t mask, prev_mask;
	if(sigfillset(&mask))
	{
		unix_error("sigfillset error!");//阻塞所有信號來訪問全局變數
	}
	if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
	{
		unix_error("sigprocmask error!");
	}
	pid_t pid = fgpid(jobs);
	if(pid != 0)//向進程組發送SIGTSTP
		kill(-pid, sig);
		
	if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
	{
		unix_error("sigprocmask error!");
	}
	errno = olderrno;
    return;
}

測試

對比tsh和參考shell程式tshref,測試了16組例子。

本文來自博客園,作者:江水為竭,轉載請註明原文鏈接:https://www.cnblogs.com/Az1r/p/16951915.html


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

-Advertisement-
Play Games
更多相關文章
  • 非對稱加解密應用廣泛,它的存在是致力於解決密鑰通過公共通道傳輸這一經典難題。對稱加密有一個天然的缺點,就是加密方和解密方都要持有同樣的密鑰,而這個密鑰在傳遞過程中有可能會被截獲,從而使加解密失效。 ...
  • To be, or not to be - that is the question. PowerBuilder編程新思維6:裝飾(用最簡單的方式做框架) 問題 這一章,是寫得最艱難的一章,原因有四: 一、WUI的範疇實在太大了 第二部分Outside原計劃寫兩部分內容Dui和Wui,但是發現如果寫 ...
  • 前言 生活中我們看待一個事物總有不同的態度,比如半瓶水,悲觀的人會覺得只有半瓶水了,而樂觀的人則會認為還有半瓶水呢。很多技術思想往往源於生活,因此在多個線程併發訪問數據的時候,有了悲觀鎖和樂觀鎖。 悲觀鎖認為這個數據肯定會被其他線程給修改了,那我就給它上鎖,只能自己訪問,要等我訪問完,其他人才能訪問 ...
  • 在之前的文章中,棧長介紹了 LongAdder 的使用,性能實在太炸了,你還在用 AtomicInteger、AtomicLong 嗎?如果你還不知道 LongAdder,趕緊看我之前寫的那篇文章。 上次也提到了,在 JDK 8+ 中的 atomic 包下,還有另外一個兄弟類:LongAccumul ...
  • 在 C++11 之前,C++ 編程只能使用 C-style 日期時間庫,其精度只有秒級別,這對於有高精度要求的程式來說,是不夠的。但這個問題在C++11 中得到瞭解決,C++11 中不僅擴展了對於精度的要求,也為不同系統的時間要求提供了支持。另一方面,對於只能使用 C-style 日期時間庫的程式來... ...
  • Java封裝OkHttp3工具類,適用於Java後端開發者。 說實在話,用過挺多網路請求工具,有過java原生的,HttpClient3和4,但是個人感覺用了OkHttp3之後,之前的那些完全不想再用了。 怎麼說呢,代碼輕便,使用起來很很很靈活,響應快,比起HttpClient好用許多。當然,這些是 ...
  • Android ViewPager2 + TabLayout + BottomNavigationView 實際案例 本篇主要介紹一下 ViewPager2 + TabLayout + BottomNavigationView 的結合操作 概述 相信大家都看過今日頭條的的樣式 如下: 頂部有這種ta ...
  • 一、序言 關於HuTool工具包,相信很多技術朋友都聽說甚至使用過。在HuTool之前,已經有比較成熟的工具包比如Apache Common包,谷歌推出的Guava包,他們已經在全世界大範圍使用了。 究竟是什麼原因導致HuTool有後來居上的趨勢,傳統的工具包對於國內開發者來講劣勢在哪裡呢,不妨來扒 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...