实战项目自主web服务器
Posted 小倪同学 -_-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战项目自主web服务器相关的知识,希望对你有一定的参考价值。
文章目录
项目简介
什么是web服务器: 通过HTTP协议构建一个web服务器,该服务器能够处理浏览器发过来的http请求,并根据http请求返回相应的http响应给浏览器。相当于搭建个人网站,你可以在个人网站上存放各种资源,别人可以通过浏览器访问你的网站,并获取资源。
背景: 该项目主要用http协议,在网络中,http协议被广泛使用如移动端,pc端浏览器。http协议是打开互联网应用窗口的重要协议,它在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
目标: 该项目的目标是,对http协议的理论有更深刻的理解,从零开始完成web服务器开发,连接下三层协议,从技术到应用,让网络难点无处遁形。
简述: 采用C/S模型,编写支持中小型应用的http,理解常见互联网应用行为,做完该项目,你可以从技术上完全理解从你上网开始,到关闭浏览器的所有操作中的技术细节。
技术特点: 该项目是一个后端开发项目,开发环境为centos 7 + vim/gcc/gdb/VS Code,开发语言为C/C++,应用网络编程(TCP/IP协议, socket流式套接字,http协议),多线程,cgi,线程池等技术。
认识http协议
http分层概览
与http相关的重要协议有TCP,IP,DNS
这里介绍一下DNS
DNS(域名系统)是将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。
协议之间是如何协同运作的呢?
http背景知识补充
目前主流服务器使用的是http/1.1版本,该项目是按照http/1.0版本来完成讲解,同时,我们还会对比1.1和1.0的区别。
http协议的特点如下
- 简单快速,HTTP服务器的程序规模小,因而通信速度很快。
- 灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
- 无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)。
- 无状态(HTTP协议自身不具有保存之前发送过的请求或响应的功能)
注意: http协议每当有新的请求产生,就会有对应的新响应产生。协议本身并不会保留你之前的一切请求或者响应,这是为了更快的处理大量的事务,确保协议的可伸缩性。
URI & URL & URN
我们想访问web上资源需要用资源标志符(URI)进行定位,和URI相关的还有URL,URN。
- URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。
- URL,是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
- URN,uniform resource name,统一资源命名,是通过名字来标识资源。
URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI,URL是 URI 的子集。任何东西,只要能够唯一地标识出来,都可以说这个标识是 URI 。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个 URL ;但如果这个标识不提供获取到对象的路径,那么它就必然不是URL 。
HTTP URL (URL是一种特殊类型的URI,包含了如何获取指定资源)的格式如下:
http://host[":"port][abs_path]
- http表示要通过HTTP协议来定位网络资源
- host表示合法的Internet主机域名或者IP地址,本主机IP:127.0.0.1
- port指定一个端口号,为空则使用缺省端口80。(通常会根据协议采用默认端口号)
- abs_path指定请求资源的URI
- 如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。(如果没有abs_path,会默认访问该服务端首页)。
例:
一个较为完整的http请求:
http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1
构建tcp服务器
我们这里的tcp服务器是对网络套接字创建,绑定,监听等进行封装,方便之后的使用。
这里我们把 tcp server 类设计为单例模式,让程序访问到的 tcp server 是同一个。代码如下
#pragma once
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#define PORT 8081
#define BACKLOG 5
class TcpServer
private:
int port;// 端口号
int listen_sock;// 套接字
static TcpServer *svr;
private:
TcpServer(int _port = PORT):port(_port),listen_sock(-1)
TcpServer(const TcpServer &s)
public:
static TcpServer *getinstance(int port)
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 线程互斥锁,防止多线程时产生竞争
if(nullptr == svr)
pthread_mutex_lock(&lock);
if(nullptr == svr)
svr = new TcpServer(port);// 创建tcpserver
svr -> InitServer();// 初始化
pthread_mutex_unlock(&lock);
return svr;
void InitServer()// 初始化
Socket();
Bind();
Listen();
void Socket()
listen_sock = socket(AF_INET,SOCK_STREAM,0);// 创建套接字
if(listen_sock < 0)
exit(1);
int opt = 1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));// 设置套接字选项,允许地址复用
void Bind()
struct sockaddr_in local;
memset(&local,0,sizeof(local));// 清零
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;// 云服务器不能直接绑定公网IP
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)// 绑定失败
exit(2);
void Listen()
if(listen(listen_sock,BACKLOG)<0)
exit(3);
~TcpServer()
;
TcpServer* TcpServer::svr = nullptr;
关于setsockopt函数可以参考这篇博客setsockopt()函数功能介绍
HTTP请求和响应
请求和响应过程
浏览器请求服务器资源需要给服务器发送http请求,服务器收到请求需要返回http响应给浏览器,我们需要详细了解请求和响应的格式,为之后编码做准备。
HTTP请求报文
HTTP请求报文分为 请求行,请求报头,空行,请求正文
- 请求行: 请求方法,请求URL,HTTP协议和版本
- 请求报头: 请求属性,每一行表示一个属性,遇到空行表示请求报头结束。
- 空行:用于分割请求报头和请求正文
- 请求正文:如果存在正文,请求报头中就需要Content-Length属性来标识正文的大小
HTTP响应报文
HTTP响应报文分为 响应行,响应报头,空行,响应正文
- 响应行:http协议和版本,状态码,状态描述符
- 响应报头:响应属性,每一行表示一个属性,遇到空行表示响应报头结束。
- 空行:分割响应报头和响应正文
- 响应正文:服务器返回的html页面或者json数据
工具类
我们在编写协议时难免会有一些程序经常用到,如:读取报头中的一行。我们可以设计一个工具类,将这些程序存放其中。
ReadLine()函数
ReadLine()函数的作用是读取sock套接字中的一行数据。无论是请求报头还是响应报头,它们的属性都是按行进行划分的。但是不同的浏览器发送过来的http请求,它们中的行分割符是不同的,大致分为如下三类
- xxxxx \\r\\n
- xxxxx \\n
- xxxxx \\r
我们要设计一个算法,兼容各种行分割符。
读取数据,如果读取到 \\n 表示该行已结束,如果读取到 \\r ,需要判断下一个字符是否为 \\n 。不是,那么该行已结束,否则需要将 \\r\\n 转变为 \\n。
这里我们需要用到 recv 函数
recv函数
功能: 接收已连接的数据报或流式套接口的数据。
函数原型:
int recv( int sockfd, void *buf, size_t len,int flags);
参数说明:
- sockfd :发送数据的套接字
- buf :存放recv函数接收到的数据
- len :buf的长度
- flags :recv发送数据的选项,一般设置为0
返回值: 成功返回0,错误返回-1
编码
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
//工具类
class Util
public:
static int ReadLine(int sock,std::string &out)
char ch = 'x';
while(ch != '\\n')
ssize_t s = recv(sock,&ch,1,0);// 从sock中读取一个字符到ch中
if(s > 0)
if(ch == '\\r')
recv(sock,&ch,1,MSG_PEEK);// MSG_PEEK 窥探下一个字符(不从sock中取走)
if(ch == '\\n')
// 把\\r\\n -> \\n
// 窥探成功读取该字符
recv(sock,&ch,1,0);
else
ch = '\\n';// \\r -> \\n
// 到这有两种情况 1.普通字符 2.\\n
out.push_back(ch);
else if(s == 0)
return 0;
else
return -1;
return out.size();
;
CutString()函数
CutString()函数的功能是将字符串按特定字符切分成左右两部分,代码如下
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
//工具类
class Util
public:
static int ReadLine(int sock,std::string &out)
...
static bool CutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)
size_t pos = target.find(sep);// 查找分割符
if(pos != std::string::npos)
sub1_out = target.substr(0,pos);
sub2_out = target.substr(pos+sep.size());
return true;
return false;
// 传入
// target = Content-Length : 18
// sep = " : "
// 传出
// sub1_out = Content-Length
// sub2_out = sep
;
日志信息
为了方便代码解析和调试,我们需要设计函数来记录日志信息,日志信息的内容如下
乍一看传入的参数是不是有点多,实际上我们只需传入 日志级别 和 日志信息 这两个参数,剩下的参数我们可以让操作系统去查找,为此可以设置宏,通过宏替换来获取 日志文件 和 代码行数 。
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG(level,message) Log(#level,message,__FILE__,__LINE__)
void Log(std::string level,std::string message,std::string file_name,int line)
std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file_name << "]" << "[" << line << "]" << std::endl;
请求和响应类的设计
http服务器收到一个请求时,服务器需要做如下工作:读取请求,分析请求,构建响应,发送响应。为了更好的管理请求报头和响应报头,我们需要设计一个类来存放请求报头和响应报头。
http请求类
class HttpRequest
public:
std::string request_line;// 请求行
std::vector<std::string> request_header;// 请求报头
std::string blank;// 请求空行
std::string request_body;// 请求正文
// 请求行的解析
std::string method;
std::string uri;
std::string version;
// 请求报头解析
std::unordered_map<std::string, std::string> header_kv;// 属性名:属性信息
int content_length;// 请求正文长度
// URI解析
std::string suffix;// 请求文件名的后缀
std::string path;// URI路径
std::string query_string;// URI参数
bool cgi;// cgi处理标志符
int size;// 打开的文件大小
public:
HttpRequest():content_length(0),cgi(false)
~HttpRequest()
;
http响应类
#define LINE_END "\\r\\n"
class HttpResponse
public:
std::string status_line;// 响应行
std::vector<std::string> response_header;// 响应报头
std::string blank;// 响应空行
std::string response_body;// 响应正文
int status_code;// 响应状态码
int fd;// 暂时保存文件描述符
public:
HttpResponse():blank(LINE_END),status_code(OK),fd(-1)
~HttpResponse()
;
请求处理
请求报文
处理请求报头时,先将报头的数据读取出来,再对其进行处理
void RecvHttpRequest()
// 读取请求行和报头
if((!RecvHttpRequestLine()) && (!RecvHttpRequestHeader()))
ParseHttpRequestLine();// 解析请求行
ParseHttpRequestHeader();// 解析请求报头
RecvHttpRequestBody();// 读取请求正文
读取请求行
利用工具类中的ReadLine()函数将请求行读入到请求类中
bool RecvHttpRequestLine()
auto& line = http_request.request_line;
if(Util::ReadLine(sock,line) > 0)
line.resize(line.size()-1);// 去除行尾 \\n
LOG(INFO,http_request.request_line);// 记录日志
else
stop = true;// 标记读取失败
return stop;
读取请求报头
按行为单位,将读取到的报头属性插入到请求报头中。
bool RecvHttpRequestHeader()
std::string line;
while(line!="\\n")
line.clear();// 读取前清空line
if(Util::ReadLine(sock,line) <= 0)// 按行读取
stop = true;
break;
if(line == "\\n")// 读取到空行,请求报头已读完
http_request.blank = line;
break;
line.resize(line.size()-1);// 去除最后的'\\n'
http_request.request_header.push_back(line);// 将读取到的报头属性尾插入请求报头中
LOG(INFO,line);
return stop;
解析请求行
请求行由 请求方法,请求URI,HTTP协议版本 构成,它们中间由空格隔开,如下图
解析请求行就是将字符串按空格分割。
将字符串按空格分割我们可以调用库函数中的stringstream
stringstream使用示例:
#include<iostream>
#include<string>
#include<sstream>
int main()
std::string msg = "GET /a/b/c.html http/1.0";
std::string method;
std::string uri;
std::string version;
std::stringstream ss(msg);
ss >> method >> uri >> version;
std::cout << method << std::endl;
std::cout << uri << std::endl;
std::cout << version << std::endl;
运行结果
解析请求行代码
void ParseHttpRequestLine()
auto &line = http_request.request_line;// 获取请求行
std::stringstream ss(line);
ss >> http_request.method >> http_request.uri >> http_request.version;
auto &method = http_request.method;// 获取请求行中的请求方法
std::transform(method.begin(),method.end(),method.begin(),::toupper);// 字母转大写,方以上是关于实战项目自主web服务器的主要内容,如果未能解决你的问题,请参考以下文章