基于C語言的簡單HTTP Web服務器實現

1. 概述

本案例使用C語言實現了一個簡單的HTTP服務器,能夠處理客戶端的GET請求,并返回靜態文件(如HTML、圖片等)。在此案例中案例,我們主要使用的知識點有:

  • Socket編程:基于TCP協議的Socket通信。

  • HTTP協議:HTTP請求和響應的基本格式。

  • 多線程:使用多線程處理客戶端請求。

  • 文件操作:讀取本地文件并發送給客戶端。

  • MIME類型:根據文件擴展名設置正確的Content-Type


2. 主要知識點

2.1 Socket編程

Socket是網絡通信的基礎,本案例使用Windows下的Socket API(winsock2.h)實現TCP通信。主要函數包括:

  • WSAStartup:初始化Winsock庫。

  • socket:創建套接字。

  • bind:綁定套接字到本地地址和端口。

  • listen:監聽客戶端連接。

  • accept:接受客戶端連接。

  • send/recv:發送和接收數據。

  • closesocket:關閉套接字。

2.2 HTTP協議

HTTP是一種無狀態的請求-響應協議。本案例實現了HTTP/1.0的基本功能:

  • 請求格式

  • GET /path HTTP/1.0
    Host: 127.0.0.1:8080

    響應格式

  • HTTP/1.0 200 OK
    Content-Type: text/html<html>...</html>

    2.3 多線程

    為了支持多個客戶端同時連接,本案例使用Windows的CreateThread函數創建新線程處理每個客戶端請求。

2.4 文件操作

服務器需要讀取本地文件并發送給客戶端。本案例使用fopenfread等函數操作文件。

2.5 MIME類型

根據文件擴展名設置正確的Content-Type,例如:

  • .html?->?text/html

  • .jpg?->?image/jpeg

  • .png?->?image/png

3. 實現思路

3.1 服務器啟動流程

  1. 初始化Winsock庫:調用WSAStartup初始化網絡通信。

  2. 創建套接字:調用socket創建TCP套接字。

  3. 綁定地址和端口:調用bind綁定套接字到本地地址和端口。

  4. 監聽連接:調用listen開始監聽客戶端連接。

  5. 接受連接:調用accept接受客戶端連接,并為每個連接創建新線程。

3.2 處理客戶端請求

  1. 讀取請求行:從客戶端讀取HTTP請求的第一行,解析請求方法和URL。

  2. 解析URL:根據URL確定請求的文件路徑。

  3. 檢查文件是否存在:使用stat函數檢查文件是否存在。

  4. 發送響應頭:根據文件類型設置Content-Type,并發送HTTP響應頭。

  5. 發送文件內容:讀取文件內容并發送給客戶端。

3.3 多線程處理

每個客戶端連接由一個獨立的線程處理,避免阻塞主線程。

4. 代碼細節分析

4.1 初始化網絡和創建套接字

int startup(unsigned short* port) {WSADATA wsaData;int ret = WSAStartup(MAKEWORD(1, 1), &wsaData);if (ret) {printf("初始化網絡通信失敗\n");return -1;}int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (server_sock == INVALID_SOCKET) {error_die("socket()失敗");}// 設置端口復用int opt = 1;setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));// 綁定地址和端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(*port);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);ret = bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));if (ret == SOCKET_ERROR) {error_die("bind()失敗");}// 動態分配端口if (*port == 0) {int len = sizeof(server_addr);getsockname(server_sock, (struct sockaddr*)&server_addr, &len);*port = ntohs(server_addr.sin_port);}// 監聽連接ret = listen(server_sock, 5);if (ret == SOCKET_ERROR) {error_die("listen()失敗");}return server_sock;
}

4.2 讀取HTTP請求

int get_line(int sock, char* buf, int size) {int i = 0;char c = 0;while (i < size - 1 && c != '\n') {int n = recv(sock, &c, 1, 0);if (n <= 0) break;if (c == '\r') {n = recv(sock, &c, 1, MSG_PEEK);if (n > 0 && c == '\n') recv(sock, &c, 1, 0);c = '\n';}buf[i++] = c;}buf[i] = '\0';return i;
}

4.3 處理客戶端請求

DWORD WINAPI accept_request(LPVOID arg) {int client = (SOCKET)arg;char buf[1024], method[255], url[255], path[255];int numchars = get_line(client, buf, sizeof(buf));// 解析請求方法和URLsscanf(buf, "%s %s", method, url);// 檢查請求方法if (_stricmp(method, "GET") && _stricmp(method, "POST")) {unimplemented(client);return 0;}// 構造文件路徑sprintf(path, "htdocs%s", url);if (path[strlen(path) - 1] == '/') strcat(path, "index.html");// 檢查文件是否存在struct stat st;if (stat(path, &st) == -1) {while ((numchars > 0) && strcmp("\n", buf))numchars = get_line(client, buf, sizeof(buf));not_found(client);} else {if ((st.st_mode & S_IFMT) == S_IFDIR) strcat(path, "/index.html");server_file(client, path);}closesocket(client);return 0;
}

4.4 發送文件內容

void cat(int client_sock, FILE* resource) {char buf[4096];int count = 0;while (1) {int ret = fread(buf, sizeof(char), sizeof(buf), resource);if (ret <= 0) break;send(client_sock, buf, ret, 0);count += ret;}printf("總共發送了%d字節\n", count);
}

5. 總結

? ? ? 通過這個案例,我們實現了一個簡單的HTTP服務器,支持靜態文件的請求和響應。核心知識點包括Socket編程、HTTP協議、多線程和文件操作。這個案例是學習網絡編程的入門項目,后續可以擴展支持更多功能,如POST請求、動態內容生成等。

靜態資源的訪問位置記得改成自己的,這是我存放的靜態資源位置。

如果edge瀏覽器訪問不了可以多刷新幾次,或者使用谷歌等其他瀏覽器。

如果通過路徑訪問的資源不存在,則返回404信息

案例完整代碼如下:

#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")#include <string.h>
#include <ctype.h>
#include <sys/stat.h> //訪問文件的屬性#define PRINTF(str) printf("[%s - %d] "#str" = %s\r\n",__func__,__LINE__,str);#define ISspace(x) isspace((int)(x))void error_die(const char* msg) {// 打印錯誤信息printf("%s\n", msg);// 退出程序exit(1);
}// 初始化網絡并創建服務端的套接字
int startup(unsigned short* port) {// 1. 網絡通信初始化WSADATA wsaData;int ret = WSAStartup(MAKEWORD(1, 1), &wsaData);if (ret) {printf("初始化網絡通信失敗\n");return -1;}// 2. 創建服務端的套接字int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (server_sock == INVALID_SOCKET) {error_die("socket()失敗");}// 設置端口號可復用int opt = 1;ret = setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));if (ret == -1) {error_die("setsockopt()失敗");}// 配置服務端套接字地址struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(struct sockaddr_in)); // 清空結構體server_addr.sin_family = AF_INET; // 地址族,這里是IPv4server_addr.sin_port = htons(*port); // 端口號server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址,這里是任意IP// 綁定套接字與服務端地址ret = bind(server_sock, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));if (ret == SOCKET_ERROR) {error_die("bind()失敗");}// 動態分配一個端口號if (*port == 0) {int len = sizeof(struct sockaddr);getsockname(server_sock, (struct sockaddr*)&server_addr, &len);*port = ntohs(server_addr.sin_port);}// 創建監聽隊列ret = listen(server_sock, 5);if (ret == SOCKET_ERROR) {error_die("listen()失敗");}return server_sock; // 返回server_sock而不是0
}//返回從套接字讀取一行信息,并把數據存入buf中
int get_line(int sock, char* buf, int size) {int i = 0;int n;char c = 0;while (i < size - 1 && c != '\n') {n = recv(sock, &c, 1, 0);if (n <= 0) {// 連接關閉或出錯,結束循環break;}if (c == '\r') {// 查看下一個字符是否是'\n'char next_char;n = recv(sock, &next_char, 1, MSG_PEEK);if (n > 0 && next_char == '\n') {// 讀取并消耗'\n'recv(sock, &next_char, 1, 0);}c = '\n'; // 統一轉換為換行符}buf[i++] = c;if (c == '\n') {break; // 換行符結束行讀取}}buf[i] = '\0'; // 添加字符串終止符return i; // 返回讀取的字符數(不含終止符)
}void unimplemented(int client_sock) {// 發送501響應char buf[1024];strcpy(buf, "HTTP/1.0 501 Method Not Implemented\r\n");send(client_sock, buf, strlen(buf), 0);strcpy(buf, "Server: RockHTTP/0.1 libcurl/7.22.0\r\n");send(client_sock, buf, strlen(buf), 0);strcpy(buf, "Content-Type: text/html\r\n");send(client_sock, buf, strlen(buf), 0);strcpy(buf, "\r\n");send(client_sock, buf, strlen(buf), 0);// 發送501頁面char unimplemented_html[] = "<HTML><HEAD><TITLE>Method Not Implemented</TITLE></HEAD><BODY><H1>501 Method Not Implemented</H1></BODY></HTML>";send(client_sock, unimplemented_html, strlen(unimplemented_html), 0);
}void not_found(int client_sock) {// 發送404響應char buf[1024];strcpy(buf, "HTTP/1.0 404 Not Found\r\n");send(client_sock, buf, strlen(buf), 0);strcpy(buf, "Server: RockHTTP/0.1 libcurl/7.22.0\r\n");send(client_sock, buf, strlen(buf), 0);sprintf(buf, "Content-Type: text/html\r\n");send(client_sock, buf, strlen(buf), 0);strcpy(buf, "\r\n");send(client_sock, buf, strlen(buf), 0);// 發送404頁面char not_found_html[] = "<HTML><HEAD><TITLE>Not Found</TITLE></HEAD><BODY><H1>404 Not Found</H1></BODY></HTML>";send(client_sock, not_found_html, strlen(not_found_html), 0);
}const char* get_content_type(const char* path) {const char* last_dot = strrchr(path, '.');if (last_dot) {if (strcmp(last_dot, ".html") == 0 || strcmp(last_dot, ".htm") == 0) {return "text/html";}else if (strcmp(last_dot, ".jpg") == 0 || strcmp(last_dot, ".jpeg") == 0) {return "image/jpeg";}else if (strcmp(last_dot, ".png") == 0) {return "image/png";}else if (strcmp(last_dot, ".gif") == 0) {return "image/gif";}else if (strcmp(last_dot, ".css") == 0) {return "text/css";}else if (strcmp(last_dot, ".js") == 0) {return "application/javascript";}}return "text/plain";
}void headers(int client_sock, const char* path) {// 發送HTTP頭部                         char buf[1024];strcpy(buf, "HTTP/1.0 200 OK\r\n");send(client_sock, buf, strlen(buf), 0);sprintf(buf, "Content-Type: %s\r\n", get_content_type(path));send(client_sock, buf, strlen(buf), 0);strcpy(buf, "\r\n");send(client_sock, buf, strlen(buf), 0);
}void cat(int client_sock, FILE* resource) {char buf[4096];int count = 0;while (1) {int ret = fread(buf, sizeof(char), sizeof(buf), resource);if (ret <= 0) {break;}send(client_sock, buf, ret, 0);count += ret;}printf("總共發送了%d字節\n", count);
}void server_file(int client_sock, const char* fileName) {char numchars = 1;char buf[1024];// 將請求包剩余數據讀完,直到遇到換行符while (numchars > 0 && strcmp(buf, "\n")) {numchars = get_line(client_sock, buf, sizeof(buf));PRINTF(buf);}// 發送文件內容FILE* resource = fopen(fileName, "rb"); // 以二進制模式打開文件if (resource == NULL) {printf("文件打開失敗\n");not_found(client_sock);}else {// 返回數據給瀏覽器headers(client_sock, fileName);// 發送請求的資源cat(client_sock, resource);printf("資源發送完畢\n");}fclose(resource);
}// 處理客戶端的連接請求 
DWORD WINAPI accept_request(LPVOID arg) {char buf[1024];int numchars;char method[255];char url[255];char path[255];size_t i, j;struct stat st;int cgi = 0;int client = (SOCKET)arg;// 讀取一行信息numchars = get_line(client, buf, sizeof(buf));printf("read %d bytes of data from client\n", numchars);PRINTF(buf);char* query_string = NULL;i = 0; j = 0;while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) {method[i] = buf[j];i++;j++;}method[i] = 0;  // 解析后, method的值:"GET"或者"POST"PRINTF(method);// 判斷是否為GET或POST請求if (_stricmp(method, "GET") && _stricmp(method, "POST")) {unimplemented(client);return 0;}// 判斷是否為CGI請求if (_stricmp(method, "POST") == 0)cgi = 1;// 解析URL,獲得資源路徑i = 0;while (ISspace(buf[j]) && (j < sizeof(buf))) // 跳過buff中的空格j++;while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) // 獲得資源url 比如 / 或者 /images/head.png{url[i] = buf[j];i++; j++;}url[i] = '\0';PRINTF(url);sprintf(path, "htdocs%s", url);if (path[strlen(path) - 1] == '/')strcat(path, "index.html"); // 如果路徑以"/"結尾,則認為是目錄,拼接上默認的HTML文件名PRINTF(path);struct stat status;// 檢查訪問的資源是否存在if (stat(path, &st) == -1) {  // stat獲取指定文件的屬性信息// 如果不能訪問它的屬性信息,那么這個文件就不存在// 此時,就需要把這個請求報文,讀完!雖然已經沒有用了,但是也要把這個報文讀完while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */numchars = get_line(client, buf, sizeof(buf));not_found(client);}else {// 如果瀏覽器的地址輸入:http://127.0.0.1:8000/movies // 如果movies是目錄,就默認訪問這個目錄下的index.htmlif ((st.st_mode & S_IFMT) == S_IFDIR)strcat(path, "/index.html");server_file(client, path);}closesocket(client);return 0;
}int main() {// httpd默認的端口是80,這里指定了8000端口,也可以使用其它端口unsigned short port = 8080;// 初始化網絡,并使用指定端口來創建服務端的套接字int server_sock = startup(&port);printf("httpd running on port %d\n", port);while (1) {// 等待客戶端的連接struct sockaddr_in client_addr;int client_len = sizeof(struct sockaddr);// 阻塞式等待客戶端的連接int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);if (client_sock == -1) {error_die("accept"); // 打印錯誤信息并結束}// 創建一個線程來處理客戶端請求DWORD threadId = 0;HANDLE handleFirst = CreateThread(NULL, 0, accept_request, (void*)client_sock, 0, &threadId);if (handleFirst == NULL) {error_die("CreateThread()失敗");}}return 0;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/73260.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/73260.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/73260.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

大型語言模型與強化學習的融合:邁向通用人工智能的新范式

1. 引言 大型語言模型&#xff08;LLM&#xff09;在自然語言處理領域的突破&#xff0c;展現了強大的知識存儲、推理和生成能力&#xff0c;為人工智能帶來了新的可能性。強化學習&#xff08;RL&#xff09;作為一種通過與環境交互學習最優策略的方法&#xff0c;在智能體訓…

langchain--LCEL

文章目錄 介紹優勢運行接口 介紹 LCEL的全稱是Lang Chain Expression Language。其實他的用處就是使用“|”運算符鏈接LangChain應用的各個組件。 是一種聲明式的方法來鏈接Langchain組件。LCEL從第一天起就被設計為支持將原型投入生產&#xff0c;無需代碼更改&#xff0c;從…

PyQt基礎——簡單的窗口化界面搭建以及槽函數跳轉

一、代碼實現 import sysfrom PyQt6.QtGui import QPixmap from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QLineEdit, QMessageBox from PyQt6.uic import loadUi from PyQt6.QtCore import Qtclass LoginWindow(QWidget):def __init__(self):sup…

Android 11.0 監聽某個app啟動或者退出功能實現

1.前言 在進行11.0的系統定制開發中,在某些app的定制過程中,需要知道某個app的啟動記錄和退出記錄, 所以就需要監聽某個app的啟動和退出的過程,需要在Activity的生命周期中來實現監聽功能 2.監聽某個app啟動或者退出功能實現的核心類 frameworks\base\core\java\android…

再談 Multiscale deformable attention

文章目錄 DCN 可變形卷積單尺度 deformable attention多尺度&#xff08;multiscale&#xff09; deformable attention精華代碼&#xff1a;deformbale attentionattention 計算&#xff1a;獲取不同尺度參考點&#xff1a; DCN 可變形卷積 deformable attention 靈感來源可變…

Java 大視界 -- Java 大數據在智慧文旅虛擬導游與個性化推薦中的應用(130)

&#x1f496;親愛的朋友們&#xff0c;熱烈歡迎來到 青云交的博客&#xff01;能與諸位在此相逢&#xff0c;我倍感榮幸。在這飛速更迭的時代&#xff0c;我們都渴望一方心靈凈土&#xff0c;而 我的博客 正是這樣溫暖的所在。這里為你呈上趣味與實用兼具的知識&#xff0c;也…

多源 BFS_多源最短路(十八)542. 01 矩陣 中等 超級源點思想

542. 01 矩陣 給定一個由 0 和 1 組成的矩陣 mat &#xff0c;請輸出一個大小相同的矩陣&#xff0c;其中每一個格子是 mat 中對應位置元素到最近的 0 的距離。 兩個相鄰元素間的距離為 1 。 示例 1&#xff1a; 輸入&#xff1a;mat [[0,0,0],[0,1,0],[0,0,0]] 輸出&#xff…

Ubuntu24.04 LTS 版本 Linux 系統在線和離線安裝 Docker 和 Docker compose

一、更換軟件源并更新系統 在 Ubuntu 24.04 LTS 中&#xff0c;系統引入了全新的軟件源配置格式。現在的源配置文件內容更加結構化且清晰&#xff0c;主要包含了軟件類型 (Types)、源地址 (URIs)、版本代號 (Suites) 以及組件 (Components) 等信息。 # cat /etc/apt/sources.li…

c++介紹智能指針 十二(2)

智能指針share_ptr,與unique_ptr不同&#xff0c;多個shar_ptr對象可以共同管理一個指針&#xff0c;它們通過一個共同的引用計數器來管理指針。當一個智能指針對象銷毀時&#xff0c;計數器減一。當計數器為0時&#xff0c;會將所指向的內存對象釋放。 #include<memory>…

react和vue 基礎使用對比

1.實現功能&#xff08;ts&#xff09; 0.基礎屬性使用 1.組件直接的通信 2.useState 動態修改值 3.循環遍歷功能 4.實現類型vue 的 watch &#xff0c;filter&#xff0c;computed 屬性功能 5.實現類似vue2的生命周期 5.類型vue v-if功能的實現 2.文件結構圖 3.具體代碼 in…

深度學習 常見優化器

一、基礎優化器 隨機梯度下降&#xff08;SGD&#xff09; ? 核心&#xff1a;?θJ(θ) η * ?θJ(θ) ? 特點&#xff1a;學習率固定&#xff0c;收斂路徑震蕩大 ? 適用場景&#xff1a;簡單凸優化問題 ? 改進方向&#xff1a;動量加速 二、動量系優化器 2. SGD with…

監控快手關注列表更新以及去視頻水印視頻

def printData(self):if len(self.UpdateDataList) > 0:self.UpdateDataList sorted(self.UpdateDataList, keylambda x: x[minutes]) # 先更新的在前sucess 0for index, video in enumerate(self.UpdateDataList):minutes video[minutes]if minutes > self.updateIn…

前端 JavaScript 中快速發起多個下載請求時,解決瀏覽器的并發下載連接限制

為什么會漏掉鏈接&#xff1f; 當你在前端 JavaScript 中快速發起多個下載請求時&#xff0c;瀏覽器可能無法同時處理所有請求&#xff0c;導致一些請求被忽略。這通常與瀏覽器的并發連接限制有關&#xff0c;例如 Chrome 可能限制每秒下載 10 個文件。 如何避免漏掉鏈接&…

如何修改桌面圖標——文件夾圖標(Windows 10)

修改文件夾圖標 EX&#xff1a;新建文件夾&#xff0c;程序創建文件夾等 修改桌面文件夾圖標&#xff0c;打開右鍵菜單功能項&#xff0c;點擊“屬性” 在屬性窗口頁面找到并單擊自定義&#xff0c;然后點擊“更改圖標” 從列表中選擇喜歡的圖標&#xff0c;或點擊瀏覽選擇個…

LiveCommunicationKit OC 實現

一、實現效果: ? LiveCommunicationKit?是蘋果公司在iOS 17.4、watchOS 10.4和visionOS 1.1中引入的一個新框架,旨在優化VoIP通話的交互體驗。該框架提供了與

基于Bert模型的增量微調3-使用csv文件訓練

我們使用weibo評價數據&#xff0c;8分類的csv格式數據集。 一、創建數據集合 使用csv格式的數據作為數據集。 1、創建MydataCSV.py from torch.utils.data import Dataset from datasets import load_datasetclass MyDataset(Dataset):#初始化數據集def __init__(self, s…

flowable新增或修改單個任務的歷史變量

簡介 場景&#xff1a;對歷史任務進行關注&#xff0c;所以需要修改流程歷史任務的本地變量 方法包含2個類 1&#xff09;核心方法&#xff0c;flowable command類&#xff1a;HistoricTaskSingleVariableUpdateCmd 2&#xff09;執行command類&#xff1a;BpmProcessCommandS…

Netty基礎—4.NIO的使用簡介一

大綱 1.Buffer緩沖區 2.Channel通道 3.BIO編程 4.偽異步IO編程 5.改造程序以支持長連接 6.NIO三大核心組件 7.NIO服務端的創建流程 8.NIO客戶端的創建流程 9.NIO優點總結 10.NIO問題總結 1.Buffer緩沖區 (1)Buffer緩沖區的作用 (2)Buffer緩沖區的4個核心概念 (3)使…

python元組(被捆綁的列表)

元組&#xff08;tuple&#xff09; 1.元組一旦形成就不可更改,元組所指向的內存單元中內容不變 定義&#xff1a;定義元組使用小括號&#xff0c;并且使用逗號進行隔開&#xff0c;數據可以是不同的數據類型 定義元組自變量&#xff08;元素&#xff0c;元素&#xff0c;元素…

輸入:0.5元/百萬tokens(緩存命中)或2元(未命中) 輸出:8元/百萬tokens

這句話描述了一種 定價模型&#xff0c;通常用于云計算、API 服務或數據處理服務中&#xff0c;根據資源使用情況&#xff08;如緩存命中與否&#xff09;來收費。以下是對這句話的詳細解釋&#xff1a; 1. 關鍵術語解釋 Tokens&#xff1a;在自然語言處理&#xff08;NLP&…