应用层协议
在这个章节中将会进一步详细讨论应用层协议及其知名协议HTTP协议。
协议
应用层负责程序之间的数据沟通,其中协议大概分为两类,自定制协议和知名协议。
自定制协议
自定制协议就是程序员自己定义的协议,用来对应用程序发送的数据进行整理或者加密,对端只有了解这种协议才能对数据进行解析。
这里利用自定制简单实现一个网络版计算器。客户端将两个数字和一个运算符传输给服务端,服务端对接收到的信息进行解析,得到数字和运算符运算出结果后将结果返回给客户端。
在开始之前我们要先自定制一个协议方便我们的客户端与服务端之间进行数据通信。
我们可以将一个表达式解析成如下形式再发送给服务端:1 + 1 -> 1; 2; +
。这样的协议形式我们只需要让服务端对数据进行相应切割就可以找到两个操作数和运算符,但是解析过程略微繁琐,所以我们还有第二种方式。
我们利用一个整形数据四个字节一个运算符一个字节的特点,一共分配九个字节大小的内存,约定前八个字节存放两个操作数,最后一个字节存放运算符,一起发送到服务端服务端再对发送的数据分块解析也可以得到数据。
第一种方式易于实现,只用发送一个字符串即可,第二种方式可以用内存拷贝的方式实现,不过最方便的还是利用一个结构体。
tcp_socket.hpp
/**
* 封装一个tcpsocket类,向外提供简单接口能够实现客户端服务端编程流程
* 1、创建套接字
* 2、绑定地址信息
* 3、开始监听/发起连接请求
* 4、获取已完成连接
* 5、发送数据
* 6、接收数据
* 7、关闭套接字
**/
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#define CHECK_RET(q) if(q == false) {return -1;}
struct calc_t
{
int num1;
int num2;
char op;
};
class TcpSocket
{
public:
TcpSocket()
{
}
~TcpSocket()
{
Close();
}
//创建套接字
bool Socket()
{
//这里首先创建的时皮条套接字
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
return true;
}
//绑定地址信息
bool Bind(const std::string& ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(&ip[0]);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
std::cerr << "bind error" << std::endl;
return false;
}
return true;
}
//服务端开始监听
bool Listen(int backlog = 5)
{
int ret = listen(_sockfd, backlog);
if(ret < 0)
{
std::cerr << "listen error" << std::endl;
return false;
}
return true;
}
//连接服务端
bool Connect(const std::string& ip, uint16_t port)
{
int ret;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(&ip[0]);
socklen_t len = sizeof(struct sockaddr_in);
ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
std::cerr << "connet error" << std::endl;
return false;
}
return true;
}
//设置套接字
void SetFd(int fd)
{
_sockfd = fd;
}
//获取新的套接字
bool Accept(TcpSocket& newsock)
{
struct sockaddr_in addr;
socklen_t len = sizeof(struct sockaddr_in);
//这里fd是皮条套接字新创建出来的连接套接字
int fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if(fd < 0)
{
std::cerr << "accept error" << std::endl;
return false;
}
//newsock._sockfd = fd;
newsock.SetFd(fd);
return true;
}
//发送数据
bool Send(void* buf, int len)
{
int ret = send(_sockfd, buf, len, 0);
if(ret < 0)
{
std::cerr << "send error" << std::endl;
return false;
}
return true;
}
bool Send(const std::string& buf)
{
int ret = send(_sockfd, &buf[0], buf.size(), 0);
if(ret < 0)
{
std::cerr << "send error" << std::endl;
return false;
}
return true;
}
//接收数据
bool Recv(void* buf, int len)
{
int ret = recv(_sockfd, buf, len, 0);
if(ret < 0)
{
std::cerr << "recv error" << std::endl;
return false;
}
else if(ret == 0)
{
std::cerr << "peer shutdown" << std::endl;
return false;
}
return true;
}
bool Recv(std::string& buf)
{
char tmp[4096] = {0};
int ret = recv(_sockfd, &tmp[0], 4096, 0);
if(ret < 0)
{
std::cerr << "recv error" << std::endl;
return false;
}
else if(ret == 0)
{
std::cerr << "peer shutdown" << std::endl;
return false;
}
buf = tmp;
return true;
}
//关闭
bool Close()
{
if(_sockfd >= 0)
{
close(_sockfd);
}
}
private:
int _sockfd;
};
tcp_cli.cpp
#include "tcp_socket.hpp"
#include <stdlib.h>
/**
* 实现客户端
* 1、创建套接字
* 2、绑定地址信息(客户端不需要手动绑定)
* 3、向服务端发起连接请求
* 4、发送数据
* 5、接收数据
* 6、关闭套接字
**/
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cerr << "./tcp_cli srvip srvport" << std::endl;
return -1;
}
TcpSocket sock;
CHECK_RET(sock.Socket());
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
CHECK_RET(sock.Connect(ip, port));
while(1)
{
calc_t tmp;
tmp.num1 = 11;
tmp.num2 = 22;
tmp.op = '+';
sock.Send((void*)&tmp, sizeof(calc_t));
}
sock.Close();
}
tcp_srv.cpp
/**
* 服务端实现
* 1、创建套接字
* 2、绑定地址信息
* 3、开始监听
* 4、获取新连接
* 5、接收数据
* 6、发送数据
* 7、关闭套接字
**/
#include "tcp_socket.hpp"
#include <stdlib.h>
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cerr << "./tcp_srv 192.169.11.128 9000" << std::endl;
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind(ip, port));
CHECK_RET(sock.Listen());
//这个新的套接字要放在循环外部,否则一次循环结束变量销毁会关闭套接字连接就会断开
TcpSocket newsock;
while(1)
{
bool ret = sock.Accept(newsock);
if(ret == false)
{
continue;
}
calc_t buf;
ret = newsock.Recv((void*)&buf, sizeof(calc_t));
if(ret == false)
{
std::cerr << "Recv error" << std::endl;
return -1;
}
std::cout << buf.num1 << " " << buf.num2 << " " << buf.op << std::endl;
}
sock.Close();
}
使用:
[misaki@localhost netbase]$ ./tcp_srv 192.168.11.128 9000
11 22 +
我们的服务端就收到了指定的数据信息。
在对于自定制协议的使用中有两条重要概念,序列化与反序列化。
序列化:将数据对象按照指定协议在内存中进行排布成为可持久化存储。
反序列化:将数据传按照指定的协议进行解析得到各个数据对象。
对于序列化与反序列化有几种常用工具。json序列化,protobuf序列化,二进制序列化
。
知名协议(HTTP协议)
应用层知名协议有很多不过最常用的就是HTTP
协议。在HTTP协议中包含一个重要信息统一资源定位符(URL),URL中又包含哪些信息呢?
URL
其实URL就是我们常说的网址。
在URL中有登录信息,服务器地址端口号,文件地址信息,查询字符串和片段标识符。
这里给出一篇博客对URL的组成形式进行了讲解。
https://www.jianshu.com/p/406d19dfabd3
在URL中要注意的是文件地址信息以?
结束往后的是查询字符串及片段标识符这两个信息。
查询字符串是客户端提交给服务器的数据。查询字符串是一个个key=val
的键值对,并且以&
作为间隔,以#
作为结尾。并且提交的数据中不能出现特殊字符,因为会和URL中的分隔符造成二义,造成URL解析失败,因此若提交的数据中有特殊字符就必须进行转义。在URL中如果字符前出现了%
则认为这个字符经过了URL的转码。URL转码规则为将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式。片段标识符是对资源的补充,可以理解为书签。
HTTP协议格式
HTTP
协议分为以下几个大的组成部分。
1、首行
首行又分为请求首行和响应首行。
请求首行格式:请求方法 URL 协议版本\r\n
。
请求方法有很多,其中常用的有GET/POST
方法。GET
方法多用于向服务器请求资源,不过也可以提交数据,只不过提交的数据在查询字符串中以明文方式传输,十分不安全并且长度有限。POST
方法是专门用于向服务器提交表单数据的,提交的数据在正文中。除此之外还有很多种请求方法,HEAD/PUT/DELETE/CONNECT/OPTIONS/TRACE/PATCH
,这里简单介绍几个。
HEAD
请求方法和GET
一样是向服务器请求资源,不过HEAD
请求只会接收服务器的回复中的首行及头部信息,而不要正文信息。
PUT
请求会用正文信息替代服务端指定文件中的数据。
DELETE
是删除服务器指定文件。
URL已经介绍过。关于协议版本目前常见的有http/0.9 http/1.0 http/1.1 http/2
。0.9版本是HTTP
最早期的协议,只有一个GET
方法,用于向服务器获取数据展示HTML
文件。并且0.9版本的连接是短连接,客户端向服务端建立连接发送请求,服务端应答完毕就会关闭套接字。与之对应的是长连接,即一轮信息交互后不会关闭客户端,可以继续通信,这样就不用频繁建立套接字,这个长连接在1.0版本中得以实现,但是这个长连接还是一来一回的连接方式,并且在1.0版本中默认关闭长连接,需要在头部信息中开启,在这个版本中还加入了POST
和HEAD
方法。之后便是1.1版本,并且是现在最常用的版本,这个版本中长连接默认开启,并且使用了管线化连接方式,即客户端可以连续发送多个请求,服务端一一进行回复,更加灵活方便,在这个版本中还将请求方法增加到现在的9个方法。
响应首行格式:协议版本 响应状态码 响应状态码描述\r\n
协议版本与请求首行中的协议版本是一样的。这里着重介绍响应状态码。
响应状态码是服务端对请求处理结果的表述,一共分为五大类:
1xx:表示描述性信息
2xx:处理正确响应,如200表示请求已经正确处理完毕
3xx:表示重定向,如301永久重定向,302临时重定向。
永久重定向指下次如果还请求此路径则直接访问重定向之后的网址,不再请求原网址
临时重定向表示知识临时重定向到新网址,下次访问还是先访问原网址
4xx:客户端错误,如404请求资源未找到,400请求格式有误
5xx:服务端错误,如502网关错误,500服务器内部错误。
响应状态码描述就是对响应状态码响应的文字描述信息。
2、头部
头部是以一个一个键值对的形式存在的,格式key: value
,标识着连接的属性和一些信息。一对键值对独占有一行,以\r\n
作为结尾。这里简单介绍几种头信息,但其实头信息非常非常的多。
//请求头部:
Connection:开启/关闭长连接
Content-Length:正文长度
Content-Type:正文数据类型,有很多类型,包括图片,HTML超文本等
User-Agent:浏览器版本信息及系统版本信息
Accept:告诉服务端自己可以接收哪些数据
Accept-Encoding:能接收的数据编码
Cookie:向服务端发送当前用户的Cookie信息即sessionid
//响应头部:
Server:服务端版本信息
Date:事件
Content-Type:正文数据类型
Transfer-Encoding:传输格式,如果为chunked表示为分块传输,此时每个分块的大小写在正文第一行,最后一个分块大小为0
Location:重定向后的新位置
Set-Coolie:向客户端发送属于当前用户的Cookie信息即sessionid,还有一些其他信息,如Cookie超时失效日期
Referer:从哪个页面跳转到当前页面
在请求头部和响应头部中都有关于Cookie
的属性信息,那么什么是Cookie
?这里举个例子,假如说我们在网上购物,此时我们看中一件商品要将其加入购物车,因此为了区分用户需要进行登录,那么本次请求就是加入购物车请求我们需要登录一次,然后请求成功了,随后我们想要购买购物车中的商品,但是HTTP协议是无状态协议,本次请求与上次请求无任何关系,于是为了购买我们不得不再登陆一次,才能确认我们用户的身份,我们每次进行用户操作都要进行登录,十分麻烦。
之后大佬们想要让HTTP协议能够进行状态维持,于是加入了Cookie
帮助我们临时保存一些验证信息,于是情况就改变了。在我们第一次登录完毕后,服务端会在服务器内为客户端建立一个会话,生成一个会话id(sessionid
),并将会话id和用户信息保存起来,此时服务端会给客户端一个响应,响应信息的头部中就会有Set-Cookie
信息,其中存储着sessionid
和一些其他相关信息例如超时失效信息。客户端在收到服务端的sessionid
信息后会将信息保存在浏览器自己的Cookie
文件中,在下次再向服务器发信息时会先将与这个服务器对应的Cookie
文件中的信息全部读出放入请求信息中,这个信息就存放在请求头部的Cookie
中,其中主要就是sessionid
。在服务端获取sessionid
后就能通过这个id找到对应用户的用户信息,从而避免需要重复登录的情况发生。
这里还要关注一个问题Cookie
与session
的区别是什么?Cookie
是保存在客户端的,每次发起请求时会发送给服务端去寻找属于当前用户的session
;而session
是保存在服务端的,是为每个用户创建的一个会话,其中保存着对应的sessionid
和用户信息。
3、正文
在头部与正文之间用一个空行作为间隔,\r\n\r\n
即表示一个空行,当遇到两个连续的\r\n
时则认为头部结束了。正文则是一些数据信息。
实现HTTP协议服务器
不管什么信息发送至服务端,服务端同一回复学习是一种态度
。
HTTP服务器实际上是一个TCP服务器,接收到数据后打印出接收的数据,然后统一按照HTTP协议格式回复信息即可。
/**
* 实现一个简单的http服务器
* 这个代码用到了我们之前封装额tcp头文件
**/
#include "tcp_socket.hpp"
#include <iostream>
#include <sstream>
int main()
{
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind("0.0.0.0", 9000));
CHECK_RET(sock.Listen());
while(1)
{
TcpSocket cliSock;
if(sock.Accept(cliSock) == false)
{
continue;
}
std::string buf;
cliSock.Recv(buf);
std::cout << "req:[" << buf << "]" << std::endl;;
std::string body = "<html><body><h1>学习是一种态度</h1></body></html>";
body += "<meta http-equiv='content-type' content='text/html;charset=utf-8'>";
std::string first = "HTTP/1.1 200 OK";
std::stringstream ss;
ss << "Content-Length: " << body.size() << "\r\n";
ss << "Content-Type: " << "text/html" << "\r\n";
std::string head = ss.str();
std::string blank = "\r\n";
cliSock.Send(first);
cliSock.Send(head);
cliSock.Send(blank);
cliSock.Send(body);
cliSock.Close();
}
sock.Close();
}
我们用浏览器访问我们的服务器,服务端会打出以下数据。
[misaki@localhost httpserver]$ ./httpserver
req:[GET / HTTP/1.1
Host: 192.168.11.128:9000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7
]
req:[GET /favicon.ico HTTP/1.1
Host: 192.168.11.128:9000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://192.168.11.128:9000/
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7
由于我们打印了客户端发来的数据所以我们可以看到很多http请求数据。浏览器也会显示以下页面。
之后我们更改一下代码,用一下重定向。
/**
* 实现一个简单的http服务器
**/
#include "tcp_socket.hpp"
#include <iostream>
#include <sstream>
int main()
{
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind("0.0.0.0", 9000));
CHECK_RET(sock.Listen());
while(1)
{
TcpSocket cliSock;
if(sock.Accept(cliSock) == false)
{
continue;
}
std::string buf;
cliSock.Recv(buf);
std::cout << "req:[" << buf << "]" << std::endl;;
std::string body = "<html><body><h1>学习是一种态度</h1></body></html>";
body += "<meta http-equiv='content-type' content='text/html;charset=utf-8'>";
//std::string first = "HTTP/1.1 200 OK";
//这类改为重定向
std::string first = "HTTP/1.1 302 OK";
std::stringstream ss;
ss << "Content-Length: " << body.size() << "\r\n";
ss << "Content-Type: " << "text/html" << "\r\n";
ss << "Location: http://www.taobao.com/\r\n";
std::string head = ss.str();
std::string blank = "\r\n";
cliSock.Send(first);
cliSock.Send(head);
cliSock.Send(blank);
cliSock.Send(body);
cliSock.Close();
}
sock.Close();
}
再次使用浏览器访问,则会跳转到淘宝页面。
之后我们再次更改代码,这次使用404状态码。
/**
* 实现一个简单的http服务器
**/
#include "tcp_socket.hpp"
#include <iostream>
#include <sstream>
int main()
{
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind("0.0.0.0", 9000));
CHECK_RET(sock.Listen());
while(1)
{
TcpSocket cliSock;
if(sock.Accept(cliSock) == false)
{
continue;
}
std::string buf;
cliSock.Recv(buf);
std::cout << "req:[" << buf << "]" << std::endl;;
std::string body = "<html><body><h1>学习是一种态度</h1></body></html>";
body += "<meta http-equiv='content-type' content='text/html;charset=utf-8'>";
//std::string first = "HTTP/1.1 200 OK";
//这类改为重定向
//std::string first = "HTTP/1.1 302 OK";
//这次改为客户端错误
std::string first = "HTTP/1.1 404 OK";
std::stringstream ss;
ss << "Content-Length: " << body.size() << "\r\n";
ss << "Content-Type: " << "text/html" << "\r\n";
ss << "Location: http://www.taobao.com/\r\n";
std::string head = ss.str();
std::string blank = "\r\n";
cliSock.Send(first);
cliSock.Send(head);
cliSock.Send(blank);
cliSock.Send(body);
cliSock.Close();
}
sock.Close();
}
还是使用浏览器访问显示以下画面。
我们的返回状态码是404为什么也可以显示页面?浏览器默认错误页面是可以自定制的,我们这里返回的相当于是一个错误页面,于是浏览器也帮我们显示了出来。
其他状态码这里就不一一演示了。