基于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.mzph.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&…