程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C 實現一個簡易的Http服務器,服務器

C 實現一個簡易的Http服務器,服務器

編輯:關於C語言

C 實現一個簡易的Http服務器,服務器


引言

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

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

 

本文 參照

  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 如下

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

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

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

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

 

後記

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved