HTTP协议之编写简单的Web服务器 web服务基于http协议
myzbx 2024-12-17 15:12 31 浏览
HTTP即超文本传输协议(Hypertext Transfer Protocol),是Web通信所使用的协议。它是基于TCP/IP实现的协议,所以需要先了解TCP通信,本篇将使用TCP来写一个简单的Web服务器端,它可以响应浏览器的访问。
通信需要服务端和客户端,在这里浏览器就属于客户端,当访问一个网页时,浏览器内部会创建套接字和服务器进行通信。服务器会响应请求返回一些HTML格式的数据给浏览器,浏览器来把这些HTML数据解析成我们看到的漂亮的页面。
当我们在浏览器的地址栏上敲下一个域名地址后,浏览器会先通过默认DNS服务器获取该域名对应的IP地址,然后向服务器发送请求,请求有一定的标准,分为:
- 请求行
- 消息头
- 空行
- 消息体
现在来随便访问一个网址,这里使用的是firefox浏览器,按Ctrl+Shift+E可以查看网络请求:
左边对应的是浏览器对服务器发出的请求,右边对应的是该条请求相关的信息。
我们先看左边的第一条,这里的请求方式是GET,表示想从服务器获取文件,获取的文件目录是/。然后服务器响应请求,发回了状态码302,表示Found重定向,我们本来访问的是www.bing.com,现在被重定向到了https://cn.bing.com/。这个重定向地址是通过响应头的location指定的,可以在右边看到。
https是加了SSL/TLS的协议,在需要安全的环境下,比如发送银行卡,身份信息等地方都会使用。只使用http这些信息很容易被窃听,SSL/TLS会对请求和响应的信息进行加密解密操作,保证数据安全。
接着同样是一些GET请求用于从服务器获取数据,状态码200 OK表示请求已经成功。还有常见的404 Not Found,表示找不到客户端请求的资源,这种情况就把链接转移到404错误页。现在来把对应的请求整理如下:
请求行:
GET?/?HTTP/2.0
消息头:
Accept:text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8
Accept-Encoding:gzip,?deflate,?br
Accept-Language:zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Connection:keep-alive
Cookie:_EDGE_V=1;?MUID=3802FC7ABA5D6F…ndefined;?_UR=OMD=13190641321
Host:cn.bing.com
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0?(Windows?NT?10.0;?…)?Gecko/20100101?Firefox/64.0
空行:
消息体:
请求行为一行数据,其中包含着请求方式,请求文件,HTTP版本。
这里的请求方式为GET。文件为/,表示根目录,这种情况一般都会被重定向到别的页面上,一般好多服务器都会自动检查有没有index.html/index.php/…等等,发现有就转到这些页面。最后是使用的HTTP版本,为2.0版本。
主要来看消息头。
- Accept:表示客户端可以处理的文件类型
- Accept-Encoding:表示客户端能理解的内容的编码方式
- Accept-Language:表示支持的语言,比如此处支持中文,英文等等
- Connection:这个决定着当前事务完成后,是否会关闭网络连接。此处指定为keep-alive,表示持久,不会关闭。指定close会关闭。
- Cookie:由于服务器响应客户端的请求后,需要断开连接去接收下一个请求,所以服务端无法记住客户端的状态,哪怕下一次再连接服务器也无法辩认为原先访问的。所以就有了Cookie,当客户端连接后服务器可以通过Set-Cookie给客户端设置一些信息,第二次连接时这个Cookie就包含的有服务器给此客户端设置的Cookie值,这样服务器就能识别客户端了。例如那些密码记录,购物车清单啊,都有用到。
- Host:表示主机域名
- Upgrade-Insercure-Request:表示客户端优先选择加密及带有身份验证的响应,并且它可以成功处理 upgrade-insecure-requests CSP 指令。
- user-Agent:用户代理,包含着我们访问所使用的应用类型,操作系统,版本号等等信息。
空行是为了区分前面的信息和消息体的,消息体是在POST请求时提交给服务器的,此处没有POST请求,所以也就没有消息体了。
现在来看响应消息的格式:
- 状态行
- 消息头
- 空行
- 消息体
把响应的内容整理如下:
状态行:
HTTP/2.0?200?OK
消息头:
cache-control:private,?max-age=0
content-encoding:br
content-type:text/html;?charset=utf-8
date:Sun,?30?Dec?2018?11:11:10?GMT
p3p:CP="NON?UNI?COM?NAV?STA?LOC?CURa?DEVa?PSAa?PSDa?OUR?IND"
set-cookie:SNRHOP=I=&TS=;?domain=.bing.com;?path=/
strict-transport-security:max-age=31536000;?includeSubDomains;?preload
vary:Accept-Encoding
X-Firefox-Spdy:h2
x-msedge-ref:Ref?A:?7F437C20D4F049CB86705CA…6?Ref?C:?2018-12-30T11:11:10Z
空行:
消息体:
<html>
....
</html>
第一行用于表示状态行,为HTTP版本 + 状态码。
在消息头中设置了内容编码,类型,日期,cookie等等信息,这里就不细说了。服务器需要向客户端返回数据,这里的数据就在消息体中进行发送的,这里返回了主页的html数据。
现在对这些基本知识有了了解,就可以来完成一个简单的Web服务器。
先创建一个WebServer类,作为网络服务器:
#pragma?once
#include?<string>
#include?<WinSock2.h>
#pragma?comment(lib,?"ws2_32.lib")
class?WebServer
{
public:
????WebServer();
????~WebServer();
????void?RespondRequest();??//响应请求
private:
????void?InitSock();??//初始化Socket
????void?RequestHandler();??//处理请求的线程
????void?SendData(const?std::string&?ct,?const?std::string&?fileName);??//发送数据
????void?SendNotFound();??//找不到指定文件时,发送404?Not?Found页面
private:
????WSADATA?m_wsaData;
????SOCKET?m_servSock,?m_clntSock;
????SOCKADDR_IN?m_servAddr,?m_clntAddr;
};
关于TCP的东西就不再多说了,相信大家已经很熟悉了。这里只需开放一个接口供用户调用,其它由我们内部使用。
在实现文件中,还需要包含以下文件:
#include?"WebServer.h"
#include?<iostream>
#include?<cassert>??//断言
#include?<future>??//线程用
#include?<fstream>??//文件操作用
#pragma?warning(disable:4996)??//忽略一些警告
const?int?CONTENT_SIZE?=?2048;??//内容大小
const?int?BUF_SIZE?=?100;??//缓冲区大小
先来看InitSock()函数,在这里就是TCP的一些绑定,监听操作:
void?WebServer::InitSock()
{
????//初始化Winsock库
int ret = WSAStartup(MAKEWORD(2, 2), &m_wsaData);
????assert(ret?==?0);
????//创建服务器套接字
????m_servSock?=?socket(PF_INET,?SOCK_STREAM,?0);
????//设置地址信息
????memset(&m_servAddr,?0,?sizeof(m_servAddr));
????m_servAddr.sin_family?=?AF_INET;
????m_servAddr.sin_addr.S_un.S_addr?=?htonl(INADDR_ANY);
????m_servAddr.sin_port?=?htons(8000);
????//绑定地址信息
ret = bind(m_servSock, (SOCKADDR *)&m_servAddr, sizeof(m_servAddr));
????assert(ret ?!=?SOCKET_ERROR);
???
//监听
ret = listen(m_servSock, 5);
????assert(ret?!=?SOCKET_ERROR);
}
初始化信息在构造函数中调用:
WebServer::WebServer()
{
????InitSock();
}
同时需要在析构函数中关闭释放:
WebServer::~WebServer()
{
????closesocket(m_servSock);
????WSACleanup();
}
大家可能发现这里只关闭了服务器套接字,这是因为前面说过,HTTP是无状态的,它响应了一个客户端后就会断开来响应别的请求,所以这里不需要关闭,而是在响应的函数中关闭。
现在来看响应请求的函数RespondRequest():
void?WebServer::RespondRequest()
{
????for?(;;)
????{
????????int?nClntAddrSize?=?sizeof(m_clntAddr);
????????//接收客户端连接
????????m_clntSock?=?accept(m_servSock,?(SOCKADDR?*)&m_clntAddr,?&nClntAddrSize);
????????//获取客户端信息
????????char*?szClntAddr?=?inet_ntoa(m_clntAddr.sin_addr);
????????int?nClntPort?=?ntohs(m_clntAddr.sin_port);
????????//打印信息
????????char?szBuf[BUF_SIZE]?=?{?0?};
????????snprintf(szBuf,?BUF_SIZE,?"Connection?Request:?%s?%d",?szClntAddr,?nClntPort);
????????std::cout?<<?szBuf?<<?std::endl;
????????//开启线程,发送数据
????????auto?fu?=?std::async(std::launch::async,?std::bind(&WebServer::RequestHandler,?this));
????}
}
这里一直循环着来接收客户端的请求,当有客户端连接后,读取客户端的地址和端口关打印出来。关于发送数据专门开启一个线程交给RequestHandler处理,因为类成员函数其实都隐含了一个this指针,所以还得传入this,这里的std::bind会把成员函数和this绑定起来,返回一个函数对象。
接着来看处理数据的线程函数:
void?WebServer::RequestHandler()
{
????char?szContent[CONTENT_SIZE]?=?{?0?};????//接收到的内容
????recv(m_clntSock,?szContent,?CONTENT_SIZE,?0);??//接收
????//打印接收到的内容
????std::cout?<<?"-------------------\n"?<<?szContent?<<?std::endl;
????std::string?content(szContent);
????//检测是否使用的是HTTP协议
????int?protocol?=?content.find("HTTP/");
????if?(protocol?==?std::string::npos)
????{
????????std::cerr?<<?"Error?Request!"?<<?std::endl;
????????closesocket(m_clntSock);
????????return;
????}
????int?loc?=?content.find_first_of("/");
????std::string?method?=?content.substr(0,?loc);??//解析请求方式
????std::string?fileName?=?content.substr(loc?+?1,?protocol?-?loc?-?1);??//解析请求文件名
????std::string?contentType;
????loc?=?fileName.find_first_of(".")?+?1;
????std::string?type?=?fileName.substr(loc);
????//解析类型
????if?(type.compare("html?")?==?0?||?type.compare("htm?")?==?0)
????????contentType?=?"text/html";??//html格式
????else?
????????contentType?=?"text/plain";??//纯文本格式
????SendData(contentType,?fileName);??//发送数据
}
可以先测试下能否收到客户端的请求,前面指定的端口为8000,所以使用浏览器访问localhost:8000,接收到的数据如图:
当成功等到这些信息后,需要解析出它的请求方式,请求文件名,通过请求文件名再判断出请求类型,若是html,请求类型则为text/html。若不是,设置为text/plain。当给服务器返回时若是text/plain,服务器就会把收到的内容当作文本显示出来,若是html,则会是解析出显示。
再来看发送数据之前,首先来新建两个HTML文件,一个为正常请求的文件index.html,一个为失踪网页404.html,并将它们放到工程目录下。
<!--?index.html?-->
<html>
<head>
????<title>My?Web?Server</title>
</head>
<body?style="background:#0f0;">
????<div?style="text-align:center;?font-size:50px;?margin-top:300px"?>Request?Successful!</div>
</body>
</html>
<!-- 404.html -->
<html>
<head>
????<title>Not?Found</title>
</head>
<body?style="background:#f00;">
????<div?style="text-align:center;?font-size:100px;?margin-top:300px">404!</div>
????<div?style="text-align:center;?font-size:50px;"?>Oops!?That?page?can't?be?found.</div>
</body>
</html>
然后继续来看发送数据:
void?WebServer::SendData(const?std::string&?ct,?const?std::string&?fileName)
{
????std::string?protocol????=?"HTTP/1.0?200?OK\r\n";??//状态行
????std::string?servName????=?"Server:My?Web?Server\r\n";??//服务器名称
????std::string?contentLen??=?"Content-length:2048\r\n";??//内容长度
????std::string?contentType?=?"Content-type:"?+?ct?+?"\r\n\r\n";??//内容类型
????//打开请求的文件
????std::ifstream?sendFile(fileName);
????if?(!sendFile.is_open())
????{
????????//打开失败,说明当前路径无此文件,发送Not?Found
????????SendNotFound();
????????return;
????}
????//发送状态行,消息头
????send(m_clntSock,?protocol.c_str(),?protocol.size(),?0);
????send(m_clntSock,?servName.c_str(),?servName.size(),?0);
????send(m_clntSock,?contentLen.c_str(),?contentLen.size(),?0);
????send(m_clntSock,?contentType.c_str(),?contentType.size(),?0);
????//发送消息体
????char?buf[CONTENT_SIZE]?=?{?0?};
????sendFile.getline(buf,?CONTENT_SIZE);
????for?(;?!sendFile.eof();)
????{
????????send(m_clntSock,?buf,?strlen(buf),?0);
????????sendFile.getline(buf,?CONTENT_SIZE);
????}
????sendFile.close();
????closesocket(m_clntSock);??//由HTTP协议响应后断开连接
}
开始先组织一下状态行和消息头,因为客户端请求为HTTP/1.0,所以我们也使用HTTP/1.0,状态设置为200 OK,表示成功。接着设置了服务器名,返回的内容长度,内容类型,需要注意的是在内容类型后有两个\r\n,后一个代表着空行,若是忘记了,客户端就识别不了你返回的消息体了。
接着,在目录中读取请求文件,若是读取失败,则表示请求文件不存在,那就返回404了。若存在,便将文件读取出来发送给客户端,这些内容就是消息体。
最后,需要断开与客户端的连接,继续接收其它请求,所以说HTTP协议是无状态的协议。
最后剩一个404页面,操作和发送数据就没什么两样,便不细说:
void?WebServer::SendNotFound()
{
????std::string?protocol????=?"HTTP/1.0?404?Not?Found\r\n";
????std::string?servName????=?"Server:My?Web?Server\r\n";
????std::string?contentLen??=?"Content-length:2048\r\n";
????std::string?contentType?=?"Content-type:text/html\r\n\r\n";
????std::ifstream?sendFile("404.html");
????if?(!sendFile.is_open())
????{
????????std::cout?<<?"Not?found?404.html"?<<?std::endl;
????????return;
????}
????//发送状态行,消息头
????send(m_clntSock,?protocol.c_str(),?protocol.size(),?0);
????send(m_clntSock,?servName.c_str(),?servName.size(),?0);
????send(m_clntSock,?contentLen.c_str(),?contentLen.size(),?0);
????send(m_clntSock,?contentType.c_str(),?contentType.size(),?0);
????char?buf[CONTENT_SIZE]?=?{?0?};
????sendFile.getline(buf,?CONTENT_SIZE);
????for?(;?!sendFile.eof();)
????{
????????//发送文件
????????send(m_clntSock,?buf,?strlen(buf),?0);
????????sendFile.getline(buf,?CONTENT_SIZE);
????}
????sendFile.close();
????closesocket(m_clntSock);
}
不同之处在于状态行得设置为404 Not Found。
现在在main中调用:
#include?"WebServer.h"
int?main()
{
????WebServer?webServ;
????webServ.RespondRequest();
????return?0;
}
运行程序,当浏览器访问localhost:8000/index.html时:
当访问其它任何页面时:
当然,懂了HTTP协议后,我们也可以作为客户端去访问各种网页,关于这个后面准备写一个C++的网络爬虫。不过之前准备先把SMTP,select,重叠IO,IOCP,boost::asio,regex等等写了再去写,所以就都是明年的事了。:)
相关文章
相关推荐
- 如何用5分钟开发一个 Webpack Loader?
-
嗨,我是勾勾。今天分享的内容是如何开发一个简单的WebpackLoader,希望通过这个过程能够让你Get到WebpackLoader的工作原理与机制。Loader作为Webpack...
- 前端——CORS跨域请求的限制与解决
-
node中设置允许跨域如果需要设置多个域允许跨域,可以根据req请求的地址进行写入不同的header;consthttp=require('http')http.cre...
- 5分钟看懂的WebAssembly入门指南(webassembly开发)
-
子肃阿里开发者2023-06-2009:01发表于浙江阿里妹导读本文是一篇WebAssembly的入门文章,从理论介绍到实战方面有全面的讲述。历史进程由于javascript的动态类型特性...
- 刚刚发布!Claude 4连续工作7小时,比Cursor、Copilot还猛?
-
你见过不吃不喝、连续工作7小时的“程序员”吗?Anthropic最新发布的Claude4,不只是AI,更像是你团队里的CTO。一、什么是Claude4?别急,这不是你熟悉的GPT“亲戚”202...
- JS对象判空的几种方式,你真的会了吗?
-
前言:为什么空对象检测如此重要?在开发中我们经常会遇到这样的场景:if(isEmpty(userInfo)){//跳转登录页}四种主流检测方案对比方案一:Object.keys()基础版fun...
- 密码被破译,行踪被美军全程掌握,日本海军军神命丧太平洋
-
【军武次位面】FriedrichLau一.突袭1941年12月7日,伴随着日军偷袭美军位于珍珠港的基地,美国也终于卷入了这场绵延全球的战火之中。为了报复日军这一行动,美军随后打出了一套组合拳,除了在太...
- 提示词技术详解(2)——零样本提示词
-
一、零样本提示(Zero-Shot)是一种会起到作用的办法。首先让模型重写提示词,然后把重写后的提示词再发给模型,以期提升回答效果。论文给出的提示词如下,仅供参考。给定一位用户的以下文字,提取其中不带...
- 这些流行饮料的中文名称,你会说吗?
-
[Photo/Pexels]Summerisinfullswing,andtheweatherishot!Tohelpyoucooldown,coldandrefre...
- 密码被破译多可怕?被美军全程盯梢,日本海军军神命丧太平洋
-
【军武次位面】FriedrichLau一.突袭1941年12月7日,伴随着日军偷袭美军位于珍珠港的基地,美国也终于卷入了这场绵延全球的战火之中。为了报复日军这一行动,美军随后打出了一套组合拳,除了在太...
- 一课译词:刀子嘴(刀子嘴是什么)
-
你身边一定有一些人,他们的言语总是那么尖锐、刺耳,但内心却又格外善良柔软,了解他们的人都知道,他们其实只是“刀子嘴,豆腐心”。“刀子嘴”,形容人说话十分刻薄(speaksarcasticallya...
- 捷克插画家柯薇塔·巴可维斯卡逝世,曾为《灰姑娘》绘制插图
-
柯瑞塔·巴可维斯卡。(图源:捷克共和国文化部)据捷克多家媒体消息,当地时间2月6日,捷克插画家柯薇塔·巴可维斯卡逝世,享年94岁。该消息经由她的儿子斯特潘·格里格(StěpánGrygar)证实。柯...
- 网络“匿名提问箱”成年轻人社交新宠 为何这么火?
-
网络“匿名提问箱”成为年轻人社交新宠“来自陌生人的关心”为什么这么火?“年度歌单里排名第一的是哪首歌?”“未来十年你的人生规划?”“有没有被甩过?”最近,这种别人能够匿名向自己提问的“提问箱”越来越得...
- 美国要开始搞6G了?专家:关键技术仍在摸索
-
2月21日,美国总统特朗普发推特“我希望5G乃至6G早日在美国落地”。日前,美国联邦通信委员会朝着特朗普的指示迈出了第一步,决定开放95千兆赫到3太赫兹频段,供6G实验使用。纽约大学教授泰德·拉帕波特...
- 常见的连续型随机变量(1)(连续型随机变量的定义与性质)
-
1.均匀分布在概率论和统计学中,均匀分布也叫矩形分布,它是对称概率分布,在相同长度间隔的分布概率是等可能的。均匀分布由两个参数a和b定义,它们是数轴上的最小值和最大值,通常缩写为U(a,b)。统计...
- 身高表上的-2SD、-1SD、中位数.....都是啥?和百分位有关系吗?
-
上周日晚,小编正气呼呼地和娃上演“作业拉锯战”时,“叮”的一声,一条微信发了过来。无独有偶,第二天又有朋友发来门诊记录,不知道SD什么意思。从家长应用的角度来看,无需太纠结,根据个人习惯选择即可。从生...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 选择器 (30)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)