目录
一,HTTP协议基本认识
在我之前写的文章中,我实现过自定义协议。但是,在实际的网络编程中我们是不太需要定制协议的。因为前辈早就定制好了。而HTTP协议就是其中的一种。 http协议又被叫做超文本传输协议,这是因为http的本质其实就是按照http协议从服务端拿文件资源。而http协议能够拿走所有的文件资源,所以http协议又被叫做超文本传输协议。
二,认识URL
URL的样子如下:
http://www.example.com/path/to/resource?query=string#fragment
域名其实就是ip地址,那为什么只要有ip地址就可以访问到主机上的唯一资源呢?
urlencode与urldecode
三,http协议的格式
1,发送格式
图示如下:
2,回应格式
图示如下:
四,服务端代码
在了解了http发送消息和响应消息的数据发送格式以后,我们便可以来动手写上一个能够按照http协议的格式进行响应的服务端。
实现如下:
1,为了能够更方便的使用创建套接字的接口,我对创建套接字的接口做如下封装
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include<cstring>
#include<arpa/inet.h>
#include<unistd.h>
//定义一些变量
#define blog 10
#define defaultport 8080
class Socket
{
public:
//构造函数
Socket()
: sockfd_(0)
{}
public:
//创建套接字
bool Sock()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (sockfd_ < 0)
{
std::cerr << "创建套接字失败" << std::endl;
return false;
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
return true; // 将创建好的套接字返回
}
//bind,服务端只要绑定端口号
bool Bind(int port = defaultport)
{
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 = INADDR_ANY;
int r1 = bind(sockfd_,(sockaddr*)&server_addr,sizeof server_addr);
if(r1<0)
{
std::cerr << "bind err" << std::endl;
return false;
}
return true;
}
//监听
bool Listen()
{
int r2 = listen(sockfd_, blog);
if(r2<0)
{
std::cerr << "listen err" << std::endl;
return 0;
}
return true;
}
//接收
int Accept(std::string* ip,int* port)
{
sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
int sockfd = accept(sockfd_, (sockaddr *)&cli_addr, &len);
if(sockfd<0)
{
std::cerr << "accept err" << std::endl;
return -1;
}
char buff[64]={0};
inet_ntop(AF_INET, &cli_addr, buff, sizeof(buff));
*ip = buff;
*port = ntohs(cli_addr.sin_port);
return sockfd;
}
//连接
bool Connect(std::string& ip,int16_t port)
{
sockaddr_in addr_;
addr_.sin_family = AF_INET;
addr_.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(addr_.sin_addr));
int r = connect(sockfd_, (sockaddr *)&addr_, sizeof (addr_));
if(r<0)
{
std::cerr << "connect err" << std::endl;
return false;
}
return true;
}
//关闭
void Close()
{
close(sockfd_);
}
public:
//成员
int sockfd_;
};
2,服务端代码
在做好上述封装工作以后便可以来构建服务端了,服务端的创建步骤如下:
1,类成员
2,构造函数
3,Init函数
4,Start函数
在理清上面创建服务端的过程以后,便可以写出如下代码:
struct ThreadData
{
int sockfd;
};
class HttpServer
{
public:
HttpServer(int port)
:port_(port)
{}
void Init()
{
listensocket_.Sock();//创建套接字
listensocket_.Bind(port_);//绑定套接字端口
listensocket_.Listen();//监听套接字
}
bool Start()
{
while (true)
{
// 建立连接
std::string ip;
int port;
int sockfd = listensocket_.Accept(&ip, &port_);
lg(Debug, "link->%s:%d sucess,sockfd:%d", ip.c_str(), port_,sockfd);
// 创建线程执行任务
pthread_t td;
ThreadData ts;
ts.sockfd = sockfd;
pthread_create(&td, nullptr, ThreadRun, &ts);
}
return true;
}
static void* ThreadRun(void*args)
{
pthread_detach(pthread_self());//与主线程分离
ThreadData* ts = static_cast<ThreadData*>(args);
Helper(ts->sockfd);//处理客户端发来的消息
delete ts;
return nullptr;
}
private:
Socket listensocket_;
int port_;
};
如何处理客户端发来的消息?
代码如下:
static void Helper(int sockfd)
{
// 接收消息并打印
char buff[1024] = {0};
int n = recv(sockfd, buff, sizeof(buff), 0);
if (n > 0)
{
//显示读出来的消息
buff[n] = 0;
std::cout << buff << std::endl;
//读出来的消息进行解析处理,得到客户端想要访问什么资源
Request req;
req.Deserialize(buff);
req.prase(buff);
std::string path = req.Select_path();
path = rootpath + path;
std::cout << "debug: " << std::endl;
req.Debugprint();
//根据http协议的方式将资源发送会给客户端
std::string text = Readrequest(path);
std::string response;
std::string sep = "\r\n";
std::string line = "HTTP 1.1 1 OK";
line += sep;
std::string head = "contentlenth:";
std::string len = std::to_string(text.size());
head += len;
head += sep;
head += sep;
response+=line;//状态行
response += head;//报头
response += text;//正文
send(sockfd, response.c_str(), response.size(), 0);
}
}
如何对客户端的发来的消息进行处理呢?
类方法如下:
实现如下:
class Request
{
public:
void Deserialize(std::string message)
{
while (true)//循环读取
{
int pos = message.find(sep);
if(pos == std::string ::npos)
{
break;
}
std::string str = message.substr(0, pos);
if(str.empty())
break;
arr.push_back(str);
message.erase(0, pos + 1);//读一行消一行
}
text = message;
}
void prase(std::string message)//将状态行的信息散开
{
std::stringstream s (message);
s >> method >> url >> http_version;
}
void Debugprint()
{
for(auto e:arr)
{
std::cout << e <<" "<<std::endl ;
}
std::cout << "method:" << method << std::endl;
std::cout << "url:" << url << std::endl;
std::cout<< "http_version:" << http_version << std::endl;
}
std::string Select_path()
{
if(url == "/"||url == "/html.index")
{
path = rootpath+"/";
}
else
{
path = url;
}
return path;
}
private:
std::vector<std::string> arr;
std::string text;
std::string method;
std::string url;
std::string http_version;
std::string path;
};
返回消息:
读取文件的函数如下:
std::string Readrequest(std::string path)//从文件内读取
{
std::ifstream in(path);
if(!in.is_open())
return "404";
std::string line;
std::string content;
while(getline(in,line))
{
content += line;
}
return content;//这个消息会拼接到正文,也就是text
}
五,http报文细节
1,Post与Get方法
在http的请求报头当中,经常使用的请求方法有如下两个:1,Post 2,Get
Post:
Get:
相对于Get方法,Post方法提交参数的方式比较隐蔽。但是两种提交参数的方式都是不安全的。因为通过抓包都可以将报文抓取然后获取报文的所有内容。如果想要安全就得加密,加密协议就是https协议。
2,Content_lenth
在相应报文的报头里面有一个Content_lenth的字段,代表着正文的长度。报头与正文之间通过一个空行来分隔,分割以后,正文内容的大小由Content_lenth来指定,进而进行读取。
3,状态码
在http报文当中,相应报文内的状态行中会有一串数字表示响应的状态。
比如,200就代表正常,状态码描述便是ok。
通常,状态码对应的消息如下
4,Location
Location字段一般都是和重定向状态3xx搭配使用的。当我的客户端申请访问我们已经移动后的资源时,服务端的Location字段便会指引客户端去访问移动后的资源。
5,content_type
content_type标识的是正文的文件类型。这个,content_type放在报头,指明文件类型,进而让客户端以正确的类型收取文件资源显示文件资源。
6,cookie
cookie字段的作用是储存少量信息,用于搭建临时会话。
分类:
更完整细节:
HTTP方法: