1. C 語言實現(xiàn)一個簡單的 web 服務器

        共 5675字,需瀏覽 12分鐘

         ·

        2022-02-16 02:04

        說到 web 服務器想必大多數(shù)人首先想到的協(xié)議是 http,那么 http 之下則是 tcp,本篇文章將通過 tcp 來實現(xiàn)一個簡單的 web 服務器。

        本篇文章將著重講解如何實現(xiàn),對于 http 與 tcp 的概念本篇將不過多講解。

        一、了解 Socket 及 web 服務工作原理

        既然是基于 tcp 實現(xiàn) web 服務器,很多學習 C 語言的小伙伴可能會很快的想到套接字 socket。socket 是一個較為抽象的通信進程,或者說是主機與主機進行信息交互的一種抽象。socket 可以將數(shù)據(jù)流送入網(wǎng)絡中,也可以接收數(shù)據(jù)流。

        socket 的信息交互與本地文件信息的讀取從表面特征上看類似,但其中所存在的編寫復雜度是本地 IO 不能比擬的,但卻有相似點。在 win 下 socket 的交互交互步驟為:WSAStartup 進行初始化--> socket 創(chuàng)建套接字--> bind 綁定--> listen 監(jiān)聽--> connect 連接--> accept 接收請求--> send/recv 發(fā)送或接收數(shù)據(jù)--> closesocket 關(guān)閉 socket--> WSACleanup 最終關(guān)閉。

        了解完了一個 socket 的基本步驟后我們了解一下一個基本 web 請求的用戶常規(guī)操作,操作分為:打開瀏覽器-->輸入資源地址 ip 地址-->得到資源。當目標服務器接收到該操作產(chǎn)生掉請求后,我們可以把服務器的響應流程步驟看為:獲得 request 請求-->得到請求關(guān)鍵數(shù)據(jù)-->獲取關(guān)鍵數(shù)據(jù)-->發(fā)送關(guān)鍵數(shù)據(jù)。服務器的這一步流程是在啟動socket 進行監(jiān)聽后才能響應。通過監(jiān)聽得知接收到請求,使用 recv 接收請求數(shù)據(jù),從而根據(jù)該參數(shù)得到進行資源獲取,最后通過 send 將數(shù)據(jù)進行返回。

        二、創(chuàng)建sokect完成監(jiān)聽

        2.1 WSAStartup初始化

        首先在c語言頭文件中引入依賴 WinSock2.h:

        #include?

        在第一點中對 socket 的創(chuàng)建步驟已有說明,首先需要完成 socket 的初始化操作,使用函數(shù) WSAStartup,該函數(shù)的原型為:

        int?WSAStartup(
        ??WORD??????wVersionRequired,
        ??LPWSADATA?lpWSAData
        )
        ;

        該函數(shù)的參數(shù) wVersionRequired 表示 WinSock2 的版本號;lpWSAData 參數(shù)為指向 WSADATA 的指針,WSADATA 結(jié)構(gòu)用于 WSAStartup 初始化后返回的信息。

        wVersionRequired 可以使用 MAKEWORD 生成,在這里可以使用版本 1.1 或版本2.2,1.1 只支持 TCP/IP,版本 2.1 則會有更多的支持,在此我們選擇版本 1.1。

        首先聲明一個 WSADATA 結(jié)構(gòu)體 ?:

        WSADATA?wsaData;

        隨后傳參至初始化函數(shù) WSAStartup 完成初始化:

        WSAStartup(MAKEWORD(1,?1),?&wsaData)

        WSAStartup 若初始化失敗則會返回非0值:

        if?(WSAStartup(MAKEWORD(1,?1),?&wsaData)?!=?0)?
        {
        ?exit(1);
        }

        2.2 創(chuàng)建socket 套接字

        初始化完畢后開始創(chuàng)建套接字,套接字創(chuàng)建使用函數(shù),函數(shù)原型為:

        SOCKET?WSAAPI?socket(
        ??int?af,
        ??int?type,
        ??int?protocol
        )
        ;

        在函數(shù)原型中,af 表示 IP 地址類型,使用 PF_INET 表示 IPV4,type 表示使用哪種通信類型,例如 SOCK_STREAM 表示 TCP,protocol 表示傳輸協(xié)議,使用 0 會根據(jù)前 2 個參數(shù)使用默認值。

        int?skt?=?socket(PF_INET,?SOCK_STREAM,?0);

        創(chuàng)建完 socket 后,若為 -1 表示創(chuàng)建失敗,進行判斷如下:

        if?(skt?==?-1)?
        {?????????
        ?return?-1;
        }

        2.3 綁定服務器

        創(chuàng)建完 socket 后需要對服務器進行綁定,配置端口信息、IP 地址等。首先查看 bind 函數(shù)需要哪一些參數(shù),函數(shù)原型如下:

        int?bind(
        ??SOCKET?????????socket,
        ??const?sockaddr?*addr,
        ??int????????????addrlen
        )
        ;

        參數(shù) socket 表示綁定的 socket,傳入 socket 即可;addr 為 sockaddr_in 的結(jié)構(gòu)體變量的指針,在 sockaddr_in 結(jié)構(gòu)體變量中配置一些服務器信息;addrlen 為 addr 的大小值。

        通過 bind 函數(shù)原型得知了我們所需要的數(shù)據(jù),接下來創(chuàng)建一個 sockaddr_in 結(jié)構(gòu)體變量用于配置服務器信息:

        struct?sockaddr_in?server_addr;

        隨后配置地址家族為AF_INET對應TCP/IP:

        server_addr.sin_family?=?AF_INET;

        接著配置端口信息:

        server_addr.sin_port?=?htons(8080);

        再指定 ip 地址:

        server_addr.sin_addr.s_addr?=?inet_addr("127.0.0.1");

        ip 地址若不確定可以手動輸入,最后使用神器 memset 初始化內(nèi)存,完整代碼如下:

        //配置服務器?
        struct?sockaddr_in?server_addr;
        server_addr.sin_family?=?AF_INET;
        server_addr.sin_port?=?htons(8080);
        server_addr.sin_addr.s_addr?=?inet_addr("127.0.0.1");
        memset(&(server_addr.sin_zero),?'\0',?8);

        隨后使用 bind 函數(shù)進行綁定且進行判斷是否綁定成功:

        //綁定
        if?(bind(skt,?(struct?sockaddr?*)&server_addr,sizeof(server_addr))?==?-1)?{???????
        ?return?-1;?
        }?

        2.4 listen進行監(jiān)聽

        綁定成功后開始對端口進行監(jiān)聽。查看 listen 函數(shù)原型:

        int?listen(
        ?int?sockfd,?
        ?int?backlog
        )

        函數(shù)原型中,參數(shù) sockfd 表示監(jiān)聽的套接字,backlog 為設置內(nèi)核中的某一些處理(此處不進行深入講解),直接設置成 10 即可,最大上限為 128。使用監(jiān)聽并且判斷是否成功代碼為:

        if?(listen(skt,?10)?==?-1?)?{????
        ?return?-1;
        }

        此階段完整代碼如下:

        #include?
        #include?
        int?main(){
        ?//初始化?
        ?WSADATA?wsaData;
        ?if?(WSAStartup(MAKEWORD(1,?1),?&wsaData)?!=?0)?{
        ??exit(1);
        ?}
        ?//socket創(chuàng)建?
        ?int?skt?=?socket(PF_INET,?SOCK_STREAM,?0);
        ?if?(skt?==?-1)?{?????????
        ??return?-1;
        ?}
        ?//配置服務器?
        ?struct?sockaddr_in?server_addr;
        ?server_addr.sin_family?=?AF_INET;
        ?server_addr.sin_port?=?htons(8080);
        ?server_addr.sin_addr.s_addr?=?inet_addr("127.0.0.1");
        ?memset(&(server_addr.sin_zero),?'\0',?8);
        ?//綁定
        ?if?(bind(skt,?(struct?sockaddr?*)&server_addr,sizeof(server_addr))?==?-1){???????
        ??return?-1;?
        ?}?
        ?//監(jiān)聽?
        ?if?(listen(skt,?10)?==?-1?)?{????
        ??return?-1;
        ?}
        ?
        ?printf("Listening?...?...\n");
        }

        運行代碼可得知代碼無錯誤,并且輸出 listening:

        在這里插入圖片描述

        2.5 獲取請求

        監(jiān)聽完成后開始獲取請求。受限需要使用 accept 對套接字進行連接,accept 函數(shù)原型如下:

        int?accept(
        ?int?sockfd,
        ?struct?sockaddr?*addr,
        ?socklen_t?*addrlen
        ?)
        ;

        參數(shù) sockfd 為指定的套接字;addr 為指向 struct sockaddr 的指針,一般為客戶端地址;addrlen 一般設置為設置為 sizeof(struct ? sockaddr_in) 即可。代碼為:

        struct?sockaddr_in?c_skt;?
        int?s_size=sizeof(struct???sockaddr_in);
        int?access_skt?=?accept(skt,?(struct?sockaddr?*)&c_skt,?&s_size);

        接下來開始接受客戶端的請求,使用recv函數(shù),函數(shù)原型為:

        ssize_t?recv(
        ?int?sockfd,?
        ?void?*buf,?
        ?size_t?len,?
        ?int?flags
        )

        參數(shù) sockfd 為 accept 建立的通信;buf 為緩存,數(shù)據(jù)存放的位置;len 為緩存大??;flags 一般設置為0即可:

        //獲取數(shù)據(jù)?
        char?buf[1024];
        if?(recv(access_skt,?buf,?1024,?0)?==?-1)?{
        ?exit(1);
        }

        此時我們再到 accpt 和 recv 外層添加一個循環(huán),使之流程可重復:

        while(1){
        ??//建立連接?
        ??printf("Listening?...?...\n");
        ??struct?sockaddr_in?c_skt;?
        ??int?s_size=sizeof(struct???sockaddr_in);
        ??int?access_skt?=?accept(skt,?(struct?sockaddr?*)&c_skt,?&s_size);
        ??
        ??//獲取數(shù)據(jù)?
        ??char?buf[1024];
        ??if?(recv(access_skt,?buf,?1024,?0)?==?-1)?{
        ???exit(1);
        ??}
        ?}?

        并且可以在瀏覽器輸入 127.0.0.1:8080 將會看到客戶端打印了 listening 新建了鏈接:

        我們添加printf語句可查看客戶端請求:

        while(1){
        ??//建立連接?
        ??printf("Listening?...?...\n");
        ??struct?sockaddr_in?c_skt;?
        ??int?s_size=sizeof(struct???sockaddr_in);
        ??int?access_skt?=?accept(skt,?(struct?sockaddr?*)&c_skt,?&s_size);
        ??
        ??//獲取數(shù)據(jù)?
        ??char?buf[1024];
        ??if?(recv(access_skt,?buf,?1024,?0)?==?-1)?{
        ???exit(1);
        ??}
        ??
        ??printf("%s",buf);
        ?}?

        接下來我們對請求頭進行對應的操作。

        2.6 請求處理層編寫

        得到請求后開始編寫處理層。繼續(xù)接著代碼往下寫沒有層級,編寫一個函數(shù)名為 req,該函數(shù)接收請求信息與一個建立好的連接為參數(shù):

        void?req(char*?buf,?int?access_socket)?
        {
        }

        然后先在 while 循環(huán)中傳遞需要的值:

        req(buf,?access_skt);

        接著開始編寫 req 函數(shù),首先在 req 函數(shù)中標記當前目錄下:

        char?arguments[BUFSIZ];??
        strcpy(arguments,?"./");

        隨后分離出請求與參數(shù):

        char?command[BUFSIZ];?????
        sscanf(request,?"%s%s",?command,?arguments+2);

        接著我們標記一些頭元素:

        char*?extension?=?"text/html";???
        char*?content_type?=?"text/plain";?????
        char*?body_length?=?"Content-Length:?";

        接著獲取請求參數(shù),若獲取 index.html,就獲取當前路徑下的該文件:

        FILE*?rfile=?fopen(arguments,?"rb");

        獲取文件后表示請求 ok,我們先返回一個 200 狀態(tài):

        char*?head?=?"HTTP/1.1?200?OK\r\n";????
        int?len;?
        char?ctype[30]?=?"Content-type:text/html\r\n";???
        len?=?strlen(head);

        接著編寫一個發(fā)送函數(shù) send_:

        int?send_(int?s,?char?*buf,?int?*len)?
        {
        ?int?total;??????????
        ?int?bytesleft;????????????????????????????????
        ?int?n;
        ?total=0;
        ?bytesleft=*len;
        ?while(total??{
        ??n?=?send(s,?buf+total,?bytesleft,?0);
        ??if?(n?==?-1)?
        ??{
        ???break;
        ??}
        ??total?+=?n;
        ??bytesleft?-=?n;
        ?}
        ?*len?=?total;??????????
        ?return?n==-1?-1:0;?????????
        }

        send 函數(shù)功能并不難在此不再贅述,就是一個遍歷發(fā)送的邏輯。隨后發(fā)送 http 響應與文件類型:

        send_(send_to,?head,?&len);
        len?=?strlen(ctype);
        send_(send_to,?ctype,?&len);

        隨后獲得請求文件的描述,需要添加頭文件#include 使用fstat,且向已連接的通信發(fā)生必要的信息 :

        //獲取文件描述
        struct?stat?statbuf;
        char?read_buf[1024];???????
        char?length_buf[20];
        fstat(fileno(rfile),?&statbuf);
        itoa(?statbuf.st_size,?length_buf,?10?);
        send(client_sock,?body_length,?strlen(body_length),?0);
        send(client_sock,?length_buf,?strlen(length_buf),?0);

        send(client_sock,?"\n",?1,?0);
        send(client_sock,?"\r\n",?2,?0);

        最后發(fā)送數(shù)據(jù):

        //·數(shù)據(jù)發(fā)送
        char?read_buf[1024];?
        len?=?fread(read_buf?,1?,?statbuf.st_size,?rfile);
        if?(send_(client_sock,?read_buf,?&len)?==?-1)?{?
        ?printf("error!");???
        }

        最后訪問地址 http://127.0.0.1:8080/index.html,得到當前目錄下 index.html 文件數(shù)據(jù),并且在瀏覽器渲染:

        所有代碼如下:

        #include?
        #include?
        #include??

        int?send_(int?s,?char?*buf,?int?*len)?{
        ?int?total;??????????
        ?int?bytesleft;????????????????????????????????
        ?int?n;
        ?total=0;
        ?bytesleft=*len;
        ?while(total??{
        ??n?=?send(s,?buf+total,?bytesleft,?0);
        ??if?(n?==?-1)?
        ??{
        ???break;
        ??}
        ??total?+=?n;
        ??bytesleft?-=?n;
        ?}
        ?*len?=?total;??????????
        ?return?n==-1?-1:0;?????????
        }

        void?req(char*?request,?int?client_sock)?{???
        ?char?arguments[BUFSIZ];??
        ?strcpy(arguments,?"./");
        ?
        ?char?command[BUFSIZ];?????
        ?sscanf(request,?"%s%s",?command,?arguments+2);
        ?
        ?char*?extension?=?"text/html";???
        ?char*?content_type?=?"text/plain";?????
        ?char*?body_length?=?"Content-Length:?";
        ?
        ?FILE*?rfile=?fopen(arguments,?"rb");
        ?

        ?char*?head?=?"HTTP/1.1?200?OK\r\n";????
        ?int?len;?
        ?char?ctype[30]?=?"Content-type:text/html\r\n";???
        ?len?=?strlen(head);
        ??
        ?send_(client_sock,?head,?&len);
        ?len?=?strlen(ctype);
        ?send_(client_sock,?ctype,?&len);
        ?

        ?struct?stat?statbuf;
        ???????
        ?char?length_buf[20];
        ?fstat(fileno(rfile),?&statbuf);
        ?itoa(?statbuf.st_size,?length_buf,?10?);
        ?send(client_sock,?body_length,?strlen(body_length),?0);
        ?send(client_sock,?length_buf,?strlen(length_buf),?0);

        ?send(client_sock,?"\n",?1,?0);
        ?send(client_sock,?"\r\n",?2,?0);
        ?

        ?char?read_buf[1024];?
        ?len?=?fread(read_buf?,1?,?statbuf.st_size,?rfile);
        ?if?(send_(client_sock,?read_buf,?&len)?==?-1)?{?
        ??printf("error!");???
        ?}
        ?
        ?return;
        }


        int?main(){
        ?WSADATA?wsaData;
        ?if?(WSAStartup(MAKEWORD(1,?1),?&wsaData)?!=?0)?{
        ??exit(1);
        ?}

        ?int?skt?=?socket(PF_INET,?SOCK_STREAM,?0);
        ?if?(skt?==?-1)?{?????????
        ??return?-1;
        ?}

        ?struct?sockaddr_in?server_addr;
        ?server_addr.sin_family?=?AF_INET;
        ?server_addr.sin_port?=?htons(8080);
        ?server_addr.sin_addr.s_addr?=?inet_addr("127.0.0.1");
        ?memset(&(server_addr.sin_zero),?'\0',?8);

        ?if?(bind(skt,?(struct?sockaddr?*)&server_addr,sizeof(server_addr))?==?-1)?{???????
        ??return?-1;?
        ?}?

        ?if?(listen(skt,?10)?==?-1?)?{????
        ??return?-1;
        ?}
        ?
        ?while(1){

        ??printf("Listening?...?...\n");
        ??struct?sockaddr_in?c_skt;?
        ??int?s_size=sizeof(struct???sockaddr_in);
        ??int?access_skt?=?accept(skt,?(struct?sockaddr?*)&c_skt,?&s_size);

        ??char?buf[1024];
        ??if?(recv(access_skt,?buf,?1024,?0)?==?-1)?{
        ???exit(1);
        ??}
        ??
        ??req(buf,?access_skt);
        ?}?
        ?
        }

        小伙伴們可以編寫更加靈活的指定資源類型、錯誤處理等完善這個 demo。

        瀏覽 29
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 视频一区国产精品 | 白丝校花被狂揉大胸羞羞 | 吴梦梦AV无码一区二区三区小说 | 爱搞搞综合 | 激情五月久久 |