目录
URL
http协议
http请求
http响应
细节优化
makefile
HttpServer.hpp
HttpServer.cc
Util.hpp
Usage.hpp
Sock.hpp
Log.hpp
wwwroot/index.html
应用层:就是程序员基于socket接口之上编写的具体逻辑,做的很多工作,都是和文本处理有关的——协议分析与处理。
http协议,一定会有大量的文版分析和协议处理。
平时我们俗称的 "网址" 其实就是说的 URL
其中new.qq.com是域名,浏览器会自动把域名解析为IP,域名后的第一个/叫做web根目录
我们平时上网:1.想获取什么内容 2.我们想上传什么内容
我们想获取的内容称为资源,当这个资源没有被我们拿到的时候,资源在服务器上。
一个服务器上可能会存在很多资源,这些资源以文件的形式保存在服务端,请求资源拿到我们的本地可理解为服务进程打开要访问的文件,读取该文件,通过网络发到客户端,但要打开这个文件,首先得找到这个文件
linux中标识一个文件,是通过路径来标识得。
URL理解:协议名称://server ip[:80]/a/b/c/d/e.html
路径加文件名就是客户要得资源。
URL通常可以用来定义互联网中唯一得一个资源,url被称作:统一资源定位符
所有得资源:在全球范围内找到它得url就能访问该资源。基于这种获取资源得方式被称作:www(万维网)
我们在百度搜索?和helloworld,第一个是?,第二个是helloworld
我们可以看到有俩个wd,其中?被转码为%3F
如果用户想在url中包含url本身用来作为特殊字符得字符,url在形成得时候,浏览器会自动进行编码encode,即把特殊符号用其它形式替代,一般服务端收到之后,需要进行转回特殊字符。
转换规则是将字符的ASCII码值转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式,汉字也会被编码。
http是基于请求和响应的,这种请求响应模式叫做CS模式。
http协议是应用层的协议,底层采用的是TCP,在双方通信之前已经经历了三次握手的过程,即已经建立好了连接
单纯在报文角度,http可以是基于行的文本协议
请求报头里会包含本次请求的相关属性,如本次请求是长连接还是短连接。下图是http的请求
http/1.1是协议版本 ,这里是客户端告诉服务器,客户端用的是哪一个版本
我们可以根据\r\n,将http请求看成线性结构
这里的版本是服务器告诉客户端,服务器用的是哪一个版本。
我们暂时要关注该协议如何封装和解包,以及向上交付。
http是如何区分报头和有效载荷?http通过\r\n区分报头和有效载荷的。这样意味着我们一定能够把报头读完,空行读完接下来在读的就是正文了。
我们如何得知正文的大小呢?在报头当中,涵盖一个Cotent-Length:123 这个就是正文的长度
我们启动后在浏览器就可以进行进行http访问了,只不过我们这里没有进行请求响应
我们当前服务端已经收到了一堆请求
GET请求方法,如果并未告诉服务器请求什么资源, 默认填一个/代表web根目录,并不代表要把web根目录下的所有文件发给它,而是请求默认的网页,HTTP/1.1是协议版本,告诉客户端,我的版本是1.1
注意以上是未加响应的效果
加一些响应
我们访问之后会得到响应
如果我们这样访问
代表我们要请求的资源是/a/b/c目录下的d资源,第一个/是web根目录,
web根目录我们可以自定义路径,比如我们可以把所有的网页资源放在这个路径下,我们就可以让客户端在请求资源时,请求的是wwwroot路径下的资源,wwwroot就叫做web根目录
这个正文应该以网页的形式存在wwwroot根目录下,然后让用户去请求这个网页
如果我们当前请求的是wwwroot目录,默认代表的是请求当前我们所对应的web服务器的默认首页
因此我们可以构建一个首页,把首页的内容返回给用户即可
访问的时候根据,根目录去访问我们想要的资源,如果只有根目录,就默认访问/index文件,如果有对应的路径,则按照对应的路径去进行访问资源。
当服务器读取的时候,首先得把对方发送的资源路径提取出来。我们写一个util.hpp进行路径提取
效果
我们可以看到字符串提取成功
对程序稍作修改,只提取第一行内容
若输入IP:端口号/a/b/c/d/e/d.html,其中请求的资源路径就是a/b/c/d/e.html,若只输入IP:端口号,则代表请求根目录
我们创建一个首页
当用户想访问某个路径时,我们要把文件给用户打开,此时拿到了用户想要的资源
如果不带任何路径,此时其实是客户只请求了一个/,我们返回默认首页,只要输入的是空路径,其实就是输入了/,我们就默认返回首页,这里我们设置默认首页是"index.html"
HttpServer:HttpServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f HttpServer
#include
#include
#include"Sock.hpp"
#include
class HttpServer
{
public:
using func_t=std::function;//返回值为void,参数为int类型的函数
private:
int listensock_;//监听套接字
uint16_t port_;//端口号
Sock sock;
func_t func_;
public:
HttpServer(const uint16_t &port,func_t func)
:port_(port),func_(func)
{
//Sock sock;
listensock_=sock.Socket();
sock.Bind(listensock_,port_);
sock.Listen(listensock_);
}
void Start()
{
signal(SIGCHLD,SIG_IGN);//对子进程的信号做忽略,主进程不必阻塞等待
for(;;)
{
std::string clientIp;//客户端IP
uint16_t clientPort=0;
//客户端端口号,默认端口号设为0
int sockfd=sock.Accept(listensock_,&clientIp,&clientPort);
if(sockfd<0) continue;//如果没连上,就继续连
if(fork()==0)//如果连上了
{
close(listensock_);//关闭格子不需要的文件描述符
func_(sockfd);//对http请求做处理
close(sockfd);//处理完之后关闭sockfd
exit(0);
}
close(sockfd);
}
}
~HttpServer()
{
if(listensock_>=0)
close(listensock_);
}
};
#include
#include"HttpServer.hpp"
#include
#include
#include
#include
#include
#include
#include"Usage.hpp"
#include"Util.hpp"
#define ROOT "./wwwroot"//一般http都要有自己的web根目录
//如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE"index.html"//默认首页
void HandlerHttpRequest(int sockfd)
{
//1.读取请求
char buffer[10240];
ssize_t s=recv(sockfd,buffer,sizeof(buffer)-1,0);
if(s>0)
{
buffer[s]=0;
//std::cout< vline;
Util::cutString(buffer,"\n",&vline);//字符串切割,一行一行切
/*for(auto &iter:v)//遍历一下v,看是否切割成功
{
std::cout< vblock;
Util::cutString(vline[0]," ",&vblock);//截取第一行,以空格为分隔符,把第一行切成三部分
std::string file=vblock[1];//拿到请求的资源
std::string target=ROOT;
if(file=="/")
file="/index.html";
target+=file;
std::cout< httpserver(new Httpserver(atoi(argv[1]),HandlerHttpRequest));
httpserver->Start();
return 0;
}
#include
#include
class Util
{
public:
//我们先按照报文当中的内容提取一行内容
static void cutString(const std::string s,const std::string &sep,std::vector*out)
//该函数是用来切割子串的
{
std::size_t start=0;
while(startpush_back(sub);//截取子串,放到vector中
//std::cout<<"----"<push_back(s.substr(start));
}
}
};
#pragma once
#include
#include
void Usage(std::string proc)
{
std::cout<<"\nUsage:"<
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"Log.hpp"
class Sock
{
private:
const static int gbacklog=20;//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
public:
Sock()
{}
int Socket()//创建套接字
{
int listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
//参数含义第一个:网络通信 第二个:流式通信
if(listensock<0)//创建套接字失败
{
logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
exit(2);
}
logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
return listensock;
}
void Bind(int sock,uint16_t port,std::string ip="0.0.0.0")
{
//bind目的是让IP和端口进行绑定
//我们需要套接字,和sockaddr(这个里面包含家族等名称)
//绑定——文件和网络
struct sockaddr_in local;
memset(&local,0,sizeof local);//初始化local
local.sin_family=AF_INET;
local.sin_port=htons(port);//端口号
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
//IP地址,由于我们构造的时候是IP是个空的字符串
//所以我们可以绑定任意IP
//我们一般推荐绑定0号地址或特殊IP
//填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
if(bind(sock,(struct sockaddr*)&local,sizeof local)<0)
{
//走到这就绑定失败了,我们打印错误信息
logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
exit(3);
}
}
void Listen(int sock)//将套接字设置为listen状态
{
//因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
if(listen(sock,gbacklog)<0)
{
logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
exit(4);
}
logMessage(NORMAL,"init server success");
}
//一般经验
//const std;;string &::输入型参数
//std::string *:输出型参数
//std::string &:输入输出型参数
int Accept(int listensock,std::string *ip,uint16_t *port)//获取连接
{
//从套接字中获取到客户端相关的信息
struct sockaddr_in src;
socklen_t len=sizeof(src);
int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
if(servicesock<0)
{
//获取连接失败
logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
return -1;
}
if(port) *port=ntohs(src.sin_port);//如果port被设置,获取新的port
//客户端端口号在src
//由于是网络发送过来得套接字信息
//所以要把信息进行网络转主机
if(ip) *ip=inet_ntoa(src.sin_addr);//如果Ip被设置,获取新的Ip
//我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
//到这里我们拿到了IP和端口号
return servicesock;
}
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)//进行连接
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
else return false;
}
~Sock(){}
};
#pragma once
#include
#include
#include
#include
#include
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
//FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
//fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
//fclose(fp);
}
Linux学习
这个一个Linux课程
我是一个Linux的学习者,我正在进行http的测试工作!!
我是一个Linux的学习者,我正在进行http的测试工作!!
我是一个Linux的学习者,我正在进行http的测试工作!!
我是一个Linux的学习者,我正在进行http的测试工作!!
我是一个Linux的学习者,我正在进行http的测试工作!!
我是一个Linux的学习者,我正在进行http的测试工作!!
我是一个Linux的学习者,我正在进行http的测试工作!!