C 實現一個簡易的Http伺服器

来源:http://www.cnblogs.com/life2refuel/archive/2016/03/14/5277111.html
-Advertisement-
Play Games

這是個Linux上簡易的http伺服器實現,主要就是tcp socket 和 pipe管道cgi重定向. 解析http協議. 簡單的處理並反饋給客戶端.並附帶一個client.c 測試服務端. 比較簡陋 去掉註釋400行以下. 能夠幫我們加深web伺服器的理解.


引言

  做一個老實人挺好的,至少還覺得自己挺老實的.

再分享一首 自己喜歡的詩人的一首 情景詩. 每個人總會有問題,至少喜歡就好,

 

本文 參照

  http 協議   http://www.cnblogs.com/rayray/p/3729533.html

  html格式   http://blog.csdn.net/allenjy123/article/details/7375029

  tinyhttpd 源碼     https://github.com/EZLippi/Tinyhttpd 

附錄 本文最後完稿的資源

  httpd 源碼打包  http://download.csdn.net/detail/wangzhione/9461441

通過本文練習, 至少會學會 Linux上fork用法, pipe管道用法0讀1寫, pthread用法等.

其它的都是業務解析內容. 

 

前言

   講的不好望見諒, 因為很多東西需要自己去寫一寫就有感悟了. 看懂源碼和會改源碼是兩碼事. 和 會優化更不同了.

凡事多練習. 不懂也都懂了. 我們先說一下總的結構.

  

client.c 是一個簡易的 測試 http請求的客戶端

httpd.c 使我們重點要說的 小型簡易的Linux上的http伺服器

index.html 測試網頁 是client.c 想請求的網頁

Makefile 編譯文件.

這裡先總的展示一下 httpd.c

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

// --------------------------------------- 輔助參數巨集 ----------------------------------------------
/*
 * c 如果是空白字元返回 true, 否則返回false
 * c : 必須是 int 值,最好是 char 範圍
 */
#define sh_isspace(c) \
    ((c==' ')||(c>='\t'&&c<='\r'))
    
//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)

//4.3 if 的 代碼檢測
#define IF_CHECK(code)    \
    if((code) < 0) \
        CERR_EXIT(#code)
        
// --------------------------------------- 輔助變數巨集 和 聲明 ------------------------------------------

// char[]緩衝區大小
#define _INT_BUF (1024)
// listen監聽隊列的大小
#define _INT_LIS (7)

/*
 * 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度 
 * fd        : 文件描述符
 * buf        : 保存的內容
 * sz        : buf 的大小
 *            : 返回讀取的長度
 */
int getfdline(int fd, char buf[], int sz);

// 返回400 請求解析失敗,客戶端代碼錯誤
extern inline void response_400(int cfd);

// 返回404 文件內容, 請求文件沒有找見
extern inline void response_404(int cfd);

// 返回501 錯誤, 不支持的請求
extern inline void response_501(int cfd);

// 伺服器內部錯誤,無法處理等
extern inline void response_500(int cfd);

// 返回200 請求成功 內容, 後面可以加上其它參數,處理文件輸出
extern inline void response_200(int cfd);

/*
 * 將文件 發送給客戶端
 * cfd        : 客戶端文件描述符
 * path        : 發送的文件路徑
 */
void response_file(int cfd, const char* path);

/*
 * 返回啟動的伺服器描述符(句柄), 這裡沒有採用8080埠,防止衝突,用了隨機埠
 * pport     : 輸出參數和輸出參數, 如果傳入NULL,將不返回自動分配的埠
 *             : 返回 啟動的文件描述符
 */
int serstart(uint16_t* pport);

/*
 * 在客戶端鏈接過來,多線程處理的函數
 * arg        : 傳入的參數, 客戶端文件描述符 (int)arg
 *             : 返回處理結果,這裡預設返回 NULL
 */
void* request_accept(void* arg);

/*
 * 處理客戶端的http請求.
 * cfd        : 客戶端文件描述符
 * path        : 請求的文件路徑
 * type        : 請求類型,預設是POST,其它是GET
 * query    : 請求發送的過來的數據, url ? 後面那些數據
 */
void request_cgi(int cfd, const char* path, const char* type, const char* query);

/*
 * 主邏輯,啟動服務,可以做成守護進程.
 * 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務
 */
int main(int argc, char* argv[])
{
    pthread_attr_t attr;
    uint16_t port = 0;
    int sfd = serstart(&port);
    
    printf("httpd running on port %u.\n", port);
    // 初始化線程屬性
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    for(;;){
        pthread_t tid;
        struct sockaddr_in caddr;
        socklen_t clen = sizeof caddr;
        int cfd = accept(sfd, (struct sockaddr*)&caddr, &clen);
        if(cfd < 0){
            CERR("accept sfd = %d is error!", sfd);
            break;
        }
        if(pthread_create(&tid, &attr, request_accept, (void*)cfd) < 0)
            CERR("pthread_create run is error!");
    }
    // 銷毀吧, 一切都結束了
    pthread_attr_destroy(&attr);
    close(sfd);
    return 0;
}

// ----------------------------------------- 具體的函數實現過程 ------------------------------------------------

/*
 * 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度 
 * fd        : 文件描述符
 * buf        : 保存的內容
 * sz        : buf 的大小
 *            : 返回讀取的長度
 */
int 
getfdline(int fd, char buf[], int sz)
{
    char* tp = buf;
    char c;
    
    --sz;
    while((tp-buf)<sz){
        if(read(fd, &c, 1) <= 0) //偽造結束條件
            break;
        if(c == '\r'){ //全部以\r分割
            if(recv(fd, &c, 1, MSG_PEEK)>0 && c == '\n')
                read(fd, &c, 1);
            else //意外的結束,填充 \n 結束讀取
                *tp++ = '\n';
            break;
        }
        *tp++ = c;
    }
    *tp = '\0';
    return tp - buf;
}

// 返回400 請求解析失敗,客戶端代碼錯誤
inline void 
response_400(int cfd)
{
    const char* estr = "HTTP/1.0 400 BAD REQUEST\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<p>你的請求有問題,請檢查語法!</p>\r\n";
    
    write(cfd, estr, strlen(estr));
}

// 返回404 文件內容, 請求文件沒有找見
inline void 
response_404(int cfd)
{
    const char* estr = "HTTP/1.0 404 NOT FOUND\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<html>"
    "<head><title>你請求的界面被查水錶了!</title></head>\r\n"
    "<body><p>404: 估計是回不來了</p></body>"
    "</html>";
    
    //開始發送數據
    write(cfd, estr, strlen(estr));
}

// 返回501 錯誤, 請求解析失敗,不支持的請求
inline void 
response_501(int cfd)
{
    const char* estr = "HTTP/1.0 501 Method Not Implemented\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<html>"
    "<head><title>小伙子不要亂請求</title></head>\r\n"
    "<body><p>too young too simple, 年輕人別總想弄出個大新聞.</p></body>"
    "</html>";
    
    //這裡還有一個好的做法是將這些內容定義在文件中輸出文件
    write(cfd, estr, strlen(estr));
}


// 伺服器內部錯誤,無法處理等
inline void 
response_500(int cfd)
{
    const char* estr = "HTTP/1.0 500 Internal Server Error\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<html>"
    "<head><title>Sorry </title></head>\r\n"
    "<body><p>最近有點方了!</p></body>"
    "</html>";
    
    write(cfd, estr, strlen(estr));
}

// 返回200 請求成功 內容, 後面可以加上其它參數,處理文件輸出
inline void 
response_200(int cfd)
{
    // 列印返回200的報文頭
    const char* str = "HTTP/1.0 200 OK\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n";
    
    write(cfd, str, strlen(str));
}

/*
 * 將文件 發送給客戶端
 * cfd        : 客戶端文件描述符
 * path        : 發送的文件路徑
 */
void 
response_file(int cfd, const char* path)
{
    FILE* txt;
    char buf[_INT_BUF];
    
    // 讀取報文頭,就是過濾
    while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))
        ;
    // 這裡開始處理 文件內容
    if((txt = fopen(path, "r")) == NULL) //文件解析錯誤,給它個404
        response_404(cfd);
    else{
        response_200(cfd); //發送給200的報文頭過去
        // 先判斷文件內容存在
        while(!feof(txt) && fgets(buf, sizeof buf, txt))
            write(cfd, buf, strlen(buf));
    }
    fclose(txt);
}

/*
 * 返回啟動的伺服器描述符(句柄)
 * pport     : 輸出參數和輸出參數, 如果傳入NULL,將不返回自動分配的埠
 *             : 返回 啟動的文件描述符
 */
int 
serstart(uint16_t* pport)
{
    int sfd;
    struct sockaddr_in saddr = { AF_INET };
    
    IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0));
    saddr.sin_port = !pport || !*pport ? 0 : htons(*pport);
    saddr.sin_addr.s_addr = INADDR_ANY;
    // 綁定一下埠信息
    IF_CHECK(bind(sfd, (struct sockaddr*)&saddr, sizeof saddr));
    if(pport && !*pport){
        socklen_t clen = sizeof saddr;
        IF_CHECK(getsockname(sfd, (struct sockaddr*)&saddr, &clen));
        *pport = ntohs(saddr.sin_port);
    }
    // 開啟監聽任務
    IF_CHECK(listen(sfd, _INT_LIS));
    return sfd;
}

/*
 * 在客戶端鏈接過來,多線程處理的函數
 * arg        : 傳入的參數, 客戶端文件描述符 (int)arg
 *             : 返回處理結果,這裡預設返回 NULL
 */
void* 
request_accept(void* arg)
{
    char buf[_INT_BUF], path[_INT_BUF>>1], type[_INT_BUF>>5];
    char *lt, *rt, *query, *nb = buf;
    struct stat st;
    int iscgi, cfd = (int)arg;

    if(getfdline(cfd, buf, sizeof buf) <= 0){ //請求錯誤
        response_501(cfd);
        close(cfd);
        return NULL;
    }
    // 合法請求處理
    for(lt=type, rt=nb; !sh_isspace(*rt) && (lt-type)< sizeof type - 1; *lt++ = *rt++)
        ;
    *lt = '\0'; //已經將 buf中開始不為empty 部分塞入了 type 中
    //同樣處理合法與否判斷, 出錯了直接返回錯誤結果
    if((iscgi = strcasecmp(type, "POST")) && strcasecmp(type, "GET")){
        response_501(cfd);
        close(cfd);
        return NULL;
    }
    // 在buf中 去掉空字元
    while(*rt && sh_isspace(*rt))
        ++rt;
    // 這裡得到路徑信息
    *path = '.';
    for(lt = path + 1; (lt-path)<sizeof path - 1 && !sh_isspace(*rt); *lt++ = *rt++)
        ;
    *lt = '\0'; //query url路徑就拼接好了
    
    //單獨處理 get 獲取 ? 後面數據, 不是POST那就是GET
    if(iscgi != 0){
        for(query = path; *query && *query != '?'; ++query)
            ;
        if(*query == '?'){
            iscgi = 0;
            *query++ = '\0';
        }
    }
    
    // type , path 和 query 已經構建好了
    if(stat(path, &st) < 0){
        while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))// 讀取內容直到結束
            ;
        response_404(cfd);
        close(cfd);
        return NULL;
    }
    // 合法情況, 執行,寫入,讀取許可權
    if ((st.st_mode & S_IXUSR) ||(st.st_mode & S_IXGRP) ||(st.st_mode & S_IXOTH))
        iscgi = 0;
    if(iscgi) //沒有cgi
        response_file(cfd, path);
    else
        request_cgi(cfd, path, type, query);
    
    close(cfd);
    return NULL;
}

/*
 * 處理客戶端的http請求.
 * cfd        : 客戶端文件描述符
 * path        : 請求的文件路徑
 * type        : 請求類型,預設是POST,其它是GET
 * query    : 請求發送的過來的數據, url ? 後面那些數據
 */
void 
request_cgi(int cfd, const char* path, const char* type, const char* query)
{
    char buf[_INT_BUF];
    int pocgi[2], picgi[2];
    pid_t pid;
    int contlen = -1; //報文長度
    char c;
    
    if(strcasecmp(type, "POST") == 0){
        while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf)){
            buf[15] = '\0';
            if(!strcasecmp(buf, "Content-Length:"))
                contlen = atoi(buf + 16);
        }
        if(contlen == -1){ //錯誤的報文,直接返回錯誤結果
            response_400(cfd);
            return;
        }
    } 
    else{ // 讀取報文頭,就是過濾, 後面就假定是 GET
        while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))
            ;
    }
    
    //這裡處理請求內容, 先處理錯誤信息
    if(pipe(pocgi) < 0){
        response_500(cfd);
        return;
    }
    if(pipe(picgi) < 0){ //管道 是 0讀取, 1寫入
        close(pocgi[0]), close(pocgi[1]);
        response_500(cfd);
        return;
    }
    if((pid = fork())<0){
        close(pocgi[0]), close(pocgi[1]);
        close(picgi[0]), close(picgi[1]);
        response_500(cfd);
        return;
    }
    // 這裡就是多進程處理了, 先處理子進程
    if(pid == 0) {
        // dup2 讓 前者共用後者同樣的文件表
        dup2(pocgi[1], STDOUT_FILENO); //標準輸出算作 pocgi管道 的寫入端
        dup2(picgi[0], STDIN_FILENO); //標準輸入做為picgif管道的讀取端
        close(pocgi[0]);
        close(pocgi[1]);
        
        // 添加環境變數,用於當前會話中
        sprintf(buf, "REQUEST_METHOD=%s", type);
        putenv(buf);
        // 繼續湊環境變數串,放到當前會話種
        if(strcasecmp(buf, "POST") == 0)
            sprintf(buf, "CONTENT_LENGTH=%d", contlen);
        else
            sprintf(buf, "QUERY_STRING=%s", query);
        putenv(buf);
        // 成功的話調到 新的執行體上
        execl(path, path, NULL);
        
        // 這行代碼原本是不用的, 但是防止 execl執行失敗, 子進程沒有退出.妙招
        exit(EXIT_SUCCESS);
    }
    // 父進程, 為所欲為了,先發送個OK
    write(cfd, "HTTP/1.0 200 OK\r\n", 17);
    close(pocgi[1]);
    close(picgi[0]);
    
    if(strcasecmp(type, "POST") == 0){
        int i; //將數據都寫入到 picgi 管道中, 讓子進程在 picgi[0]中讀取 => STDIN_FILENO
        for(i=0; i<contlen; ++i){
            read(cfd, &c, 1);
            write(picgi[1], &c, 1);
        }
    }
    //從子進程中 讀取數據 發送給客戶端, 多線程跨進程阻塞模型
    while(read(pocgi[0], &c, 1) > 0)
        write(cfd, &c, 1);
    
    close(pocgi[0]);
    close(picgi[1]);
    //等待子進程結束
    waitpid(pid, NULL, 0);
}

 我們看見 上面 函數 解釋的很清楚, 對於 response_* 響應部分占了大頭的一半.其實本質也就200行左右. 很適合臨摹一下.

 

正文

  現在到了正文,說的很水. 再扯一點. 自己學習反人類的庫libuv, 就是note.js 底層通信的那套網路庫. 也就是看官方的demo

一個個的臨摹. 瞭解的. 也就會用. 後面也就簡易的看看源碼. 也就懂了. 最經看的深入之後還是覺得,越簡單越直白越好.封裝太多了,

容易繞暈自己,而且很多功能用不上,遇到bug了又得查看繁瑣的萬行源碼.

  總結就是, 學好基礎 問題, 走到哪裡都容易, 至少能做. 做的好不好, 以後再說.

那我們分析了. 第一個 看下麵函數聲明

// 返回400 請求解析失敗,客戶端代碼錯誤
extern inline void response_400(int cfd);

這裡使用了C的內聯函數, 內聯函數聲明必須要有inline.否則編譯器解析 函數名稱會不一致找不見. 再扯一點對於

strcasecmp 其實是 linux上提供的函數 , window上使用需要做額外配置. 說白了就是不跨平臺. 下麵一種跨平臺的實現如下

/*
 * 這是個不區分大小寫的比較函數
 * ls        : 左邊比較字元串
 * rs        : 右邊比較字元串
 *            : 返回 ls>rs => >0 ; ls = rs => 0 ; ls<rs => <0
 */
int 
str_icmp(const char* ls, const char* rs)
{
    int l, r;
    if(!ls || !rs)
        return (int)(ls - rs);
    
    do {
        if((l=*ls++)>='a' && l<='z')
            l -= 'a' - 'A';
        if((r=*rs++)>='a' && r<='z')
            r -= 'a' - 'A';
    } while(l && l==r);
    
    return l-r;
}

參照編譯器源碼給的一種實現. 性能方面基本上還可以. 這裡再扯一點. 為什麼C中常說用指針速度快.

分析如下 普通的 a[6] ,訪問過程是 先取a首地址,再取a+6地址後面 取*(a+6)的值.

而如果直接用 ptr = a, ++ => ptr -> a+6 那就省略了一步 a+6的問題. 所以快一點.

再扯一點 a[6]其實就是語法糖, 本質也就是 *(a + 6), 通過這個推廣, a[-1] 也合法 等價於 *(a - 1).

後面再簡單分析一下 細節

我們總的思路是 伺服器httpd 採用多線程接收客戶端請求. 再分析報文, 主要是分get請求和post請求.

get請求直接請求, 如果get 後面有? 或post請求 走 cgi 動態處理界面.

說白都很簡單, http 是在tcp 基礎上添加了 http報文的基礎解析內容. 本質是業務邏輯的處理.

這裡繼續說一說 本文中採用的管道細節

    //這裡處理請求內容, 先處理錯誤信息
    if(pipe(pocgi) < 0){
        response_500(cfd);
        return;
    }
    if(pipe(picgi) < 0){ //管道 是 0讀取, 1寫入
        close(pocgi[0]), close(pocgi[1]);
        response_500(cfd);
        return;
    }
    if((pid = fork())<0){
        close(pocgi[0]), close(pocgi[1]);
        close(picgi[0]), close(picgi[1]);
        response_500(cfd);
        return;
    }

這裡是請求失敗會相應釋放打開的埠. 理論上在exit之後系統會自動回收打開的埠.但是不及時.

對於上面管道 是 子進程充定向管道為標準輸入輸出. 父進程向管道中寫入給子進程標準輸入輸出. 這就是傳說的cgi.

最後說明一段代碼

/*
 * 主邏輯,啟動服務,可以做成守護進程.
 * 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務
 */
int main(int argc, char* argv[])
{
    pthread_attr_t attr;
    uint16_t port = 0;
    int sfd = serstart(&port);
    
    printf("httpd running on port %u.\n", port);
    // 初始化線程屬性
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    for(;;){
        pthread_t tid;
        struct sockaddr_in caddr;
        socklen_t clen = sizeof caddr;
        int cfd = accept(sfd, (struct sockaddr*)&caddr, &clen);
        if(cfd < 0){
            CERR("accept sfd = %d is error!", sfd);
            break;
        }
        if(pthread_create(&tid, &attr, request_accept, (void*)cfd) < 0)
            CERR("pthread_create run is error!");
    }
    // 銷毀吧, 一切都結束了
    pthread_attr_destroy(&attr);
    close(sfd);
    return 0;
}

這是主業務, 亮點在於 pthread_attr 這塊, 添加了線程分離屬性, 自己回收. 不需要內核繼續保存線程屍體.

最後記得釋放.

到這裡基本細節我們都說完了. 對於 serstart 中採用了隨機埠, 是為了不合 伺服器可能的http服務8080埠衝突, 就來個隨機埠.

對於socket 採用0埠,意思就是操作系統隨機分配. 

 

測試

  下麵我們開始測試測試 的 client.c 代碼

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

//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)

//4.3 if 的 代碼檢測
#define IF_CHECK(code)    \
    if((code) < 0) \
        CERR_EXIT(#code)

//待拼接的字元串
#define _STR_HTTP_1 "GET /index.html HTTP/1.0\r\nUser-Agent: Happy is good.\r\nHost: 127.0.0.1:"
#define _STR_HTTP_3 "\r\nConnection: close\r\n\r\n"

// 簡單請求一下
int main(int argc, char* argv[])
{
    char buf[1024];
    int sfd;
    struct sockaddr_in saddr = { AF_INET };
    int len, port;
    // argc 預設為1 第一個參數 就是 執行程式串
    if((argc != 2) || (port=atoi(argv[1])) <= 0 )
        CERR_EXIT("Usage: %s [port]", argv[0]);
    
    // 開始了,就這樣了    
    IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0));
    saddr.sin_port = htons(port);
    saddr.sin_addr.s_addr = INADDR_ANY;
    IF_CHECK(connect(sfd, (struct sockaddr*)&saddr, sizeof saddr));
    
    //開始發送請求
    strcpy(buf, _STR_HTTP_1);
    strcat(buf, argv[1]);
    strcat(buf, _STR_HTTP_3);
    write(sfd, buf, strlen(buf));

    //讀取所喲內容
    while((len = read(sfd, buf, sizeof buf - 1))){
        buf[len] = '\0';
        printf("%s", buf);    
    }
    putchar('\n');    

    close(sfd);
    return 0;
}

這裡就簡單向httpd 發送get 請求 index.html界面. 這裡再扯一點, 這個httpd 許多細節沒有考慮,容錯性不是那麼健全.

這些都好做,只要理解了實現思路和詳細瞭解HTTP協議就可以寫出好的HTTP知識,當然TCP的功底不可或缺,這點也很有挑戰.

對於index.html 界面如下

<html>
<head>
    <title> 有意思 </title>
</head>
<body>
    <p> 只有野獸不會欺騙 <p>
</body>
</html>

最後上 Makefile

all:httpd.out client.out
    
httpd.out:httpd.c
    gcc -g -Wall -o $@ $^ -lpthread
client.out:client.c
    gcc -g -Wall -o $@ $^

最後執行結果示意圖圖如下,先啟動 httpd伺服器

 後面開啟http測試機, 需要輸入埠34704 如下

到這裡我們至少簡單測試都過了.

一切都是那麼自然而然. 前提你要個節奏,這個你能堅持. 節奏很重要, 裝逼是次要的.下次有機會再分享

開發中需要用到的一些開發模型和細節. 或者分享簡單高效的網路庫知識. 最後扯一點, 都是從不懂,一點都不懂

堅持臨摹開始的.後面就懂了, 只有不懂和痛苦,噁心才會有點意思.哈哈.

 

後記

  錯誤是難免的, 歡迎交流, 拜~~~


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

-Advertisement-
Play Games
更多相關文章
  • 必知:軟體企業要求基礎軟體工程師具備六大基本素質,即良好的編碼能力、自覺的規範意識和團隊精神、認識和運用資料庫的能力、較強的英語閱讀和寫作能力、具有軟體工程的概念和求知欲和進取心。                   1.良好的編碼能力。軟體人員的一個重要職責是把用戶的需求功能用某種電腦語言予以實
  • python利用or在列表解析中調用多個函數.py
  • 2.在定義常量的時候C語言中定義為const而JAVA中為final3.在JAVA聲明成員變數的時候,使用static來定義。4.在JAVA中的boolean類型只包括true和false,但是在C中非0為true,0為false5.在JAVA中byte、short、int、long其存儲空間分別為
  • 今天幫同學想用C實現數組的折半查找,本來演算法挺簡單的,可是折騰了好幾個小時才發現問題在哪,這個sizeof坑人不淺啊。 明白這裡了,附上一篇C實現折半的代碼  
  • 一、Java的特點:一次編譯,到處運行時間。   C語言在windows下執行:C源程式(.c)——>編譯 windows可執行文件(.exe)——>windows操作系統  Java語言:Java源文件——>編譯 Java位元組碼文件(.class)——>JVM虛擬機下 (能直接解釋Java位元組碼C
  • -->位元組碼解釋器工作就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、線程恢復等基礎功能都需要依賴計數器來完成。 -->為了線程切換後能恢復到正確的執行位置,每條線程都需要一個獨立的程式計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類記憶體區域為"線程私
  • 初學python第一天,希望自己真正瞭解電腦語言,並且做出成效。   寫下學習筆記,記錄學習進度,娛樂學習,不斷成長。   python詳細介紹:   python是什麼?運用到哪裡?有哪些在使用它? python是一門編程語言,可以運用到各種場景,基本大型公司都在用它,主要運用在網路編程方面。如
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...