結束網絡編程_第24章製作 HTTP 服務器端

在這裏插入圖片描述

24.1 HTTP 概要

本章以編寫真實應用程序爲目標, 在所學理論知識的基礎上, 編寫 HTTP (Hypertext Transfer Protocol, 超文本傳輸協議) 服務器端, 即 Web 服務器端.

理解 Web 服務器端

互聯網是普及使 Web 服務器端爲大衆熟知. 下面是我對 Web 服務器端的定義:
在這裏插入圖片描述
HTTP 是 Hypettext Transfer Protocol 的縮寫, Hypertext (超文本) 是可以根據客戶端請求而跳轉的結構化信息. 例如, 各位通過瀏覽器訪問圖靈社區的主頁是, 首頁文件將傳輸到瀏覽器並展示給大家, 此時各位點擊鼠標跳轉到任意頁面. 這種可以跳轉的文本( Text ) 稱爲超文本.

HTTP 協議又是什麼呢? HTTP協議是以超文本傳輸爲目的而設計的應用層協議, 這種協議同樣屬於基於 TCP/IP 實現的協議, 因此, 我們也可以直接實現 HTTP. 從結果上看, 實現該協議相當於實現 Web 服務器端. 另外, 瀏覽器也屬於基於套接字的客戶端, 因爲連接到任意 Web 服務器端, 另外, 瀏覽器也屬於基於套接字的客戶端, 因爲連接到任意 Web 服務器端時, 瀏覽器內部也會創建套機字. 只不過瀏覽器多了一項功能, 它將服務器端傳輸的 HTML 格式的超文本解析爲可讀較強的視圖. 總之, Web 服務器端是以 HTTP 協議爲基礎傳輸超文本的服務器端.
在這裏插入圖片描述

HTTP

下面詳細討論 HTTP 協議. 雖然它相對簡單, 但要完全駕馭也並非易事. 接下來只介紹編寫
Web 服務器端時的必要內容.

無狀態的 Stateless 協議

爲了在網絡環境下同時向大量客戶端提供服務, HTTP 協議的請求及相應方式設計如圖 24-1 所示.
在這裏插入圖片描述
從圖 24-1 中可以看到, 服務端相應客戶端請求後立即斷開連接. 換言之, 服務器端, 不會維持客戶端狀態. 即使同一客戶端再次發送請求, 服務器端也無法辯別認出原先那個, 而會以相同方式處理請求. 因此, HTPP 又稱爲 “無狀態的 Stateless 協議”.
在這裏插入圖片描述

請求消息 (Request Message) 的 結構

下面介紹客戶端向服務器端發送請求消失的結構. Web 服務器需要解析並相應客戶端請求, 客戶端和服務器端之間的數據請求方式標準如圖 24-22 所示.
在這裏插入圖片描述
從圖 24-2 中可以看到, 請求消息 可以分爲行, 消息頭, 消息體等3部分. 其中 , 請求行含有 方式(請求目的) 信息. 經典請求方式有GET 和 POST, GET 主要用於請求數據, POST 主要 用於傳輸數據. 爲了降低複雜度, 我們實現只能相應 GET 請求 Web 服務器端. 下面解析圖 24-2 中的請求行信息 . 其中 “GET/index.html HTTP/1.1” 具有如下含義:
在這裏插入圖片描述
請求行只能通過一行 (line) 發送, 因此, 服務器端很容易從HTTP請求中提取第一行, 並分析請求 行中的消息 .

請求行只能通過 1 行 (line) 發送, 因此, 服務器端很容易從HTTP請求中提取 第一行, 並分析請求行中的信息 .

請求行下面的消息頭中包含發送 請求的 (將要接收相應 消息的) 瀏覽器信息 , 用戶認證信息 等關於 HTTP 消息的附加信息 . 最後的消息體中裝有 客戶端向服務器端傳輸數據, 爲了裝入數據, 需要以 POST 方式發送請求. 但我們的目的是實現 GET 方式的服務器端, 所以可以忽略這部分內容. 另外, 消息體和消息頭之間以空格分開, 因此不會發生邊界問題.

相應消息 (Response Message) 的結構

下面介紹 Web 服務器端向客戶端傳遞的相應信息的結構. 從圖 24–3 中 可以看到, 該相應消息 由狀態行 頭信息 , 消息體燈等 3部分 構成. 狀態 行中含有 關於請求 的狀態信息 , 這 是其與 請求消息相比最爲顯著的區別.
在這裏插入圖片描述
從圖24-3中可以看到, 第一個字符串狀態行中含有關於客戶端請求處理結果. 例如, 客戶端請求 index.html 文件時, 表示 index.html 文件是否存在, 服務器端是否發生問題而無法響應等不同情況的信息將寫入狀態行. 圖24-3中的"HTTP/1.1 200 OK" 具體如下含義:
在這裏插入圖片描述
表示 “客戶端請求的執行結果” 的數字稱爲 狀態碼, 典型的有如下幾種.
在這裏插入圖片描述
消息頭中含有傳輸的數據類型和長度等信息. 圖24-3中的消息頭含有如下信息:
在這裏插入圖片描述
最後插入 1 個空行後, 通過消息體發送客戶端請求的文件數據. 以上就是實現 Web 服務器端過程中必要的 HTTP 協議. 要編寫完整的 Web 服務器端還需要更多 HTTP 協議相關知識, 而對於我們的目標而言, 這些內容已經夠了.

24.2 實現簡單的 Web 服務器端
現在開始在 HTTP 協議 的基礎上編寫 Web 服務器端. 先給出 Windows 平臺下的示例, 再給出 Linux 下的示例. 前面介紹了 HTTP 協議的相關背景知識, 有了這些基礎就不難分析源代碼. 因此, 除了一些簡單的註釋外, 不再另行說明代碼.

實現基於 Windows 的多線程 Web 服務器端

Web 服務器端採用 HTTP 協議, 即使用 IOCP 或 epoll 模型也不會大幅度提升性能(當然並不是完全沒有). 客戶端和服務器端交換 1 次數據後將立即斷開連接, 沒有足夠時間發揮 IOCP 或 epoll 的優勢. 在服務器端和客戶端保持較長連接的前提下 頻繁發送大小不一的消息時(最典型的就是網遊服務器端), 才嫩真正發揮出這2中模型的優勢.
在這裏插入圖片描述
因此, 我通過 多線程 模型 實現 服務端 Web 服務器端. 也就是說, 客戶端每次請求時, 都會創建一個新的線程相應客戶端請求.
在這裏插入圖片描述

#include <stdio.h>
#include <stdio.h>
#include <string.h>
#include <WinSock2.h>
#include <process.h>

#define BUF_SIZE 2048
#define BUF_SMALL 100

unsigned WINAPI RequestHandler(void* arg);
const char* ContentType(char* file);
void SendData(SOCKET sock, char* ct, char* fileName);
void SendErrorMSG(SOCKET sock);
void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAdr, clntAdr;

	HANDLE hThread;
	DWORD dwThreadID;
	int clntAdrSize;

	if (argc != 2)
	{
		printf("Usage : %s <port> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error");
	}

	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == SOCKET_ERROR)
	{
		ErrorHandling("socket() error");
	}
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error");
	}

	if (listen(hServSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error");
	}

	/* 請求及響應 */
	while (1)
	{
		clntAdrSize = sizeof(clntAdr);
		hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
		printf("Connection Request : %s : %d \n", 
			inet_ntoa(clntAdr.sin_addr), ntohs(clntAdr.sin_port));

		hThread = (HANDLE)_beginthreadex(
			NULL, 0, RequestHandler, (void*)hClntSock, 0, (unsigned*)&dwThreadID);
	}

	closesocket(hServSock);
	WSACleanup();
	return 0;
}


unsigned WINAPI RequestHandler(void* arg)
{
	SOCKET hClntSock = (SOCKET)arg;
	char buf[BUF_SIZE];
	char method[BUF_SMALL];
	char ct[BUF_SMALL];
	char fileName[BUF_SMALL];

	recv(hClntSock, buf, BUF_SIZE, 0);
	if (strstr(buf, "HTTP/") == NULL) /* strstr(在一字符串中查找指定的字符串) 查看是否爲HTTP提出的請求 */
	{
		SendErrorMSG(hClntSock);
		closesocket(hClntSock);
		return 1;
	}

	strcpy(method, strtok(buf, " /")); /* strtok(分割字符串) */
	if (strcmp(method, "GET")) /* 查看是否爲GET方式的請求 */
	{
		SendErrorMSG(hClntSock);
	}

	strcpy(fileName, strtok(NULL, " /")); /* 查看請求文件名 */
	strcpy(ct, ContentType(fileName)); /* 查看Content-type */
	SendData(hClntSock, ct, fileName); /* 響應 */
	return 0;
}

const char* ContentType(char* file)
{
	char extension[BUF_SMALL];
	char fileName[BUF_SMALL];
	strcpy(fileName, file);
	strtok(fileName, ".");
	strcpy(extension, strtok(NULL, "."));
	if (!strcmp(extension, "html") || !strcmp(extension, "htm"))
	{
		return "text/html";
	}
	else
	{
		return "text/plain";
	}
}

void SendData(SOCKET sock, char* ct, char* fileName)
{
	char protocol[] = "HTTP/1.0 200 OK\r\n";
	char servName[] = "Server : simple web server\r\n";
	char cntLen[] = "Content-length:2048\r\n";
	char cntType[BUF_SMALL];
	char buf[BUF_SIZE];
	FILE* sendFile;

	sprintf(cntType, "Content-type:%s\r\n\r\n", ct);
	if ((sendFile = fopen(fileName, "r")) == NULL)
	{
		SendErrorMSG(sock);
		return;
	}

	/* 傳輸頭信息 */
	send(sock, protocol, strlen(protocol), 0);
	send(sock, servName, strlen(servName), 0);
	send(sock, cntLen, strlen(cntLen), 0);
	send(sock, cntType, strlen(cntType), 0);

	/* 傳輸請求數據 */
	while (fgets(buf, BUF_SIZE, sendFile) != NULL)
	{
		send(sock, buf, strlen(buf), 0);
	}

	closesocket(sock); /* 由HTTP協議響應後斷開 */
}

void SendErrorMSG(SOCKET sock)
{
	char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
	char servName[] = "Server:simple web server\r\n";
	char cntLen[] = "Content-length:2048\r\n";
	char cntType[] = "Content-type:text/html\r\n\r\n";
	char content[] = "<html><head><title>NETWORK</title></heand>"
		"<body><font size=+5><br> 發生錯誤! 查看請求文件名和請求方式! "
		"</font></boy></html>";

	send(sock, protocol, strlen(protocol), 0);
	send(sock, servName, strlen(servName), 0);
	send(sock, cntLen, strlen(cntLen), 0);
	send(sock, cntType, strlen(cntType), 0);

	closesocket(sock);
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

運行結果:
在這裏插入圖片描述
在這裏插入圖片描述

實現基於 Linux 的多線程 Web 服務器端

Linux 下的 Web 服務器端與上述示例不同, 將使用標準 I/O 函數. 在此列出的目的的主要是爲了讓各位多複習各種知識點, 沒有任何特殊含義.
在這裏插入圖片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define BUF_SIZE 1024
#define SMALL_BUF 100

void *request_handler(void *arg);
void send_data(FILE *fp, char *ct, char *file_name);
char *content_type(char *file);
void send_error(FILE* fp);
void error_hangling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int clnt_adr_size;
    char buf[BUF_SIZE];
    pthread_t t_id;
    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_hangling("listen() error");
    }

    if (listen(serv_sock, 20) == -1)
    {
        error_hangling("listen() error");
    }

    while (1)
    {
        clnt_adr_size = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_sock, &clnt_adr_size);
        printf("Connection Request : %s:%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port));
        pthread_create(&t_id, NULL, request_handler, &clnt_sock);
        pthread_detach(t_id);
    }
    
    close(serv_sock);
    return 0;
}

void *request_handler(void *arg)
{
    int clnt_sock = *((int *)arg);
    char req_line[SMALL_BUF];
    FILE* clnt_read;
    FILE* clnt_write;

    char method[10];
    char ct[15];
    char file_name[30];

    clnt_read = fdopen(clnt_sock, "r");
    clnt_write = fdopen(dup(clnt_sock), "w");
    fgets(req_line, SMALL_BUF, clnt_read);
    if (strstr(req_line, "HTTP/") == NULL)
    {
        send_error(clnt_write);
        fclose(clnt_read);
        fclose(clnt_write);
    }

    strcpy(method, strtok(req_line, " /"));
    strcpy(file_name, strtok(NULL, " /"));
    strcpy(ct, content_type(file_name));
    if (strcmp(method, "GET") != 0)
    {
        send_error(clnt_write);
        fclose(clnt_read);
        fclose(clnt_write);
        return;
    }

    fclose(clnt_read);
    send_data(clnt_write, ct, file_name);
}

void send_data(FILE *fp, char *ct, char *file_name)
{
    char protocol[] = "HTTP/1.0 200 OK\r\n";
    char server[] = "Server:Linux Web Server \r\n";
    char cnt_len[] = "Content-length: 2048\r\n";
    char cnt_type[SMALL_BUF];
    char buf[BUF_SIZE];
    FILE* send_file;

    sprintf(cnt_type, "Content-type:%s\r\n\r\n", ct);
    send_file = fopen(file_name, "r");
    if (send_file == NULL)
    {
        send_error(fp);
        return;
    }

    /* 傳輸頭文件信息 */
    fputs(protocol, fp);
    fputs(server, fp);
    fputs(cnt_len, fp);
    fputs(cnt_type, fp);

    /* 傳輸請求數據 */
    while (fgets(buf, BUF_SIZE, send_file) != NULL)
    {
        fputs(buf, fp);
        fflush(fp);
    }

    fflush(fp);
    fclose(fp);
}
char *content_type(char *file)
{
    char extension[SMALL_BUF];
    char file_name[SMALL_BUF];
    strcpy(file_name, file);
    strtok(file_name, ".");
    strcpy(extension, strtok(NULL, "."));

    if (!strcmp(extension, "html") || !strcmp(extension, "htm"))
    {
        return "text/html";
    }
    else
    {
        return "text/plain";
    }
    
}
void send_error(FILE* fp)
{
    char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
    char server[] = "Server:Linux Web Server \r\n";
    char cnt_len[] = "Content-length:2048\r\n";
    char cnt_type[] = "Content-type:text/html\r\n\r\n";
    char content[] = "<html><head><title>NETWORK</title></head>"
    "<body><font size=+5><br>發生錯誤! 查看請求文件名和連接請求方式! "
    "</font></body></html>";

    fputs(protocol, fp);
    fputs(server, fp);
    fputs(cnt_len, fp);
    fputs(cnt_type, fp);
    fflush(fp);
}

void error_hangling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

運行結果失敗, 無法訪問, 不知原因, 等老師教到, 快了,

結語:

我最近 買了實體書 , 先看完電子版(先過一遍知識點, 我沒有這麼牛逼能記住, 可以複習的嘛! ), 再買實體版 , 避免它又成爲收藏書沒啥用, 這本書非常適合新手

你可以下面這個網站下載這本書<TCP/IP網絡編程>
https://www.jiumodiary.com/

時間: 2020-06-23

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章