1. C 語言實現一個簡單的 web 服務器

        共 14260字,需瀏覽 29分鐘

         ·

        2022-07-05 06:13

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

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

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

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

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

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

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

        2.1 WSAStartup初始化

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

        #include <WinSock2.h>

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

        int WSAStartup(
          WORD      wVersionRequired,
          LPWSADATA lpWSAData
        )
        ;

        該函數的參數 wVersionRequired 表示 WinSock2 的版本號;lpWSAData 參數為指向 WSADATA 的指針,WSADATA 結構用于 WSAStartup 初始化后返回的信息。

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

        首先聲明一個 WSADATA 結構體  :

        WSADATA wsaData;

        隨后傳參至初始化函數 WSAStartup 完成初始化:

        WSAStartup(MAKEWORD(11), &wsaData)

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

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

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

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

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

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

        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 函數需要哪一些參數,函數原型如下:

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

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

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

        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 初始化內存,完整代碼如下:

        //配置服務器 
        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 函數進行綁定且進行判斷是否綁定成功:

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

        2.4 listen進行監(jiān)聽

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

        int listen(
         int sockfd, 
         int backlog
        )

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

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

        此階段完整代碼如下:

        #include <WinSock2.h>
        #include<stdio.h> 
        int main(){
         //初始化 
         WSADATA wsaData;
         if (WSAStartup(MAKEWORD(11), &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 函數原型如下:

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

        參數 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函數,函數原型為:

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

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

        //獲取數據 
        char buf[1024];
        if (recv(access_skt, buf, 10240) == -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);
          
          //獲取數據 
          char buf[1024];
          if (recv(access_skt, buf, 10240) == -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);
          
          //獲取數據 
          char buf[1024];
          if (recv(access_skt, buf, 10240) == -1) {
           exit(1);
          }
          
          printf("%s",buf);
         } 

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

        2.6 請求處理層編寫

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

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

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

        req(buf, access_skt);

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

        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: ";

        接著獲取請求參數,若獲取 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ā)送函數 send_:

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

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

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

        隨后獲得請求文件的描述,需要添加頭文件#include <sys/stat.h>使用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"10);
        send(client_sock, "\r\n"20);

        最后發(fā)送數據:

        //·數據發(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 文件數據,并且在瀏覽器渲染:

        所有代碼如下:

        #include <WinSock2.h>
        #include<stdio.h> 
        #include <sys/stat.h> 

        int send_(int s, char *buf, int *len) {
         int total;          
         int bytesleft;                                
         int n;
         total=0;
         bytesleft=*len;
         while(total < *len) 
         {
          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"10);
         send(client_sock, "\r\n"20);
         

         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(11), &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, 10240) == -1) {
           exit(1);
          }
          
          req(buf, access_skt);
         } 
         
        }

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

        這里是w3cschool編程獅,想學習編程的朋友們關注我們,了解更多的編程知識、IT資訊~


        同時,也歡迎各位朋友們添加我們學習顧問的微信 ↓,可以領取10G免費學習資料包,也可以申請加入學習交流群(包括前端、Python、Java、Php、C、小程序、數據庫等)。


        編程獅學習顧問-七七

        添加時請備注(咨詢

        如果您覺得本篇文章還不錯
        請多多支持一下我們
        點贊、在線、分享三連鼓勵一下

        瀏覽 43
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
          
          

            1. 波多野吉衣一级片 | 丝袜足交视频在线观看 | 一插菊花综合 | 中文字幕成人在线 | 精品无码人妻一区二区免费蜜桃 |