1. 程式人生 > >編寫簡易的 HTTP 服務器程序

編寫簡易的 HTTP 服務器程序

C http server

服務端代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
#include "windows.h"
#include "winsock2.h"

#define PORT 3000
#define BACKLOG 10

#define BUFSIZE 1024

int main()
{
    WORD sockVersion = MAKEWORD(2,2);
    WSADATA wsaData;
    if(WSAStartup(sockVersion, &wsaData)!=0)
    {
        return 0;
    }

    int sockfd;
    int client_fd;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    int struct_len;
    char buf[BUFSIZ];
    char str[4096];

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bzero(&(server_addr.sin_zero),8);
    struct_len = sizeof(struct sockaddr_in);

    /* 創建一個socket */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    /* 綁定套接字到端口 */
    if (bind(sockfd, (struct sockaddr *)&server_addr, struct_len) == SOCKET_ERROR) {
        perror("Bind error\n");
    }

    /* 啟動socket監聽請求,開始等待客戶端發來的請求 */
    if (listen(sockfd, BACKLOG) == SOCKET_ERROR) {
        perror("Listen error\n");
        return -1;
    }
    printf("http server running on port %d\n", PORT);
    
    while (1) {
        /* 調用了accept函數,阻塞了程序,直到接收到客戶端的請求 */
        client_fd = accept(sockfd, &client_addr, &struct_len);
        if (client_fd == INVALID_SOCKET) {
            printf("accept error\n");
            continue;
        }

        /* 調用recv函數接收客戶端發來的請求信息 */
        int ret = recv(client_fd, buf, BUFSIZ, 0);
        if (ret >0) {
            buf[ret] = '\0';
            printf("recv data: \n%s\n", buf);

            char content[1024] = "hello world";
            char content_len[1024];

            memset(str, 0, 4096);
            strcat(str, "HTTP/1.0 200 OK\r\n");
            strcat(str, "Content-Type: text/html;charset=utf-8\r\n");
            strcat(str, "Content-Length: ");
            sprintf(content_len, "%lu\r\n", strlen(content));
            strcat(str, content_len);
            strcat(str, "\r\n");
            strcat(str, content);
            printf("send data: \n%s\n", str);
            
            /* 發送響應給客戶端 */
            int s_ret = send(client_fd, str, strlen(str), 0);
            if ( s_ret < 0) {
                perror("send error\n");
            }            
        }
        close(client_fd);
    }

    close(sockfd);
    WSACleanup();
    return 0;
}

編譯:

gcc server.c -o server.exe

運行:

./server.exe

技術分享圖片

瀏覽器訪問http://127.0.0.1:3000

技術分享圖片

啟動server的流程
技術分享圖片

創建一個套接字,通過各參數指定套接字的類型。

int socket(int af,int type,int protocol);
  • family:協議族。AF_INET:IPV4協議;AF_INET6:IPv6協議;AF_LOCAL:Unix域協議;AF_ROUTE:路由套接字;AF_KEY:密鑰套接字

  • type:套接字類型。SOCK_STREAM : 字節流套接字;SOCK_DGRAM:數據包套接字;SOCK_SEGPACKET:有序分組套接字;SOCK_RAW:原始套接字

  • protocol:某個協議類型常量。TCP:0,UDP :1, SCTP :2


套接字地址結構

在socket編程中,大部分函數都用到一個指向套接字地址結構的指針作為參數。針對不同的協議類型,會有不同的結構體定義格式,對於ipv4,結構如下所示:

struct sockaddr_in {
    short    sin_family;           /* IP協議族,IPV4是AF_INET */
    u_short    sin_port;           /* 一個16比特的TCP/UDP端口地址 */
    struct in_addr    sin_addr;    /* 32比特的IPV4地址,網絡字節序 */
    char    sin_zero[8];           /* 未使用字段 */
};

註:sockaddr_in是**Internet socket address structure**的縮寫。

ip地址結構

struct in_addr {
  union {
    struct { u_char  s_b1, s_b2, s_b3, s_b4; } S_un_b;
    struct { u_short s_w1, s_w2; } S_un_w;
    u_long S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

套接字地址結構的作用是為了將ip地址和端口號傳遞到socket函數,寫成結構體的方式是為了抽象。當作為一個參數傳遞進任何套接字函數時,套接字地址結構總是以引用方式傳遞。然而,協議族有很多,因此以這樣的指針作為參數之一的任何套接字函數必須處理來自所有支持的任何協議族的套接字地址結構。使用void *作為通用的指針類型,因此,套接字函數被定義為以指向某個通用套接字結構的一個指針作為其參數之一,正如下面的bind函數原型一樣。

int bind(SOCKET s,const struct sockaddr *name,int namelen);


這就要求,對這些函數的任何調用都必須要將指向特定於協議的套接字地址結構的指針進行強制類型轉換,變成某個通用套接字地址結構的指針。例如:

struct sockaddr_in addr;
bind(sockfd, (struct sockaddr *)&addr , sizeof(addr));


對於所有socket函數而言,sockaddr的唯一用途就是對指向特定協議的套接字地址結構的指針執行強制類型轉換,指向要綁定給sockfd的協議地址。


bind函數

將套接字地址結構綁定到套接字

int bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
  • sockfd:socket描述符,唯一標識一個socket。bind函數就是將這個描述字綁定一個名字。

  • addr:一個sockaddr指針,指向要綁定給sockfd的協議地址。一個socket由ip和端口號唯一確定,而sockaddr就包含了ip和端口的信息 地址的長度

綁定了socket之後,就可以使用該socket開始監聽請求了。

listen函數

將sockfd從未連接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的連接請求。

int listen(SOCKET s,int backlog);

listen函數會將套接字從CLOSED狀態轉換到LISTEN狀態,第二個參數規定內核應該為相應套接字排隊的最大連接個數。

關於backlog參數,內核為任何一個給定的監聽套接字維護兩個隊列:

1、未完成連接隊列,在隊列裏面的套接字處於SYN_RCVD狀態

2、已完成隊列,處於ESTABLISHED狀態

兩個隊列之和不超過backlog的大小。

listen完成之後,socket就處於LISTEN狀態,此時的socket調用accept函數就可以接受客戶端發來的請求了。


accept函數

int accept(SOCKET s,struct sockaddr *addr,int *addrlen);

用於從已完成連接隊列頭返回下一個已完成連接,如果已完成連接隊列為空,那麽進程就會被阻塞。因此調用了accept函數之後,進程就會被阻塞,直到有新的請求到來。

第一個參數sockfd是客戶端的套接字描述符,第二個是客戶端的套接字地址結構,第三個是套接字地址結構的長度。

如果accept成功,那麽返回值是由內核自動生成的全新描述符,代表所返回的客戶端的TCP連接。

對於accept函數,第一個參數稱為監聽套接字描述符,返回值稱為已連接套接字。服務器僅創建監聽套接字,它一直存在。已連接套接字由服務器進程接受的客戶連接創建,當服務器完成某個連接的響應後,相應的已連接套接字就被關閉了。

accept函數返回時,會返回套接字描述符或出錯指示的整數,以及引用參數中的套接字地址和該地址的大小。如果對返回值不感興趣,可以把兩個引用參數設為空。

accept之後,一個TCP連接就建立起來了,接著,服務器就接受客戶端的請求信息,然後做出響應。


recv和send函數

int recv(SOCKET s,char *buf,int len,int flags);
int send(SOCKET s,const char *buf,int len,int flags);


分別用於從客戶端讀取信息和發送信息到客戶端。在此不做過多的解釋。


套接字地址結構大小和值-結果參數

可以看到,在bind函數和accept函數裏面,都有一個套接字地址結構長度的參數,區別在於一個是值形式,另一個是引用形式。套接字地址結構的傳遞方式取決於該結構的傳遞方向:是從進程到內核,還是從內核到進程。

1、從進程到內核:bind、connect、sendto。 函數將指針和指針所指內容的大小都傳給了內核,於是內核知道到底需要從進程復制多少數據進來。
2、從內核到進程: accept、recvfrom、getsockname、getperrname。 這四個函數的結構大小是以只引用的方式傳遞。 因為當函數被調用時,結構大小是一個值,它告訴內核該結構的大小,這樣內核在寫該結構時不至於越界;當函數返回時,結構大小又是一個結果,它告訴內核在該結構中究竟存儲了多少信息。


HTTP響應報文

發送響應給客戶端時,發送的報文要遵循HTTP協議,HTTP的響應報文格式如下:

<status-line>
<headers>
<blank line>
[<response-body>]


第一行status-line,狀態欄,格式:HTTP版本 狀態碼 狀態碼代表文字

第二行headers是返回報文的類型,長度等信息

第三行是一個空行,

第四行是響應報文的實體。

一個HTTP響應報文例子:

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 122

hello world


最後close函數關閉套接字


編寫簡易的 HTTP 服務器程序