应用层
- 一、应用层
- 1.1、再谈协议
- 1.2、HTTP协议
- 1.2.1、认识URL
- 1.2.2、urlencode和urldecode
- 1.2.3、HTTP协议格式
- 1.2.4、HTTP的方法
- 1.2.5、HTTP的状态码
- 1.2.6、HTTP常见的Header
- 二、结合代码理解HTTP通信流程
一、应用层
程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.
1.1、再谈协议
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些
“结构化的数据” 怎么办呢?
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:
客户端发送一个形如"1+1"的字符串; 这个字符串中有两个操作数, 都是整形; 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
…
约定方案二:
定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转
化回结构体; 这个过程叫做 “序列化" 和 "反序列化”
// proto.h 定义通信的结构体
typedef struct Request {
int a;
int b;
} Request;
typedef struct Response {
int sum;
} Response;
// client.c 客户端核心代码
Request request;
Response response;
scanf("%d,%d", &request.a, &request.b);
write(fd, request, sizeof(Request));
read(fd, response, sizeof(Response));
// server.c 服务端核心代码
Request request;
read(client_fd, &request, sizeof(request));
Response response;
response.sum = request.a + request.b;
write(client_fd, &response, sizeof(response));
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解
析, 就是ok的. 这种约定, 就是 应用层协议
1.2、HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的.
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用.HTTP(超文本传输协议)就是其中之一.
1.2.1、认识URL
1.2.2、urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
编码工具
1.2.3、HTTP协议格式
HTTP请求:
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
HTTP响应:
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个
- Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.
1.2.4、HTTP的方法
其中最常用的就是GET方法和POST方法.
1.2.5、HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
1.2.6、HTTP常见的Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
二、结合代码理解HTTP通信流程
完整版代码在码云
main函数调用
// ./httpServer 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]); //不按照要求输入则报错提示
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port)); //智能指针指向Httpserver对象
httpsvr->initServer(); //调用方法初始化并启动
httpsvr->start();
return 0;
}
初始化Httpserver对象 调用他的方法
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include"Protocol.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t=std::function<bool(const HttpRequest&,HttpResponse &)>;
class HttpServer
{
public:
HttpServer(func_t func,const uint16_t &port = gport) : _func(func),_listensock(-1), _port(port)
{}
void HandlerHttp(int sock)
{
// 1. 读到完整的http请求
// 2.反序列化
//3.httprequest,httpresponse _func(req,resp)
// 4.resp 反序列化
// 5.send
char buffer[4096];
HttpRequest req;
HttpResponse resp;
size_t n=recv(sock,buffer,sizeof(buffer)-1,0); //通过sock套接字读数据保存到buffer
if(n>0)
{
buffer[n]=0;
req.inbuffer=buffer;
req.parse();
_func(req,resp); //通过req请求得到 resp响应
send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0); //发送
}
}
void initServer()
{
// 1. 创建socket文件套接字对象
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
exit(SOCKET_ERR);
}
// 2. 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;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// 3. 设置socket 为监听状态
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog
{
exit(LISTEN_ERR);
}
}
void start()
{
for (;;)
{
// 4. server 获取新链接
// sock, 和client进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
continue;
}
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
if(fork()>0) exit(0);
HandlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
// father
waitpid(id, nullptr, 0);
}
}
~HttpServer() {}
private:
int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
uint16_t _port;
func_t _func;
};
} // namespace server
请求类和响应类
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include<sstream>
#include "Util.hpp"
const std::string sep = "\r\n";
const std::string default_root = "./wwwroot";
const std::string home_page = "index.html";
const std::string html_404="wwwroot/404.html";
class HttpRequest
{
public:
HttpRequest() {}
~HttpRequest() {}
void parse()
{
// 1.从inbuffer中拿到第一行 分隔符\r\n
std::string line = Util::getOneLine(inbuffer, sep);
if(line.empty()) return ;
// 2.从请求行中提取三个字段
std::cout<<"line :"<<line<<std::endl;
std::stringstream ss(line); //流 以空格作为分隔符
ss>>method>>url>>httpversion; //直接写入
// 3.添加web默认路径
path=default_root;
path+=url;
if(path[path.size()-1]=='/') path+=home_page;
}
public:
std::string inbuffer; // 读到的所有东西都在这
// std::string reqline; //请求行
// std::vector<std::string> reqheader; //请求头
// std::string body; //正文
std::string method; // 请求头里面的 请求方法
std::string url; // 请求头里面的 访问路径
std::string httpversion; // 请求头里面的 版本号
std::string path;
};
class HttpResponse
{
public:
std::string outbuffer;
};
手写的get方法
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// 测试
cout << "-------------------http start---------------------" << endl;
std::cout << req.inbuffer<<std::endl;
std::cout << "method: " << req.method << std::endl;
std::cout << "url: " << req.url << std::endl;
std::cout << "httpversion: " << req.httpversion << std::endl;
std::cout << "path: " << req.path << std::endl;
cout << "-------------------http endl---------------------" << endl;
std::string respline = "HTTP/1.1 200 OK\r\n"; // 状态行
std::string respheader = "Content_Type:text/html\r\n"; // 响应报头
std::string respblank = "\r\n"; // 空行
std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。</p></body></html>";
// std::string body;
// if(!Util::readFile(req.path,&body))
// {
// Util::readFile(html_404,&body); //一定成功
// }
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
return true;
}
- 流程就是运行./httpserver 8080 ,输入正确,
- 创建智能指针httpsvr指向Httpserver对象,这个对象会在创建智能指针的时候new出来,传入的是手写的Get方法和port端口号,
- 通过智能指针调用初始化函数和start函数 对该对象进行操作
- 初始化服务器会通过一系列的操作完成
4.1. 创建socket套接字对象,
4.2. bind自己的网络信息
4.3.设置socket为监听状态- 运行start函数
5.1.创建结构体对象用来接收对方发过来的请求链接
5.2.通过accept获取新的sock通信文件描述符
5.3.通过新的文件描述符来通信(本例子用的多进程版本)- 在多进程的过程中会创建子进程,子进程创建孙子进程同时关闭子进程 让孙子进程被托管,让孙子进程去执行通信流程 HandlerHttp(sock);
- 执行的流程是
// 1. 读到完整的http请求
// 2.反序列化
//3.httprequest,httpresponse _func(req,resp)
// 4.resp 反序列化
// 5.send- 在函数执行期间,会创建请求对象和接收对象通过套接字revc来读取数据,期间会调用func函数,也就是传入的get函数来打印数据便于查看,同时调用send函数来把处理后的数据返回给请求方
以上为流程。