Linux网络——应用层协议教程
目录
协议
协议
- socket api(套接字)的接口,在读写数据时都是按照 01 二进制的方式来接收的,那么当我们要传输一些结构复杂的数据时,该怎么办呢?
- 这时候就需要用到协议了,协议就是一种约定,约定好网络通信中请求的协议与响应的协议,这样我们在进行通信时,按照相应的协议来组织数据就可以保证通信两端正常接收发送数据;
- 应用层协议概念:负责应用程序之间的沟通,它的功能主要是将多个数据对象组织成为一个二进制数据串进行传输,其中最知名的协议——HTTP 协议;
- 自定制协议概念:由程序员自己定制数据格式,但是定制的同时需要考虑传输性能、解析性能、调试便捷性,以及如何组织更加适用于当前的应用场景;
- 序列化:将数据对象按照指定协议进行组织实现持久化存储或者网络通信传输的二进制数据串的过程;
- 反序列化:按照指定协议,将一个二进制数据串解析得到各个数据对象的过程;
- 序列化方式:结构体二进制序列化、json、protobuf…
自定制实例
- 接下来以一个计算器的例子来说明,我们简单模拟一下计算器中的加法操作,在组织数据时采用定义结构体来表示我们需要交互的信息,发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体,这个过程叫做 “序列化” 和 “反序列化”;
- 数据组织格式:以结构体来存放数据,结构体中有三个成员变量,分别为数据 1、数据 2、运算符;
struct data_t{
int num1;
int num2;
char op;
};
- 客户端操作:在向服务端发送请求时,先将数据按照结构体的格式组织好,然后发送给服务端;
#include "tcpsocket.hpp"
int main(int argc, char *argv[]){
//通过参数传入要连接的服务端的地址信息
if (argc != 3) {
printf("usage: ./tcp_cli srvip srvport\n");
return -1;
}
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket cli_sock;
//1. 创建套接字
CHECK_RET(cli_sock.Socket());
//2. 绑定地址信息(不推荐)
//3. 向服务端发起连接
CHECK_RET(cli_sock.Connect(srvip, srvport));
//4. 收发数据
struct data_t tmp;
tmp.num1 = 11;
tmp.num2 = 22;
tmp.op = '+';
//因为TCP套接字类中的发送数据接口是进行string对象的发送,所以结构体类型的数据我们在外部自己发送
int fd = cli_sock.GetFd(); //拿到套接字描述符
send(fd, (void*)&tmp, sizeof(struct data_t), 0);
int result;
recv(fd, &result, sizeof(int), 0);
std::cout << result << std::endl;
//5. 关闭套接字
CHECK_RET(cli_sock.Close());
return 0;
}
- 服务端操作:接收到客户端发送的数据后,按照结构体格式解析数据,然后对数据进行运算后将结果发回给客户端;
#include "tcpsocket.hpp"
int main(int argc, char *argv[]) {
//通过程序运行参数指定服务端要绑定的地址
// ./tcp_srv 192.168.2.2 9000
if (argc != 3) {
printf("usage: ./tcp_src 192.168.2.2 9000\n");
return -1;
}
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket lst_sock;//监听套接字
//1. 创建套接字
CHECK_RET(lst_sock.Socket());
//2. 绑定地址信息
CHECK_RET(lst_sock.Bind(srvip, srvport));
//3. 开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//4. 获取新建连接
TcpSocket clisock;
std::string cliip;
uint16_t cliport;
bool ret = lst_sock.Accept(&clisock, &cliip,&cliport);
if (ret == false) {
continue;
}
std::cout<<"get newconn:"<< cliip<<"-"<<cliport<<"\n";
//5. 收发数据--使用获取的新建套接字进行通信
int fd = clisock.GetFd();
struct data_t tmp;
recv(fd, &tmp, sizeof(struct data_t), 0);
int result;
//按照结构体格式拿到数据然后返回
if (tmp.op == '+'){
result = tmp.num1 + tmp.num2;
}else {
result = 100;
}
send(fd, &result, sizeof(int), 0);
}
//6. 关闭套接字
lst_sock.Close();
}
HTTP协议
概念
- 概念:超文本传输协议,是一个明文字符串传输协议,是在传输层基于 TCP 协议实现的,是一个简单的请求——响应协议;
协议格式
概念:HTTP 的协议格式也叫 HTTP 协议的数据结构,它是 HTTP 协议的实现方式,主要分为四个部分;
- 首行:请求行、响应行,对于请求与响应的简单关键描述;
- 头部:对于请求、响应以及正文的一些关键描述,由一个个键值对组成
key: val
,每个键值对以\r\n
结尾; - 空行:
\r\n
——间隔头部与正文,\r\n\r\n
——头部的结尾; - 正文:客户端提交给服务端,或者服务端响应给客户端的数据;
首行
请求行
- 请求行格式:
请求方法 URL/URI 协议版本\r\n
; 请求方法:发展到现在为止,HTTP 请求中一共有有以下 9 种请求方法;
GET
:从服务器获取实体资源,响应后会获取正文实体,因为请求没有正文,如果请求过程中需要提交数据,那么提交数据不在正文中而是在 URL 中;(重要)- GET 提交数据在 URL 中,所以会显示出来,不安全;
- 因为 URL 长度有限制,所以提交的数据也有长度限制;
HEAD
:功能与 GET 类似,但是获取的资源没有正文实体,只有头部;(重要)POST
:向服务端提交数据,数据放在正文中;(重要)PUT
:从客户端向服务器传送的数据取代指定的文档的内容;DELETE
:请求服务器删除指定的页面;CONNECT
:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器;OPTIONS
:允许客户端查看服务器的性能;TRACE
:回显服务器收到的请求,主要用于测试或诊断;PATCH
:是对 PUT 方法的补充,用来对已知资源进行局部更新;
URL:俗称网址,学名统一资源定位符,用于定位网络中某个主机上的某个资源(URI:统一资源标识符,URL 是 URI 的一个子集);
- 格式:
协议名称://用户名:密码@域名:端口/资源路径?查询字符串#片段标识符
; - 协议名称:HTTP 或者 HTTPS;
用户名:密码
:这个相当于在访问服务器时传入用户名密码,因此在访问时就直接登陆了,这样做安全性不高,所以很少使用;- 域名:服务器的别名,最终访问服务器需要经过域名解析得到服务器 IP 地址,我们也可以直接写入服务器的 IP 地址;
- 端口:服务器中浏览器的端口是默认固定的,HTTP——80 端口,HTTPS——443 端口,所以一般可以不添加,浏览器会自动添加的,如果是一些其他自定义端口,则需自行添加;
(注意,一般在请求行的 URL 中只会显示从文件路径开始往后的字符,而 IP 地址以及端口都会在头部字段Host
中显示) - 资源路径:这个路径是一个相对根目录路径,并不是绝对路径,在访问时会进行转换,保证其他目录下数据的安全;
查询字符串:提交给服务器所想查询的数据,由一个个
key=val
键值对组成,键值对之间以&
间隔;urlencode
编码:用户请求的资源路径或者查询字符串中存在的特殊字符可能会与 URL 中用来间隔的特殊字符冲突,此时就需要将特殊字符的每个字节转换为 16 进制数字字符,并前缀%
,例如:C++
——>C%2b%2b
;urldecode
解码:查找%
,然后将%
后的两个 16 进制字符转换为 ASCLL 码值即可(第一个数字左移四位加上第二个数字);
- 片段标识符:当你一打开某个网站的时候,它会直接定位到具有该片段标识符文字的位置;
- 格式:
协议版本
- 0.9 版本:最早期的版本,协议还不规范,只支持 GET 方法、只支持超文本数据传输;
- 1.0 版本:规范了 HTTP 协议格式,新增了 GET、HEAD、POST 请求方法,并且支持了各种后续发展的操作:多媒体资源传输、简单的缓存控制;
- 1.1 版本:更多的是对 1.0 版本进行性能的优化,支持了更多请求方法(post,get,head,options,put,delete,trace,connect)和特性(支持长连接、更加完善的缓存控制、分块传输等等);
- 2.0 版本:因为 HTTP 协议的庞大冗余,因此 2.0 版本不再是新增特性,而是重新定义 HTTP 协议,在该版本中采用二进制数据传输,支持服务器推送依赖资源,并且长连接的响应不需要按序进行等等;
- 短连接:建立连接 ——> 发送请求 ——> 得到响应 ——> 断开连接,当请求的资源中有一些其他依赖资源,那么需要再次发送请求,不过需要再次建立连接才可;
- 长连接:建立连接 ——> 发送请求 ——> 得到响应 ——> 发送依赖资源请求 ——> 得到响应 ——> 断开连接;虽然可以连续发送请求,但是后一个请求的发送必须得在前一个请求得到响应之后,这样效率并不高;
- 管线化长连接:建立连接 ——> 发送请求1 ——> 发送依赖资源请求2 ——> 得到响应1 ——> 得到响应2 ——> 断开连接;虽然可以连续发送请求,但是响应的返回必须是按照请求的顺序进行返回的,缺陷:请求 1 响应慢,请求 2 响应快,但是响应 2 不能立马返回,因为 响应 1 并没有返回,这就导致性能并没有大幅度提升;
- 主动推送依赖资源:建立连接 ——> 发送请求1 ——> 得到响应1 ——> 依赖资源自动响应2 ——> 依赖资源自动响应3 ——> 断开连接;该连接可以在请求的同时主动推送其他依赖资源,并且支持对响应返回顺序不需要按序进行,这使得性能得到了极大提升;
响应行
- 响应行格式:
协议版本 响应状态码 状态码描述\r\n
; - 协议版本:上面的请求行中介绍了;
响应状态码:直观的向客户反馈处理结果;
- 1xx:一些描述信息,表示接收的请求正在处理,101——协议切换状态码;
- 2xx:成功状态码,表示本次请求正确处理,200——OK;
- 3xx:重定向状态码,表示本次请求的资源移动到了新的链接处,但是原链接依然可用,302——临时重定向,301——永久重定向;
客户端请求原链接的时候,响应行会返回 3xx,并且会在头部字段 Location 中返回新的链接,并且客户端会再次发送对新链接的请求,当第二次请求该资源时,如果是临时重定向,那么就会请求原链接,如果是永久重定向,那么会直接请求新链接(这个在第一次请求后会有保存); - 4xx:客户端错误码,表示客户端的错误,服务器无法处理请求,404——服务器找不到请求的网页;
- 5xx:服务端错误码,表示服务器的错误,服务器处理请求出错,500——服务器出现内部错误,502——响应错误,504——响应超时;
- 贴上两篇相关博客:HTTP常用的14种状态码、常见的HTTP状态码
- 状态码描述:针对响应状态码的文字描述,这个不能准确知道响应状态是什么,只能靠响应状态码来准确判断响应状态是什么;
头部
- 头部格式:
key: val\r\nkey: val\r\n...
; Connection
:长短连接的控制,keep-alive——长连接,close——短连接;Referer
:记录本次请求的来源链接;Content-Type
:是服务器向客户端发送的头,代表内容的媒体类型和编码格式,如果是二进制流类型或者这个字段写错了,那么就是下载文件;Content-Length
:用来表示正文的长度,这个可以用来解决粘包问题;Location
:用于指定重定向后的新链接地址;cookie 与 session:HTTP 本身是一个无状态协议,但是有的时候有需要确定客户端(用户)的信息,所以出现了该头部字段;
cookie:涉及的头部字段,请求头——
Cookie
,响应头——Set-Cookie
;- 一个客户端请求登录,服务端验证登录,成功后通过
Set-Cookie
字段设置 cookie 信息(用户信息、状态…),然后返回给客户端; - 客户端收到响应后,将返回的 cookie 信息保存在 cookie 文件中,下次请求服务器的时候从 cookie 文件中读出信息,然后通过
Cookie
字段发送给服务器,这样就完成了一个用户状态确定; - cookie 是一个维护 HTTP 通信状态的技术,但是存在安全隐患,用户的信息容易泄露;
- 一个客户端请求登录,服务端验证登录,成功后通过
session:为了解决上面的问题,就出现了 session;
- session 是服务端只对每一个客户端所建立的会话,当客户端成功登录后,服务端会创建会话,生成一个新的会话 id,并且会保存下用户的信息以及状态…,然后通过
Set-Cookie
字段将会话 id 返回给客户端; - 客户端收到响应后,将返回的会话 id 保存在 cookie 文件中,下次请求服务器的时候从 cookie 文件中读出会话 id,然后通过
Cookie
字段发送给服务器,这样在确定了用户状态的情况下也保证了信息的安全,因为就算被截获了,也只是一串 id 数字而已;
- session 是服务端只对每一个客户端所建立的会话,当客户端成功登录后,服务端会创建会话,生成一个新的会话 id,并且会保存下用户的信息以及状态…,然后通过
- 贴上两篇相关博客:HTTP常用标准请求头字段、鲜为人知的HTTP协议头字段详解大全
空行
- 头部结尾会有一个
\r\n
,而空行也是一个\r\n
,所以当出现连续的\r\n
(\r\n\r\n
)时,也就意味着头部结束了;
正文
- 此次请求的需求,响应回复的数据;
HTTP 服务器
- HTTP 是一个应用层协议,它只是应用程序间如何沟通的一种数据格式约定,在传输层是基于 TCP 实现的,因此 HTTP 客户端实际上就是一个 TCP 客户端,HTTP 服务器实际上就是一个 TCP 服务器,只不过他们在进行通信时是按照 HTTP 协议来约定数据格式;
简单的 HTTP 服务器的搭建:
- 搭建 TCP 服务器;
- 获取新建连接;
- 使用新建连接接收数据,发送来的是 HTTP 协议格式的请求数据;
- 接收过程:先接收 HTTP 头部,解析头部中
Content-Length
字段获取到正文长度; - 接收指定长度的正文;
- 根据请求方法以及资源路径确定客户端的请求目的;
- 进行具体的业务处理;
- 处理完毕后,按照 HTTP 协议格式来组织响应数据,回复给客户端;
- 如果是短连接,则直接关闭套接字,如果是长连接,则继续等待接收数据;
实例
- 下面实现一个简单的 HTTP 服务器,它所完成的业务就是返回一个静态页面,内容为——
hello world
,另外该代码中使用了一个 TCP 服务器的头文件,这个我在Linux网络编程——套接字这篇博客中已经实现了,所以在这就不放代码了;
#include "tcpsocket.hpp"
#include <sstream>
int main(int argc, char *argv[]){
//利用命令参数来设置IP地址以及端口号,形如:./tcp_srv 192.168.2.2 9000
if (argc != 3) {
printf("usage: ./tcp_src 192.168.2.2 9000\n");
return -1;
}
std::string srvip = argv[1]; // ip地址
uint16_t srvport = std::stoi(argv[2]); //端口号
TcpSocket lst_sock;// 创建TCP套接字对象
//初始化监听套接字
CHECK_RET(lst_sock.Socket());
//为监听套接字绑定地址信息
CHECK_RET(lst_sock.Bind(srvip, srvport));
//开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//获取新建连接
TcpSocket clisock;
std::string cliip;
uint16_t cliport;
bool ret = lst_sock.Accept(&clisock, &cliip,&cliport);
if (ret == false) {
continue;
}
//接收数据
std::string buf;
clisock.Recv(&buf);
//我们将http请求信息打印字在终端上
std::cout << "request:["<<buf<<"]\n";
//下面开始填充响应信息
std::string body;
body = "<html><body><h1>Hello Bit</h1></body></html>"; //正文
std::stringstream ss;
ss << "HTTP/1.1 500 OK\r\n"; //响应首行
ss << "Connection: close\r\n"; //头部:短连接
ss << "Content-Length: " << body.size() << "\r\n"; //头部:正文长度
ss << "Content-Type: text/html\r\n"; //头部:正文格式
//下面这行为重定向,如果设置了,那么就会跳转到所设置的服务器
//ss << "Location: http://www.baidu.com\r\n"; 头部:重定向
ss << "\r\n"; //空行
ss << body;
//发送数据
clisock.Send(ss.str());
//关闭新建连接套接字
clisock.Close();
}
//关闭监听套接字
lst_sock.Close();
}
注意:
- 如果是在云服务器上完成的这个简单 HTTP 服务器,因为云服务器为了保证安全,所以会关闭所有端口的访问,所以需要先设置安全组策略,开启对应端口;
- 如果实在本地虚拟机上完成的这个简单 HTTP 服务器,那么需要关闭防火墙,命令-
sudo systemctl stop firewalld
,不过即使你这样做了,那么你也只能在自己电脑的浏览器上访问服务器,因为本地虚拟机是私网,不是公网,所以只能自己和自己玩,如果想要同一网段下的其他主机对该服务器进行访问,需要一定的配置,这个自行查阅。
当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »