使用cat讀取和echo寫內核文件節點的一些問題

来源:http://www.cnblogs.com/pengdonglin137/archive/2017/12/09/8012793.html
-Advertisement-
Play Games

作者 [email protected] 彭東林 平臺 busybox-1.24.2 Linux-4.10.17 Qemu+vexpress-ca9 概述 在寫驅動的時候,我們經常會向用戶空間導出一些文件,然後用戶空間使用cat命令去讀取該節點,從而完成kernel跟user的通信。但是有 ...


作者

[email protected] 彭東林  

平臺

busybox-1.24.2 Linux-4.10.17 Qemu+vexpress-ca9  

概述

在寫驅動的時候,我們經常會向用戶空間導出一些文件,然後用戶空間使用cat命令去讀取該節點,從而完成kernel跟user的通信。但是有時會發現,如果節點對應的read回調函數寫的有問題的話,使用cat命令後,節點對應的read函數會被頻繁調用,log直接刷屏,而我們只希望read被調用一次,echo也是一樣的道理。背後的原因是什麼呢?如何解決呢?下麵我們以debugfs下的節點讀寫為例說明一下。  

正文

 

一、read和write的介紹

  1、系統調用 read   ssize_t read(int fd, void *buf, size_t count);
這個函數會從fd表示的文件描述符中讀取count個位元組到buf緩衝區當中,返回值有下麵幾種: 如果返回值大於0,表示實際讀到的位元組數,返回0的話,表示讀到了文件結尾,同時文件的file position也會被更新。實際讀到的位元組數可能會比count小。 如果返回-1,表示讀取失敗,errno會被設置為相應的值。   2、系統調用 write ssize_t write(int fd, const void *buf, size_t count); 這個函數將以buf為首地址的緩衝區當中的count個位元組寫到文件描述符fd表示的文件當中,返回值: 返回正整數,表示實際寫入的位元組數,返回0表示沒有任何東西被寫入,同時文件位置指針也會被更新 返回-1,表示寫失敗,同時errno會被設置為相應的值     3、LDD3上對驅動中實現的read回調函數的解釋   原型:    ssize_t (*read) (struct file *fp, char __user *user_buf, size_t count, loff_t *ppos); fp 被打開的節點的文件描述符 user_buf表示的是用戶空間的一段緩衝區的首地址,從kernel讀取的數據需要存放該緩衝區當中 count表示用戶期望讀取的位元組數 *ppos表示當前當前文件位置指針的大小,這個值會需要驅動程式自己來更新,初始大小是0    如果返回值等於傳遞給read系統調用的count參數,則說明所請求的位元組數傳輸成功完成。這是最理想的情況 如果返回值是正的,但是比count小,則說明只有部分數據傳輸成功。這種情況下因設備的不同可能有許多原因。大部分情況下,程式會再次讀數據。例如,如果用fread函數讀數據,這個庫函數就會不斷調用系統調用,直至所請求的數據傳輸完畢為止 如果返回值為0,則表示已經達到了文件尾 負值意味著發生了錯誤,該值指明瞭發生了什麼錯誤,錯誤碼在<linux/errno.h>中定義。比如這樣的一些錯誤:-EINTR(系統調用被中斷)或者-EFAULT(無效地址)   4、LDD3上對驅動中實現的write回調函數的解釋   原型: ssize_t (*write) (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos); fp:被打開的要寫的內核節點的文件描述符 user_buf:表示的是用戶空間的一段緩衝區的首地址,其中存放的是用戶需要傳遞給kernel的數據 count:用戶期望寫給kernel的位元組數 *ppos:文件位置指針,需要驅動程式自己更新   如果返回值等於count,則完成了所請求數目的位元組傳輸 如果返回值為正的,但小於count,則這傳輸了部分數據。程式很可能再次試圖寫入餘下的數據 如果返回值為0,意味著什麼也沒有寫入。這個結果不是錯誤,而且也沒有理由返回一個錯誤碼。再次重申,標準庫會重覆調用write 負值意味著發生了錯誤,與read相同,有效的錯誤碼定義在<linux/errno.h>中   上面加粗的紅色字體引起驅動中的write或者read被反覆調用的原因。  

二、簡略的分析一下read和write系統調用的實現

  在用戶空間調用read函數後,內核函數vfs_read會被調用:
 1 ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
 2 {
 3     ssize_t ret;
 4 
 5     if (!(file->f_mode & FMODE_READ))
 6         return -EBADF;
 7     if (!(file->f_mode & FMODE_CAN_READ))
 8         return -EINVAL;
 9     if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
10         return -EFAULT;
11 
12     ret = rw_verify_area(READ, file, pos, count);
13     if (!ret) {
14         if (count > MAX_RW_COUNT)
15             count =  MAX_RW_COUNT;
16         ret = __vfs_read(file, buf, count, pos);
17         if (ret > 0) {
18             fsnotify_access(file);
19             add_rchar(current, ret);
20         }
21         inc_syscr(current);
22     }
23 
24     return ret;
25 }
下麵是需要關註的: 第9行檢查用戶空間的buf緩衝區是否可以寫入 第14行檢查count的大小,這裡MAX_RW_COUNT被設置為1個頁的大小,這裡的值是4KB,也就是一次用戶一次read最多獲得4KB數據 第16行調用__vfs_read,這個函數最終會調用到我們的驅動中的read函數,可以看到這個函數的參數跟驅動中的read函數一樣,驅動中read返回的數字ret會返回給用戶,這裡並沒有看到更新pos,所以需要在我們的驅動中自己去更新。   用戶空間調用write函數後,內核函數vfs_write會被調用:
 1 ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
 2 {
 3     ssize_t ret;
 4 
 5     if (!(file->f_mode & FMODE_WRITE))
 6         return -EBADF;
 7     if (!(file->f_mode & FMODE_CAN_WRITE))
 8         return -EINVAL;
 9     if (unlikely(!access_ok(VERIFY_READ, buf, count)))
10         return -EFAULT;
11 
12     ret = rw_verify_area(WRITE, file, pos, count);
13     if (!ret) {
14         if (count > MAX_RW_COUNT)
15             count =  MAX_RW_COUNT;
16         file_start_write(file);
17         ret = __vfs_write(file, buf, count, pos);
18         if (ret > 0) {
19             fsnotify_modify(file);
20             add_wchar(current, ret);
21         }
22         inc_syscw(current);
23         file_end_write(file);
24     }
25 
26     return ret;
27 }

這裡需要關註:

第9行,檢查用戶空間的緩衝區buf是否可以讀 第15行,限制一次寫入的數據最多為1頁,比如4KB 第17行的_vfs_write的參數跟驅動中的write的參數一樣,__vfs_write的返回值ret也就是用戶調用write時的返回值,表示實際寫入的位元組數,這裡也沒有看到更新pos的代碼,所以需要我們自己在驅動的write中實現  

三、簡略分析cat和echo的實現

由於使用的根文件系統使用busybox做的,所以cat和echo的實現在busybox的源碼中,如下: coreutils/cat.c coreutils/echo.c   CAT: 下麵簡略分析cat的實現,cat的預設實現採用了sendfile,採用sendfile可以減少不必要的記憶體拷貝,從而提高讀寫效率,這就是所謂的Linux的“零拷貝”。為了便於代碼分析,可以關閉這個功能,然後cat就會調用read和write實現了: Busybox Settings  --->     General Configuration  --->         [ ] Use sendfile system call   下麵是cat的核心函數: 以 cat xxx為例其中src_fd就是被打開的內核節點的文件描述符,dst_fd就是標準輸出描述符,size是0
 1 static off_t bb_full_fd_action(int src_fd, int dst_fd, off_t size)
 2 {
 3     int status = -1;
 4     off_t total = 0;
 5     bool continue_on_write_error = 0;
 6     ssize_t sendfile_sz;
 7     char buffer[4 * 1024];   // 用戶空間緩衝區,4KB大小
 8     enum { buffer_size = sizeof(buffer) };  // 每次read期望獲得的位元組數
 9 
10     sendfile_sz = 0;
11     if (!size) {
12         size =  (16 * 1024 *1024); // 剛開始,如傳入的size是0,這裡將size設置為16MB
13         status = 1; /* 表示一直讀到文件結尾,也就是直到read返回0 */
14     }
15 
16     while (1) {
17         ssize_t rd;
18         
19         rd = safe_read(src_fd, buffer, buffer_size);  // 這裡調用的就是read, 讀取4KB,rd是實際讀到的位元組數
20         if (rd < 0) {
21             bb_perror_msg(bb_msg_read_error);
22             break;
23         }
24  read_ok:
25         if (!rd) { /* 表示讀到了文件結尾,那麼結束迴圈 */
26             status = 0;
27             break;
28         }
29         /* 將讀到的內容輸出到dst_fd表示的文件描述符 */
30         if (dst_fd >= 0 && !sendfile_sz) {
31             ssize_t wr = full_write(dst_fd, buffer, rd);
32             if (wr < rd) {
33                 if (!continue_on_write_error) {
34                     bb_perror_msg(bb_msg_write_error);
35                     break;
36                 }
37                 dst_fd = -1;
38             }
39         }
40         
41         total += rd;  // total記錄的是讀到的位元組數的累計值
42         if (status < 0) { /* 如果傳入的size不為0,那麼status為-1,直到讀到size個位元組後,才會退出。如果size為0,這個條件不會滿足 */
43             size -= rd;
44             if (!size) {
45                 /* 'size' bytes copied - all done */
46                 status = 0;
47                 break;
48             }
49         }
50     }
51  out:
52     return status ? -1 : total;  // 當讀完畢,status為0,這裡返回累計讀到的位元組數
53 }

從上面的分析我們知道如下信息:

使用cat xxx時,上面的函數傳入的size為0,那麼上面的while迴圈會一直進行read,直到出錯或者read返回0,read返回0也就是讀到文件結尾。最後如果出錯,那麼返回-1,否則的話,返回讀到的累計的位元組數。 到這裡,應該就是知道為什麼驅動中的read會被頻繁調用了吧,也就是驅動中的read的返回值有問題。   ECHO: echo的核心函數是full_write 這裡fd是要寫的內核節點,buf緩衝區中存放的是要寫入的內容,len是buf緩衝區中存放的位元組數
 1 ssize_t FAST_FUNC full_write(int fd, const void *buf, size_t len)
 2 {
 3     ssize_t cc;
 4     ssize_t total;
 5 
 6     total = 0;
 7 
 8     while (len) {
 9         cc = safe_write(fd, buf, len);
10 
11         if (cc < 0) {
12             if (total) {
13                 /* we already wrote some! */
14                 /* user can do another write to know the error code */
15                 return total;
16             }
17             return cc;  /* write() returns -1 on failure. */
18         }
19 
20         total += cc;
21         buf = ((const char *)buf) + cc;
22         len -= cc;
23     }
24 
25     return total;
26 }

上面的函數很簡單,可以得到如下信息:

如果write的函數返回值cc小於len的話,會一直調用write,直到報錯或者len個位元組全部寫完。而這裡的cc對應的就是我們的驅動中write的返回值。最後,返回實際寫入的位元組數或者一個錯誤碼。 到這裡,應該也已經清除為什麼調用一次echo後,驅動的write為什麼會被頻繁調用了吧,還是驅動中write的返回值的問題。   知道的上面的原因,下麵我們結合一個簡單的驅動看看。  

四、實例分析

1、先看兩個刷屏的例子

這個驅動在/sys/kernel/debug生成一個demo節點,支持讀和寫。

 1 #include <linux/init.h>
 2 #include <linux/module.h>
 3 #include <linux/debugfs.h>
 4 #include <linux/fs.h>
 5 #include <asm/uaccess.h>
 6 
 7 static struct dentry *demo_dir;
 8 
 9 static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos)
10 {
11     char kbuf[10];
12     int ret, wrinten;
13 
14     printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
15         user_buf, count, *ppos);
16 
17     wrinten = snprintf(kbuf, 10, "%s", "Hello");
18 
19     ret = copy_to_user(user_buf, kbuf, wrinten+1);
20     if (ret != 0) {
21         printk(KERN_ERR "read error");
22         return -EIO;
23     }
24 
25     *ppos += wrinten;
26 
27     return wrinten;
28 }
29 
30 static ssize_t demo_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos)
31 {
32     char kbuf[10] = {0};
33     int ret;
34 
35     printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
36            user_buf, count, *ppos);
37 
38     ret = copy_from_user(kbuf, user_buf, count);
39     if (ret) {
40         pr_err("%s: write error\n", __func__);
41         return -EIO;
42     }
43 
44     *ppos += count;
45 
46     return 0;
47 }
48 
49 static const struct file_operations demo_fops = {
50     .read = demo_read,
51     .write = demo_write,
52 };
53 
54 static int __init debugfs_demo_init(void)
55 {
56     int ret = 0;
57 
58     demo_dir = debugfs_create_file("demo", 0444, NULL,
59         NULL, &demo_fops);
60 
61     return ret;
62 }
63 
64 static void __exit debugfs_demo_exit(void)
65 {
66     if (demo_dir)
67         debugfs_remove(demo_dir);
68 }
69 
70 module_init(debugfs_demo_init);
71 module_exit(debugfs_demo_exit);
72 MODULE_LICENSE("GPL");

我們先來看看運行結果:

先試試寫: [root@vexpress mnt]# echo 1 > /d/demo 執行這個命令並不會返回,會卡主,再看看kernel log,已經刷屏: [ 1021.547015] user_buf: 00202268, count: 2, ppos: 0 [ 1021.547181] user_buf: 00202268, count: 2, ppos: 2 [ 1021.547319] user_buf: 00202268, count: 2, ppos: 4 [ 1021.547466] user_buf: 00202268, count: 2, ppos: 6 .... .... [ 1022.008736] user_buf: 00202268, count: 2, ppos: 6014 [ 1022.008880] user_buf: 00202268, count: 2, ppos: 6016 [ 1022.009012] user_buf: 00202268, count: 2, ppos: 6018 ... ...   再試試讀: [root@vexpress mnt]# cat /d/demo HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello... ... 可以看到,終端被Hello填滿了,再看看kernel log,刷屏了: [ 1832.074616] user_buf: becb6be8, count: 4096, ppos: 0 [ 1832.075033] user_buf: becb6be8, count: 4096, ppos: 5 [ 1832.075240] user_buf: becb6be8, count: 4096, ppos: 10 [ 1832.075898] user_buf: becb6be8, count: 4096, ppos: 15 [ 1832.076093] user_buf: becb6be8, count: 4096, ppos: 20 [ 1832.076282] user_buf: becb6be8, count: 4096, ppos: 25 [ 1832.076468] user_buf: becb6be8, count: 4096, ppos: 30 [ 1832.076653] user_buf: becb6be8, count: 4096, ppos: 35 [ 1832.076841] user_buf: becb6be8, count: 4096, ppos: 40 ... ...   可以看到規律,對於write,每次的count都是2,因為寫下來的是個字元串的"1",ppos以2為臺階遞增。此外,可以看到user_buf每次都相同,結合echo源碼可以發現,用戶的user_buf是在堆上分配的,所以地址比較小 對於read,每次要讀的count都是4KB,ppos是以5為臺階遞增,正好是strlen("Hello"),user_buf的值每次都相同,結合cat源碼可以發現,用戶的user_buf是在棧上分配的,所以地址比較大 下圖是x86系統下Linux進程的進程地址空間的記憶體佈局,這是只是說明一下意思。     下麵開始分別針對write和read進行修改:  

2、對write進行修改

write版本2: 既然經過前面的分析,知道write被頻繁調用的原因是用戶調用write實際寫入的位元組數小於期望的,而用戶的write的返回值來自驅動的write,那麼我們直接然write返回count不就可以了嗎。
 1 static ssize_t demo_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos)
 2 {
 3     char kbuf[10] = {0};
 4     int ret;
 5 
 6     printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
 7           user_buf, count, *ppos);
 8 
 9     ret = copy_from_user(kbuf, user_buf, count);
10     if (ret) {
11         pr_err("%s: write error\n", __func__);
12         return -EIO;
13     }
14 
15     *ppos += count;
16 
17     return count;
18 }

驗證:

[root@vexpress mnt]# echo 1 > /d/demo 敲完回車後,立馬就返回了,kernel log也只列印了一次: [ 2444.363351] user_buf: 00202408, count: 2, ppos: 0   write版本3: 其實,kernel提供了一個很方便的函數,simple_write_to_buffer,這個函數專門完成從user空間向kernel空間拷貝數據:
1 static ssize_t demo_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos)
2 {
3     char kbuf[10] = {0};
4 
5     printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
6            user_buf, count, *ppos);
7 
8     return simple_write_to_buffer(kbuf, sizeof(kbuf), ppos, user_buf, count);
9 }
驗證: [root@vexpress mnt]# echo 1 > /d/demo 敲完回車後,立馬就返回了,kernel log也只列印了一次: [ 2739.984844] user_buf: 00202340, count: 2, ppos: 0   簡單看看simple_write_to_buffer的實現:
 1 /**
 2  * simple_write_to_buffer - copy data from user space to the buffer
 3  * @to: the buffer to write to
 4  * @available: the size of the buffer
 5  * @ppos: the current position in the buffer
 6  * @from: the user space buffer to read from
 7  * @count: the maximum number of bytes to read
 8  *
 9  * The simple_write_to_buffer() function reads up to @count bytes from the user
10  * space address starting at @from into the buffer @to at offset @ppos.
11  *
12  * On success, the number of bytes written is returned and the offset @ppos is
13  * advanced by this number, or negative value is returned on error.
14  **/
15 ssize_t simple_write_to_buffer(void *to, size_t available, loff_t *ppos,
16         const void __user *from, size_t count)
17 {
18     loff_t pos = *ppos;
19     size_t res;
20 
21     if (pos < 0)
22         return -EINVAL;
23     if (pos >= available || !count)
24         return 0;
25     if (count > available - pos)
26         count = available - pos;
27     res = copy_from_user(to + pos, from, count);
28     if (res == count)
29         return -EFAULT;
30     count -= res;
31     *ppos = pos + count;
32     return count;
33 }
34 EXPORT_SYMBOL(simple_write_to_buffer);

可以看到,最後返回的是count,如果copy_from_user沒都拷貝全,將來write還是會被再次調用。

 

3、對read進行修改

我們知道read被返回調用的原因是,read返回的值小於用戶期望讀取的值,對於這裡,就是4KB。而對於cat來說,每次read都期望獲取4KB的數據,而且在不考慮出錯的情況下,只有read返回0,cat才會終止。   read版本2:
 1 static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos)
 2 {
 3     char kbuf[10];
 4     int ret, wrinten;
 5 
 6     printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
 7         user_buf, count, *ppos);
 8 
 9     wrinten = snprintf(kbuf, 10, "%s", "Hello");
10 
11     ret = copy_to_user(user_buf, kbuf, wrinten+1);
12     if (ret != 0) {
13         printk(KERN_ERR "read error");
14         return -EIO;
15     }
16 
17     *ppos += wrinten;
18 
19     return 0;
20 }

驗證: [root@vexpress mnt]# cat /d/demo 執行回車後,"Hello"卻沒有輸出,但是驅動的read驅動被調用了一次: [  118.837456] user_buf: beeb0be8, count: 4096, ppos: 0 這是什麼原因呢?可以看看cat的核心函數bb_full_fd_action,其中,如果read返回0,並不會將讀到的內容輸出到標準輸出上,所以cat的時候什麼都沒看到。   既然返回0不行,那麼返回count,也就是用戶期望的4KB,行不行呢? read版本3:
 1 static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos)
 2 {
 3     char kbuf[10];
 4     int ret, wrinten;
 5 
 6     printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
 7         user_buf, count, *ppos);
 8 
 9     wrinten = snprintf(kbuf, 10, "%s", "Hello");
10 
11     ret = copy_to_user(user_buf, kbuf, wrinten+1);
12     if (ret != 0) {
13         printk(KERN_ERR "read error");
14         return -EIO;
15 	   

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

-Advertisement-
Play Games
更多相關文章
  • 1.父類必須包含構造函數麽? 父類必須要有一個構造函數,有參無參都可以。 構造函數是對象的基本,沒有構造函數就沒有對象,若父類中顯示的有參數的構造函數,在子類繼承就必須寫一個構造函數來調用父類的構造函數。 如果父類中有沒有參數的構造函數,在子類中可以不顯示的寫父類的構造函數,系統會自動調用沒有參數的 ...
  • 前段時間,Insus.NET有實現一組字元串在輸出時,靠左或靠右對齊。《輸出的字元靠右對齊》http://www.cnblogs.com/insus/p/7953304.html 現在Insus.NET參考此方法,實一張塔松葉,實現之前,先練習一下,輸出半張: public void WriteTr ...
  • 當Web Api 2.0使用OAuth2授權時,如何在Swagger中添加Authorization請求頭? Swagger說明文檔支持手動調用Api, 但是當Api使用OAuth2授權時,由於沒有地方可以輸入授權Token, 導致響應結果一直是401沒有授權。 解決方案: 在Swagger配置文件 ...
  • Ticks是一個周期,存儲的是一百納秒,換算為秒,一千萬分之一秒。我們需要計算2個時間之間,經過多少Ticks,可以使用下麵的方法來實現,使用2個時間相減。得到結果為正數,是使用較晚的時間減去較早的時間。反之為負數,即是使用較早的時間減去較晚的時間。創建一個對象: class Ag { privat ...
  • 一.TcpClient與TcpServe。 首先我們需要知道伺服器的IP地址,在伺服器端建立監聽,當監聽到客戶端的連接請求後,連接到客戶端。 而客戶端則需要連接到指定的IP伺服器地址,建立網路流,則可以實現通信。 接下來給出一個伺服器端與客戶端的實例: 伺服器端: 此時伺服器端應用的是Socket類 ...
  • 實例01 實現一個簡單的Web服務訪問 本實例將實現IP地址查詢介面服務,根據用戶傳入的IP地址返回IP所在的省、市、地區,實例中將會用到IP地址庫用於查詢信息,由於數據較多,所以讀者可在光碟資源文件中直接附加資料庫文件,這裡將不再介紹導入數據的過程。 程式實現步驟如下: (1)打開Visual S ...
  • 常用集合介面系列:http://www.cnblogs.com/fengxiaojiu/p/7997704.html 常用集合類系列:http://www.cnblogs.com/fengxiaojiu/p/7997541.html 常用集合類: 數組(Array)的不足(即:集合與數組的區別) 1 ...
  • Elastic 的底層是開源庫 Lucene。但是,你沒法直接用 Lucene,必須自己寫代碼去調用它的介面。Elastic 是 Lucene 的封裝,提供了 REST API 的操作介面,開箱即用。Elastic 的底層是開源庫 。但是,你沒法直接用 Lucene,必須自己寫代碼去調用它的介面。E ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...