C 基於UDP實現一個簡易的聊天室

来源:http://www.cnblogs.com/life2refuel/archive/2016/04/06/5358072.html
-Advertisement-
Play Games

引言 本文是圍繞Linux udp api 構建一個簡易的多人聊天室.重點看思路,幫助我們加深 對udp開發中一些api瞭解.相對而言udp socket開發相比tcp socket開發註意的細節要少很多. 但是水也很深. 本文就當是一個demo整合幫助開發者回顧和繼續瞭解 linux udp開發的 ...


引言

  本文是圍繞Linux udp api 構建一個簡易的多人聊天室.重點看思路,幫助我們加深

對udp開發中一些api瞭解.相對而言udp socket開發相比tcp socket開發註意的細節要少很多.

但是水也很深. 本文就當是一個demo整合幫助開發者回顧和繼續瞭解 linux udp開發的基本流程.

首先我們來看看 linux udp 和 tcp的異同.

/*
這裡簡單比較一下TCP和UDP在編程實現上的一些區別:

TCP流程
     建立一個TCP連接需要三次握手,而斷開一個TCP則需要四個分節。當某個應用進程調用close(主動端)後(可以是伺服器端,也可以是客戶 
端),這一端的TCP發送一個FIN,表示數據發送完畢;另一端(被動端)發送一個確認,當被動端待處理的應用進程都處理完畢後,發送一個FIN到主動 
端,並關閉套介面,主動端接收到這個FIN後再發送一個確認,到此為止這個TCP連接被斷開。 

UDP套介面 
  UDP套介面是無連接的、不可靠的數據報協議;既然他不可靠為什麼還要用呢?
  其一:當應用程式使用廣播或多播是只能使用UDP協議;
  其二:由於它是無連接的,所以速度快。因為UDP套介面是無連接的,如果一方的數據報丟失,那另一方將無限等待,解決辦法是設置一個超時。      在編寫UDP套介面程式時,有幾點要註意:建立套介面時socket函數的第二個參數應該是SOCK_DGRAM,說明是建立一個UDP套介面;
  由於UDP是無連接的,所以伺服器端並不需要listen或accept函數;
  當UDP套介面調用connect函數時,內核只記錄連接放的IP地址 和埠,並立即返回給調用進程.
*/

 參照

    linux udp api簡介   http://blog.csdn.net/wocjj/article/details/8315559

     tcp 和udp區別    http://www.cnblogs.com/Jessy/p/3536163.html

這裡簡單引述一下 udp相比tcp 用到的兩個api .  recvfrom()/sendto() 具體細節如下

#include <sys/types.h>  
#include <sys/socket.h> 

/*
 * 這兩個函數基本等同於 一個 send 和 recv . 詳細參數解釋如下
 * s        : 文件描述符,等同於 socket返回的值
 * buf        : 數據其實地址
 * len        : 發送數據長度或接受數據緩衝區最大長度
 * flags    : 發送標識,預設就用O.帶外數據使用 MSG_OOB, 偷窺用MSG_PEEK .....
 * addr     : 發送的網路地址或接收的網路地址
 * alen     : sento標識地址長度做輸入參數, recvfrom表示輸入和輸出參數.可以為NULL此時addr也要為NULL
 *        : 返回0表示執行成功,否則返回<0 . 更多細節查詢man手冊
 */
extern int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *addr, int alen);  
extern int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *addr, int *alen); 

上面就是兩個函數的大致用法. 具體可以查看linux api幫助手冊. 最好就用 man sendto / man recvfrom 把那一系列函數都看看.

現在很多文章都是轉載,但是找不見轉載的地址, 下麵會舉一個簡易的UDP回顯伺服器的demo加深理解.

 

前言

  首先看設計圖

有點low. 簡單看看吧. 那我們先看 客戶端代碼  udpclt.c 代碼

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>

// 測試埠和網路地址
#define _INT_PORT (8088)
#define _INT_BUF 1024

// udp 伺服器主函數
int main(int argc, char* argv[]) {
    int sd, len;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    char msg[_INT_BUF];    
    
    //創建伺服器socket 地址,客戶端給它發送信息
    if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("main socket ");
        exit(sd);
    }
    // 這裡簡單輸出連接信息
    printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT);    

    //拼接對方地址
    addr.sin_port = htons(_INT_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){
        perror("main bind ");
        exit(-1);
    }
    
    // 迴圈處理消息讀取發送到客戶端
    while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){
        msg[len] = '\0';
        printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port),  msg);
        //這裡發送信息過去, 也可以事先connect這裡就不綁定了
        sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen);
    }    

    close(sd);
    puts("udp server end ------------------------------<");

    return 0;
}

編譯是

gcc -g -Wall -o udpclt.out udpclt.c

udp 伺服器 udpsrv.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>

// 測試埠和網路地址
#define _INT_PORT (8088)
#define _INT_BUF 1024

// udp 伺服器主函數
int main(int argc, char* argv[]) {
    int sd, len;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    char msg[_INT_BUF];    
    
    //創建伺服器socket 地址,客戶端給它發送信息
    if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("main socket ");
        exit(sd);
    }
    // 這裡簡單輸出連接信息
    printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT);    

    //拼接對方地址
    addr.sin_port = htons(_INT_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){
        perror("main bind ");
        exit(-1);
    }
    
    // 迴圈處理消息讀取發送到客戶端
    while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){
        msg[len] = '\0';
        printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port),  msg);
        //這裡發送信息過去, 也可以事先connect這裡就不綁定了
        sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen);
    }    

    close(sd);
    puts("udp server end ------------------------------<");

    return 0;
}

編譯是

gcc -g -Wall -o udpsrv.out udpsrv.c

後面運行結果如下 udp伺服器如下 (Ctrl + C 退出)

udp 客戶端如下 (Ctrl + D 結束輸入)

到這裡將上面代碼 敲一遍基本上udp 一套api就會使用了. 後面進入正題設計聊天室代碼.

 

正文

  首先看客戶端設計代碼. 主要思路是子進程處理數據的輸出, 父進程處理伺服器數據的接收. 具體設計如下(畫的圖有點low就不畫了.../(ㄒoㄒ)/~~)

udpmulclt.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// 名字長度包含'\0'
#define _INT_NAME (64)
// 報文最大長度,包含'\0'
#define _INT_TEXT (512)

//4.0 控制台列印錯誤信息, fmt必須是雙引號括起來的巨集
#define CERR(fmt, ...) \
    fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
         __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)

//4.1 控制台列印錯誤信息並退出, t同樣fmt必須是 ""括起來的字元串常量
#define CERR_EXIT(fmt,...) \
    CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
 * 簡單的Linux上API錯誤判斷檢測巨集, 好用值得使用
 */
#define IF_CHECK(code) \
    if((code) < 0) \
        CERR_EXIT(#code)

// 發送和接收的信息體
struct umsg{
    char type;                //協議 '1' => 向伺服器發送名字, '2' => 向伺服器發送信息, '3' => 向伺服器發送退出信息
    char name[_INT_NAME];    //保存用戶名字
    char text[_INT_TEXT];    //得到文本信息,空間換時間
};

/*
 * udp聊天室的客戶端, 子進程發送信息,父進程接受信息
 */
int main(int argc, char* argv[]) {
    int sd, rt;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    pid_t pid;
    struct umsg msg = { '1' };    

    // 這裡簡單檢測
    if(argc != 4) {
        fprintf(stderr, "uage : %s [ip] [port] [name]\n", argv[0]);
        exit(-1);
    }    
    // 下麵對接數據
    if((rt = atoi(argv[2]))<1024 || rt > 65535)
        CERR("atoi port = %s is error!", argv[2]);
    // 接著判斷ip數據
    IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
    addr.sin_port = htons(rt);
    // 這裡拼接用戶名字
    strncpy(msg.name, argv[3], _INT_NAME - 1);
    
    //創建socket 連接
    IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
    // 這裡就是發送登錄信息給udp聊天伺服器了
    IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));    
    
    //開啟一個進程, 子進程處理髮送信息, 父進程接收信息
    IF_CHECK(pid = fork());
    if(pid == 0) { //子進程,先忽略退出處理防止成為僵屍進程
        signal(SIGCHLD, SIG_IGN);                
        while(fgets(msg.text, _INT_TEXT, stdin)){
            if(strcasecmp(msg.text, "quit\n") == 0){ //表示退出
                msg.type = '3';
                // 發送數據並檢測
                IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
                break;
            }
            // 洗嘜按發送普通信息
            msg.type = '2';
            IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
        }
        // 處理結算操作,並殺死父進程
        close(sd);
        kill(getppid(), SIGKILL);
        exit(0);
    }
    // 這裡是父進程處理數據的讀取
    for(;;){
        bzero(&msg, sizeof msg);
        IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
        msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '\0';
        switch(msg.type){
        case '1':printf("%s 登錄了聊天室!\n", msg.name);break;
        case '2':printf("%s 說了: %s\n", msg.name, msg.text);break;
        case '3':printf("%s 退出了聊天室!\n", msg.name);break;
        default://未識別的異常報文,程式直接退出
            fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
                    ntohs(addr.sin_port), msg.type, msg.name, msg.text);
            goto __exit;
        }
    }    

__exit:    
    // 殺死並等待子進程退出
    close(sd);
    kill(pid, SIGKILL);
    waitpid(pid, NULL, -1);    

    return 0;
}

這裡主要需要註意的是

// 發送和接收的信息體
struct umsg{
    char type;                //協議 '1' => 向伺服器發送名字, '2' => 向伺服器發送信息, '3' => 向伺服器發送退出信息
    char name[_INT_NAME];    //保存用戶名字
    char text[_INT_TEXT];    //得到文本信息,空間換時間
};

傳輸和接收的數據格式, type表示協議或行為. 我這裡細心了處理 name, text最後一個字元必須是 '\0'. 其它都是業務代碼.再扯一點

struct sockaddr_in addr = { AF_INET };

等價於

struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;

也是一個C開發中技巧吧. 再扯一點linux上提供 bzero函數, 但是window上沒有. 寫了個通用的如下

//7.0 置空操作
#ifndef BZERO
//v必須是個變數
#define BZERO(v) \
    memset(&v,0,sizeof(v))
#endif/* !BZERO */    

可以試試吧畢竟跨平臺....

好了那我們說 udp 聊天室的伺服器設計思路. 就是伺服器會維護一個客戶端鏈表. 有信息來就廣播. 好簡單吧.就是這樣.正常的事都簡單.

簡單的是美的. 好了看代碼總設計和實現. udpmulsrv.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// 名字長度包含'\0'
#define _INT_NAME (64)
// 報文最大長度,包含'\0'
#define _INT_TEXT (512)

//4.0 控制台列印錯誤信息, fmt必須是雙引號括起來的巨集
#define CERR(fmt, ...) \
    fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
         __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)

//4.1 控制台列印錯誤信息並退出, t同樣fmt必須是 ""括起來的字元串常量
#define CERR_EXIT(fmt,...) \
    CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
 * 簡單的Linux上API錯誤判斷檢測巨集, 好用值得使用
 */
#define IF_CHECK(code) \
    if((code) < 0) \
        CERR_EXIT(#code)

// 發送和接收的信息體
struct umsg{
    char type;                //協議 '1' => 向伺服器發送名字, '2' => 向伺服器發送信息, '3' => 向伺服器發送退出信息
    char name[_INT_NAME];    //保存用戶名字
    char text[_INT_TEXT];    //得到文本信息,空間換時間
};

// 維護一個客戶端鏈表信息,記錄登錄信息
typedef struct ucnode {
    struct sockaddr_in addr;
    struct ucnode* next;
} *ucnode_t ;


// 新建一個結點對象
static inline ucnode_t _new_ucnode(struct sockaddr_in* pa){
    ucnode_t node = calloc(sizeof(struct ucnode), 1);    
    if(NULL == node)
        CERR_EXIT("calloc sizeof struct ucnode is error. ");
    node->addr = *pa;
    return node;
}

// 插入數據,這裡head預設頭結點是當前伺服器結點
static inline void _insert_ucnode(ucnode_t head, struct sockaddr_in* pa) {
    ucnode_t node = _new_ucnode(pa);
    node->next = head->next;
    head->next = node;    
}

// 這裡是有用戶登錄處理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    _insert_ucnode(head, pa);
    head = head->next;
    // 從此之後才為以前的鏈表
    while(head->next){
        head = head->next;
        IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
    }
}

// 信息廣播
static void _broadcast_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    int flag = 0; //1表示已經找到了
    while(head->next) {
        head = head->next;
        if((flag) || !(flag=memcmp(pa, &head->addr, sizeof(struct sockaddr_in))==0)){
            IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
        }
    }
}

// 有人退出群聊
static void _quit_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    int flag = 0;//1表示已經找到
    while(head->next) {
        if((flag) || !(flag = memcmp(pa, &head->next->addr, sizeof(struct sockaddr_in))==0)){
            IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->next->addr, sizeof(struct sockaddr_in)));
            head = head->next;
        }        
        else { //刪除這個退出的用戶
            ucnode_t tmp = head->next;
            head->next = tmp->next;
            free(tmp);
        }
    }        
}

// 銷毀維護的對象池,沒有往複雜的考慮了簡單處理退出了
static void _destroy_ucnode(ucnode_t* phead) {
    ucnode_t head;
    if((!phead) || !(head=*phead)) return;    
    while(head){
        ucnode_t tmp = head->next;
        free(head);
        head = tmp;
    }    

    *phead = NULL;
}

/*
 * udp聊天室的伺服器, 子進程廣播信息,父進程接受信息
 */
int main(int argc, char* argv[]) {
    int sd, rt;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    struct umsg msg;    
    ucnode_t head;

    // 這裡簡單檢測
    if(argc != 3) {
        fprintf(stderr, "uage : %s [ip] [port]\n", argv[0]);
        exit(-1);
    }    
    // 下麵對接數據
    if((rt = atoi(argv[2]))<1024 || rt > 65535)
        CERR("atoi port = %s is error!", argv[2]);
    // 接著判斷ip數據
    IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
    addr.sin_port = htons(rt); //埠要採用網路位元組序
    // 創建socket
    IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
    // 這裡bind綁定設置的地址
    IF_CHECK(bind(sd, (struct sockaddr*)&addr, alen));
    
    //開始監聽了
    head = _new_ucnode(&addr);    
    for(;;){
        bzero(&msg, sizeof msg);
        IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
        msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '\0';
        fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
                    ntohs(addr.sin_port), msg.type, msg.name, msg.text);
        // 開始判斷處理
        switch(msg.type) {
        case '1':_login_ucnode(head, sd, &addr, &msg);break;
        case '2':_broadcast_ucnode(head, sd, &addr, &msg);break;
        case '3':_quit_ucnode(head, sd, &addr, &msg);break;
        default://未識別的異常報文,程式把其踢走
            fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
                    ntohs(addr.sin_port), msg.type, msg.name, msg.text);
            _quit_ucnode(head, sd, &addr, &msg);
            break;
        }        
    }
        
    // 這段代碼是不會執行到這的, 可以加一些控制讓其走到這. 看人
    close(sd);
    _destroy_ucnode(&head);    
    return 0;
}

這裡主要圍繞的結構就是

// 維護一個客戶端鏈表信息,記錄登錄信息
typedef struct ucnode {
    struct sockaddr_in addr;
    struct ucnode* next;
} *ucnode_t ;

註冊添加登錄廣播退出等.這裡再扯一下. 關於C static開發技巧. C中有一種 *.h 開發模式, 全部採用static 內嵌代碼段. 這樣

可以省略*.c 文件. 小巧的封裝可以使用. 繼續扯一點. 開發也寫C++,雖然鄙視. C++ 中有個 *.hpp文件. 比較好. 它表達的意思

是這個代碼是開源的. 全部採用充血模型. 類中代碼都放在類中實現.非常值得提倡. 這也是學boost的時候學到的. 很實在.

好了說代碼吧. 也比較隨大流. 看看也都明白了. 簡單分析一處吧

// 這裡是有用戶登錄處理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    _insert_ucnode(head, pa);
    head = head->next;
    // 從此之後才為以前的鏈表
    while(head->next){
        head = head->next;
        IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
    }
}

因為我採用的頭查法. 那就除了剛插入的頭的下一個結點都需要發送登錄信息. 比較精巧.

好看編譯命令

gcc -g -Wall -o udpmulsrv.out udpmulsrv.c
gcc -g -Wall -o udpmulclt.out udpmulclt.c

最後測試截圖如下

很好玩,歡迎嘗試.到這裡基本上udp基礎api 應該都瞭解了.從上面代碼也許能看出來. 設計比較重要. 設計決定大思路.

下次有機會 要麼分享開源的網路庫,要麼分享資料庫開發.

 

後記

   錯誤是難免的,歡迎吐槽交流. ( ^_^ )/~~拜拜

      別董大(其一)         高適     千里黃雲白日曛,     北風吹雁雪紛紛。     莫愁前路無知己,     天下誰人不識君。   (作者註: 別董大意思是 分別了我的朋友董大)

   


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

-Advertisement-
Play Games
更多相關文章
  • 根據導師的要求,這次的任務是要模擬一個類似BT網路的P2P網路,並實現一些演算法,查了些資料,都說NS2對於P2P網路的模擬不好,基本沒有模本可以用,而且效率很低,只能模擬幾萬個節點左右,看到挺多人推薦使用peersim,就準備下來用用看。而且在官網上看到已經有很多別人編譯好的覆蓋網路的模本,其中也包 ...
  • 外鍵 創建: 方式1:在創建表時使用foreign key(欄位名) references 表名(主鍵); 方式2:通過修改表結構add foreign key(欄位名) references 表名(主鍵); 刪除: alter table 表名 drop foreign key 外鍵名; 如果查看 ...
  • 歷屆試題 蘭頓螞蟻 時間限制:1.0s 記憶體限制:256.0MB 時間限制:1.0s 記憶體限制:256.0MB 問題描述 蘭頓螞蟻,是於1986年,由克裡斯·蘭頓提出來的,屬於細胞自動機的一種。 平面上的正方形格子被填上黑色或白色。在其中一格正方形內有一隻“螞蟻”。 螞蟻的頭部朝向為:上下左右其中一 ...
  • HashMap 的性能因數 1. 容量:表示桶位的數量。 2. 初始容量: 表在創建是所擁有的桶位數。 如果你知道將要在HashMap存儲多少項,創建一個初始容量合適的HashMap將可以避免自動再散列的開銷 /** * The default initial capacity - MUST be ... ...
  • 說是解決,其實不是很完美的解決的,寫出來只是想記錄一下這個問題或者看一下有沒有哪位仁兄會的,能否知道一二。 下麵說說出現問題: 問題是這樣的,當我查詢一個一對多的實體的時候,工具直接就爆了,差不多我就猜到是哪裡死迴圈了,最後等了好久,查看原因,果然是堆溢出,再然後是jsckson的錯誤。那麼必然是序 ...
  • 最近加入了python部落,感覺裡面的刷題寶很有意思,玩了一下,知道了許多以前並不清楚的內置函數,然後感覺到快要記不住了,所以開始陳列一下 1.divmod(a,b):取a除以b的商和餘數,功效等價於(a//b, a%b); 2.dir():參數為函數名,類名。它會告訴我們對應函數包含有什麼參數 3 ...
  • 前兩天學習了兩種java操作excel的方法,對於網站需要創建、修改、導出excel文件,可以使用這兩種方法,第一種方法是使用JExcel,第二種方法是使用poi中的HSSF,前一種方法比較簡單。這裡做個總結。 1.JExcel,需要的jar包jxl.jar 只讀文件 Workbook wb=Wor ...
  • Spring 框架提供了構建 Web 應用程式的全功能 MVC 模塊。Spring MVC屬於SpringFrameWork的後續產品,已經融合在Spring Web Flow裡面。Spring MVC的易用性、功能強大等優點已經被越來越多的企業所接受,也成為一個使用廣泛的mvc框架。因此,尚學堂對 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...