C 共用記憶體封裝

来源:https://www.cnblogs.com/life2refuel/archive/2019/01/05/10225517.html
-Advertisement-
Play Games

引言 - 背景 2016 年寫過一篇關於 linux 共用記憶體 shm api 掃盲文. C擴展 從共用記憶體shm到memcache外部記憶體 比較簡單. 沒有深入分析(能力有限, 也深入分析不了). 3年(2019)過去了. 本質而言共用記憶體這種編程活化石般 雙刃劍, 像 "redis" 這種分散式 ...


引言 - 背景

  2016 年寫過一篇關於 linux 共用記憶體 shm api 掃盲文. C擴展 從共用記憶體shm到memcache外部記憶體

比較簡單. 沒有深入分析(能力有限, 也深入分析不了). 3年(2019)過去了. 本質而言共用記憶體這種編程活化石般

雙刃劍, 像 "redis" 這種分散式記憶體資料庫完全可以替代它做想做的業務(硬體過剩).  這裡為什麼繼續鞭屍呢?

想要為這種最快的數據交互 IPC 方式, 做個多平臺移植實戰代碼. 更加詳細底層原理和麵試問題, 請自行搜索.

好的疑惑和思索可以分享個博主, 共同提升.

 

前言 - shm api 介紹

  先帶大家熟悉 linux 和 winds 共用記憶體 shm 相關的 api 介紹.  編程很簡單, 無外乎學習和實踐. 當然不寫

代碼, 狂看原理的, 當我沒說. 畢竟身居高位, 指點江山, 都曾經有過夢想, 也流過無數智慧 ~

1. linux shm 相關 api 熟悉

  linux shm 有兩套 api 這裡還是只說 System V 標準的 4 個系統 api

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

/*
 * System V 風格的 IPC 函數實現共用記憶體介面
 */

//
// shmctl - 共用記憶體控制操作
// __shmid  : 共用記憶體標識符, shmget () 返回返回值
// __cmd    : IPC_STAT -> 得到共用記憶體的狀態, 把共用記憶體的 shmid_ds 結構複製到 buf 中
//          : IPC_SET  -> 改變共用記憶體的狀態, 把 buf 複製到共用記憶體的 shmid_ds 結構內
//          : IPC_RMID -> 刪除這片共用記憶體
// __buf    : 共用記憶體管理結構體
// return   : 0 正確, 出錯 -1, 錯誤原因查 errno 表
//          : EINVAL   -> 參數 size 小於 SHMMIN 或大於 SHMMAX
//          : EEXIST   -> 預建立 key 所致的共用記憶體, 但已經存在
//          : EIDRM    -> 參數 key 所致的共用記憶體已經刪除
//          : ENOSPC   -> 超過了系統允許建立的共用記憶體的最大值 SHMALL
//          : ENOENT   -> key 所指的共用記憶體不存在, 參數 shmflg 也未設 IPC_CREAT 位
//          : EACCES   -> 沒有許可權
//          : ENOMEM   -> 核心記憶體不足
//
//
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;

//
// shmget - 得到共用記憶體段
// __key    : 新建或者獲取已經存在的共用記憶體的 key
// __size   : 創建共用記憶體的大小, 獲取設置預設 0
// __shmflg : 標誌集合
//          : IPC_CREAT -> 若不存在則創建, 需要在 shmflg 中 '|許可權信息' |0664 若存在則打開
//          : IPC_EXCL  -> 與 IPC_CREAT 搭配使用, 若存在則創建失敗 errno == EEXIST
//          : 0         -> 獲取已經存在的共用記憶體
// return   : __shmid, 出錯 -1
//
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;

//
// shmat - 附加共用記憶體段
// __shmid  : 共用記憶體標識符, shmget () 返回返回值
// __shmaddr: NULL 表示由系統選擇
//          : 非 NULL 且 shmflg 是 SHM_RND, 會按照頁對齊的原則從 shmaddr 開始
//            找最近的地址開始分配, 否則 shmaddr 指定的地址必須是頁對齊的
// __shmflg : 0          -> 預設操作標誌
//          : SHM_RDONLY -> 表示掛接到該共用記憶體的進程必須有讀許可權
//          : SHM_REMAP  -> 表示如果要映射的共用記憶體已經有現存的記憶體, 那麼就將舊的替換
// return   : 成功返回映射記憶體的地址, 失敗返回 (void *)-1 設 errno
//
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg) __THROW;

//
// shmdt - 分離共用記憶體段
// __shmaddr: 共用記憶體標識符, shmat() 返回值
// return   : 成功返回 0, 失敗返回 -1 設 errno
//
extern int shmdt (const void *__shmaddr) __THROW;

對於 shm_ctl 用到的相關結構和巨集截取部分如下

/* Mode bits for `msgget', `semget', and `shmget'.  */
#define IPC_CREAT    01000        /* Create key if key does not exist. */
#define IPC_EXCL    02000        /* Fail if key exists.  */
#define IPC_NOWAIT    04000        /* Return error on wait.  */

/* Control commands for `msgctl', `semctl', and `shmctl'.  */
#define IPC_RMID    0        /* Remove identifier.  */
#define IPC_SET        1        /* Set `ipc_perm' options.  */
#define IPC_STAT    2        /* Get `ipc_perm' options.  */
#ifdef __USE_GNU
# define IPC_INFO    3        /* See ipcs.  */
#endif

/* Special key values.  */
#define IPC_PRIVATE    ((__key_t) 0)    /* Private key.  */


/* Data structure used to pass permission information to IPC operations.  */
struct ipc_perm
  {
    __key_t __key;            /* Key.  */
    __uid_t uid;            /* Owner's user ID.  */
    __gid_t gid;            /* Owner's group ID.  */
    __uid_t cuid;            /* Creator's user ID.  */
    __gid_t cgid;            /* Creator's group ID.  */
    unsigned short int mode;        /* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;        /* Sequence number.  */
    unsigned short int __pad2;
    __syscall_ulong_t __glibc_reserved1;
    __syscall_ulong_t __glibc_reserved2;
  };

/* Permission flag for shmget.  */
#define SHM_R        0400        /* or S_IRUGO from <linux/stat.h> */
#define SHM_W        0200        /* or S_IWUGO from <linux/stat.h> */

/* Flags for `shmat'.  */
#define SHM_RDONLY    010000        /* attach read-only else read-write */
#define SHM_RND        020000        /* round attach address to SHMLBA */
#define SHM_REMAP    040000        /* take-over region on attach */
#define SHM_EXEC    0100000        /* execution access */

/* Commands for `shmctl'.  */
#define SHM_LOCK    11        /* lock segment (root only) */
#define SHM_UNLOCK    12        /* unlock segment (root only) */

__BEGIN_DECLS

/* Segment low boundary address multiple.  */
#define SHMLBA        (__getpagesize ())
extern int __getpagesize (void) __THROW __attribute__ ((__const__));


/* Type to count number of attaches.  */
typedef __syscall_ulong_t shmatt_t;

/* Data structure describing a shared memory segment.  */
struct shmid_ds
  {
    struct ipc_perm shm_perm;        /* operation permission struct */
    size_t shm_segsz;            /* size of segment in bytes */
    __time_t shm_atime;            /* time of last shmat() */
#ifndef __x86_64__
    unsigned long int __glibc_reserved1;
#endif
    __time_t shm_dtime;            /* time of last shmdt() */
#ifndef __x86_64__
    unsigned long int __glibc_reserved2;
#endif
    __time_t shm_ctime;            /* time of last change by shmctl() */
#ifndef __x86_64__
    unsigned long int __glibc_reserved3;
#endif
    __pid_t shm_cpid;            /* pid of creator */
    __pid_t shm_lpid;            /* pid of last shmop */
    shmatt_t shm_nattch;        /* number of current attaches */
    __syscall_ulong_t __glibc_reserved4;
    __syscall_ulong_t __glibc_reserved5;
  };

(滿屏的惡意, 直接從系統 include 拔下來, 木有強迫症) 基於上面大概. 寫個 demo 帶大家熟悉其用法

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

// str_hash_rs - Robert Sedgwicks Hash Function
unsigned str_hash_rs(const char * s) {
    register unsigned hash = 0;
    for (unsigned c, a = 63689; (c = *s); ++s) {
        hash = hash * a + c;
        a *= 378551;
    }
    return hash & 0x7FFFFFFF;
}

#define STR_SHM     "shm_1_to_2"
#define INT_SHM     4096

// shm_init - // shm_init - init 操作, -1 error, 0 init create, 1 exists
int shm_init(key_t key, size_t size);

//
// 文件 : shm.c
// 目標 : 
//       1' 熟悉 Linux System V shm 機制
//       2' 第一次創建共用記憶體, 寫入數據
//       3' 第二次讀取共用記憶體, 隨後銷毀
//
int main(int argc, char * argv[]) {
    char cmd[BUFSIZ];
    key_t key = (key_t)str_hash_rs(STR_SHM);
    printf("name = %s -> key = %d\n", STR_SHM, key);

    // shm init
    int ret = shm_init(key, INT_SHM);
    if (ret < 0) {
        perror("shm_init "STR_SHM);
        return EXIT_FAILURE;
    }

    if (ret == 0) {
        // shm 剛創建
        return EXIT_SUCCESS;
    }

    // ret == 1 shm 已經存在我們開始獲取共用記憶體
    int shmid = shmget(key, 0, 0);
    if (shmid < 0) {
        perror("shmget");
        return EXIT_FAILURE;
    }
    printf("key = %d -> shmid = %d\n", key, shmid);

    // 開始附加共用記憶體段
    void * shmaddr = shmat(shmid, NULL, 0);
    if ((intptr_t)shmaddr < 0) {
        perror("shmat shmid "STR_SHM);
        return EXIT_FAILURE;
    }
    printf("shmid = %d, shmaddr = %p\n", shmid, shmaddr);

    snprintf(cmd, sizeof cmd, "ipcs -m -i %d", shmid);

    // 開始操作記憶體
    long * ptr = shmaddr;
    printf("ptr = %p, *ptr = %ld\n", ptr, *ptr);

    long old = __sync_lock_test_and_set(ptr, 1);
    if (old) {
        // 共用記憶體分離
        printf("__sync_lock_test_and_set old = %ld, *ptr = %ld\n", old, *ptr);
        return shmdt(shmaddr);
    }

    printf("Second run now old = %ld, *ptr = %ld\n", old, *ptr);

    __sync_lock_release(ptr);

    printf("shmid = %d, shmdt after\n> %s\n", shmid, cmd);
    system(cmd);

    // 記憶體分離
    shmdt(shmaddr);

    // 刪除共用記憶體
    shmctl(shmid, IPC_RMID, NULL);

    // 最後測試共用記憶體狀態
    printf("shmid = %d, shmdt after\n> %s\n", shmid, cmd);
    system(cmd);

    return EXIT_SUCCESS;
}

// shm_init - init 操作, -1 error, 0 init create, 1 exists
int 
shm_init(key_t key, size_t size) {
    // 通過 key 創建共用記憶體 or 打開共用記憶體
    int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL|0666);
    if (shmid < 0) {
        if (errno == EEXIST) {
            // 當前的共用記憶體已經存在
            return 1;
        }
        return shmid;
    }

    // create shm and shm at
    void * shmaddr = shmat(shmid, NULL, 0);
    if ((intptr_t)shmaddr < 0) {
        return -1;
    }

    // memset zero
    memset(shmaddr, 0, size);

    // shm dt
    return shmdt(shmaddr);
}

代碼極其適合感受和練習. 寫完後相應的 api 操作就熟悉了. 練習代碼的設計思路是, 跑第一次初始化共用記憶體.

跑第二次輸出共用記憶體中數據. 附帶說一點 linux 可以通過下麵命令查看和刪除共用記憶體

ipcs -m
ipcrm -m [shmid]
ipcs -m | awk -F' ' 'NR!=1{print $2}'

詳細展示是這樣的

wang@zhi:~$ ipcs -m

------------ 共用記憶體段 --------------
鍵           shmid    擁有者   許可權   位元組     連接數    狀態      
0x00000000  753664    wang    600   524288     2     目標       
0x00000000  622593    wang    600   524288     2     目標

ipcrm -m [shmid]

順帶看 demo.c 編譯完執行後輸出結果

wang@zhi:~/code/shm$ gcc -g -Wall demo.c
wang@zhi:~/code/shm$ ./a.out
name = shm_1_to_2 -> key = 1294752001
wang@zhi:~/code/shm$ ./a.out
name = shm_1_to_2 -> key = 1294752001
key = 1294752001 -> shmid = 242122773
shmid = 242122773, shmaddr = 0x7f3ec7fde000
ptr = 0x7f3ec7fde000, *ptr = 0
Second run now old = 0, *ptr = 1
shmid = 242122773, shmdt after
> ipcs -m -i 242122773

共用記憶體段 shmid=242122773
uid=1000        gid=1000        cuid=1000       cgid=1000
模式=0666       訪問許可權=0666
bytes=4096      lpid=7282       cpid=7280       nattch=1
附加時間=Wed Dec 26 21:06:33 2018
脫離時間=Wed Dec 26 21:06:33 2018
更改時間=Wed Dec 26 21:06:32 2018

shmid = 242122773, shmdt after
> ipcs -m -i 242122773
ipcs: id 242122773 not found

2. winds shm 相關 api 熟悉

  跨平臺是個悲傷的話題. 這隻是小人物單純的站隊. 

//
// http://www.office-cn.net/t/api/createfile.htm
// https://docs.microsoft.com/zh-cn/windows/desktop/api/fileapi/nf-fileapi-createfilea
// CreateFile - 創建或打開一個對象的句柄
// lpFileName               : 創建或打開的對象的名字
// dwDesiredAccess          : 指明對象的控制模式. 一個應用程式可以包含讀控制, 寫控制, 讀/寫控制, 設備查詢控制
// dwShareMode              : 指定對象的共用模式
// lpSecurityAttributes     : 一個指向 SECURITY_ATTRIBUTES 結構對象的指針, 決定返回的句柄是否被子進程所繼承
// dwCreationDisposition    : 指明當打開的對象存在或不存在的時候各需怎樣處理
// dwFlagsAndAttributes     : 指定文件屬性和標誌
// hTemplateFile            : 把具有 GENERIC_READ 許可權的句柄指定為一個模板文件
// return                   : 對象句柄
//
HANDLE CreateFile(
  LPCTSTR lpFileName,
  DWORD dwDesiredAccess,
  DWORD dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD dwCreationDisposition,
  DWORD dwFlagsAndAttributes,
  HANDLE hTemplateFile
);

//
// http://www.office-cn.net/t/api/deletefile.htm
// https://docs.microsoft.com/zh-cn/windows/desktop/api/fileapi/nf-fileapi-deletefilea
// DeleteFileA - 刪除指定文件
// lpFileName               : 欲刪除文件的名字
// return                   : 非零表示成功,零表示失敗
//
BOOL DeleteFile(
  LPCSTR lpFileName
);

//
// http://www.office-cn.net/t/api/createfilemapping.htm
// https://docs.microsoft.com/zh-cn/windows/desktop/api/winbase/nf-winbase-createfilemappinga
// CreateFileMapping - 創建一個新的文件映射對象
// hFile                    : 指定欲在其中創建映射的一個文件句柄. INVALID_HANDLE_VALUE 表示在記憶體中創建一個文件映射
// lpFileMappingAttributes  : 指定一個安全對象, 在創建文件映射時使用. 如果為 NULL 表示使用預設安全對象
// flProtect                : 指定文件映射對象的頁面保護
// dwMaximumSizeHigh        : 文件映射的最大長度高32位
// dwMaximumSizeLow         : 文件映射的最大長度低32位
// lpName                   : 指定文件映射對象的名字. 如存在這個名字的一個映射, 函數就會打開它, NULL 表示沒有名字
// return                   : 新建文件映射對象的句柄, NULL 意味著出錯
//
HANDLE CreateFileMapping(
  HANDLE                hFile,
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  DWORD                 flProtect,
  DWORD                 dwMaximumSizeHigh,
  DWORD                 dwMaximumSizeLow,
  LPCSTR                lpName
);

//
// http://www.office-cn.net/t/api/closehandle.htm
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
// CloseHandle - 關閉一個內核對象。其中包括文件、文件映射、進程、線程、安全和同步對象等
// hObject                  : 欲關閉的一個對象的句柄
// return                   : 非零表示成功,零表示失敗
//
BOOL WINAPI CloseHandle(
  _In_ HANDLE hObject
);

//
// http://www.office-cn.net/t/api/index.html?mapviewoffile.htm
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa366761(v=vs.85).aspx
// MapViewOfFile - 將一個文件映射對象映射到當前應用程式的地址空間
// hFileMappingObject       : 文件映射對象的句柄
// dwDesiredAccess          : 映射對象的文件數據的訪問方式
// dwFileOffsetHigh         : 表示文件映射起始偏移的高32位.
// dwFileOffsetLow          : 表示文件映射起始偏移的低32位
// dwNumberOfBytesToMap     : 指定映射文件的位元組數
// return                   : 文件映射在記憶體中的起始地址, NULL 表示出錯
//
LPVOID WINAPI MapViewOfFile(
  _In_ HANDLE hFileMappingObject,
  _In_ DWORD  dwDesiredAccess,
  _In_ DWORD  dwFileOffsetHigh,
  _In_ DWORD  dwFileOffsetLow,
  _In_ SIZE_T dwNumberOfBytesToMap
);

//
// http://www.office-cn.net/t/api/unmapviewoffile.htm
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa366882(v=vs.85).aspx
// UnmapViewOfFile - 在當前應用程式的記憶體地址空間解除對一個文件映射對象的映射
// lpBaseAddress            : 指定要解除映射的一個文件映射的基準地址, 這個地址是早先用 MapViewOfFile 函數獲得的
// return                   : 非零表示成功,零表示失敗
//
BOOL WINAPI UnmapViewOfFile(
  _In_ LPCVOID lpBaseAddress
);

閱讀完我整理註釋和URL. 就沒有其它了.

 

正文 - 共用記憶體封裝

  前戲做完, 是時候進入介面設計正式封裝環節. 

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

-Advertisement-
Play Games
更多相關文章
  • 分別用while迴圈和for迴圈來玩轉猜數字游戲 1.對於限定猜數字次數 (1)while迴圈 number = 56 count = 0while count < 3: guess_number = int(input("guess number:")) if guess_number == nu ...
  • CMD終端關於pip報錯,scrapy報錯的一種處理方法 如果在終端輸入pip,或scrapy,報如下錯誤: Fatal error in launcher: Unable to create process using '"' 這通常是因為你的電腦裝了兩個版本的python(python2.x和p ...
  • 一、Scala程式的開始->HelloScala 這裡的操作如同java的helloworld程式,直接放代碼! 在命令行操作中輸入命令: scalac HelloScala.scala scala HelloScala 二、Scala的數據類型 Scala與java相同,有八種基本數據類型: By ...
  • Python開發IDE:pycharm ,eclipse 快捷鍵:Ctrl+?整體註釋 一·運算符 +(加) -(減) *(乘) /(除) **(冪) %(餘) //(商) 判斷某個東西是否在某個東西裡邊 in not in 結果:布爾值 ==(等於) >(大於) <(小於) >=(大於等於) <= ...
  • Python的基本數據類型有數字(Number),字元串(string),列表(List)、集合(Set),元組(Tuple)和字典(Dictionary)。 數字:1.包括了整型、浮點型和布爾型,還加入了複數(int、float、bool、complex); 2.若是想知道它具體是什麼類型,可以調 ...
  • [原創]使用python對視頻/音頻文件進行詳細信息採集,併進行去重操作 轉載請註明出處 一.關於為什麼用pymediainfo以及pymediainfo的安裝 使用python對視頻/音頻文件進行詳細信息採集,併進行去重操作的核心是使用pymediainfo這個庫 之前本人一直在試著用moviep ...
  • 題意 "題目鏈接" Sol 設$f[i]$表示炸彈到達$i$這個點的概率,轉移的時候考慮從哪個點轉移而來 $f[i] = \sum_{\frac{f(j) (1 \frac{p}{q})}{deg(j)}}$ $f[1]$需要+1(炸彈一開始在1) cpp // luogu judger enabl ...
  • 字典dict字典是由大括弧{鍵:值}組成.字典是無序的.字典的鍵是不可變的,不能使用列表作為鍵.但可以使用元祖作為字典的鍵.例如: 新增 setdefault(鍵,值):如果只寫鍵不寫值將列印該鍵所對應的值,如果沒有找到鍵返回Note fromkeys(字典鍵列表,值):如果值是列表,是可變的那麼如 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...