目录
- 应用层:
- 自定制协议
- 实例
- HTTP协议
- 首行
- 头部
- 空行
- 正文
- http服务器的搭建
- HTTPS协议
- 传输层
- UDP协议
- TCP协议
应用层:
应用层负责应用程序之间的沟通—程序员自己定义数据的组织格式
应用层协议:如何将多个数据对象组织成为一个二进制数据串进行传输
需要考虑的要素:传输性能,解析性能,调试便捷性
序列化:将数据对象按照指定协议进行组织成实现持久化存储或者网络通信传输的二进制数据串的过程
反序列化:按照指定协议,将二进制数据串解析得到各个数据对象的过程
序列化方式:结构体二进制序列化、json序列化、protobuf序列化
自定制协议
自定制协议:程序员自己定制数据格式
实例
网络版计算器:
客户端将两个数字,以及运算符,传输给服务端
服务端对数据进行运算,将结果返回给客户端
具体结构在上一篇博客套接字部分,这里只是讲自定制协议
1.首先在tcpsocket头文件里面自定义数据格式
struct data_t
{
int num1;
int num2;
char op;
};
2.在tcp_cli客户端收发数据部分进行发送与接收
//4.收发数据
struct data_t tmp;
tmp.num1 = 11;
tmp.num2 = 22;
tmp.op = '+';
//方法一:调用自定义函数
//std::string buf;
//buf.resize(sizeof(struct data_t));
//memcpy(&buf[0], &tmp, sizeof(struct data_t));
//CHECK_RET(cli_sock.Send(buf));
//buf.clear();
//CHECK_RET(cli_sock.Recv(&buf));
//std::cout << *(int*)buf.c_str() << std::endl;
//方法二:
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;
3.在tcp_srv服务端收发数据部分进行接收与发送
//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 = 0;
}
send(fd, &result, sizeof(int), 0);
HTTP协议
HTTP协议:超文本传输协议
http协议格式:http的协议实现,http协议的数据结构
首行:请求行,响应行(对于请求与相应的简单关键描述)
头部:对于请求或者响应或者正文的一些关键描述
由一个个键值对组成key:val, 每个键值对以\r\n结尾
空行:\r\n,间隔头部与正文;\r\n\r\n – 头部结尾
正文:客户端提交给服务端,或者服务端响应给客户端的数据
首行
-请求行:请求方法,URL(URI),协议版本\r\n
请求方法:
GET:从服务器获取实体资源,请求没有正文,但是也可以提交数据,但是提交的数据没有在正文中而是在URL中
1.get提交数据不安全;2.URL长度有限制
HEAD:功能与GET类似,但是不要正文实体
POST:向服务端提交数据,请求有正文,数据放在正文中
URL:网址–统一资源定位符–用于定位网络中某个主机上的某个资源
组成:协议名称://用户名:密码@域名:端口/资源路径?查询字符串
https://user:password@www.baidu.com:80/s?ie=utf-8&wd=ASCII
域名:服务器别名–最终访问服务器需要经过域名解析得到服务器IP
资源路径:这个路径是一个相对根目录
查询字符串:提交给服务器的数据,由一个个key=val形式键值对组成,键值对之间以&符号间隔
urlencode:编码-用户请求的资源路径,或者查询字符串中存在特殊字符,则有可能与url中的特殊字符冲突;将特殊字符每个字节转换为16进制数字字符,并前缀%
urldecode:解码-遇到%则认为紧随其后的两个字符进行了编码,将这两个字符转换为数字,第一个数字左移4位加上第二个数字
协议版本:0.9,1.0,1.1,2
0.9:最早期版本,只支持GET方法,并且协议还没有当前的规范,只支持超文本数据传输。
1.0:规范了http协议格式,并且新增支持GET,HEAD,POST请求方法,支持各种多媒体资源传输,简单的缓存控制。
1.1:更多的是对1.0版本进行性能的优化,支持了更多请求方法以及特性(支持长连接,更加完善的缓存控制,分块传输…)
2.0:因为http协议的庞大冗余,因此2.0不是新增特性,而是重新定义http协议:①使用二进制数据传输;②支持主动推送资源;③服务器进行长连接响应,不需要按序进行…
Connection:用于控制长连接的打开关闭状态 keep-alive/close
-响应行:协议版本,响应状态码,状态码描述\r\n
响应状态码:直观向客户端反馈处理结果
1××:一些描述信息;101-协议切换状态码
2××:表示本次请求正确处理;200
3××:重定向-表示本次请求的资源移动到了新的链接处,并且原链接依然可用;301/302
4××:表示客户端错误;404
5××:表示服务器错误;500-服务器内部错误;502-代理服务器没有收到正确响应;504-超时
状态码描述:就是针对状态的文字描述
头部
头部:关于请求或者响应,或者正文的一些描述字段
组成:key:val\r\n key:val\r\n
典型头部字段:
Connection:长短连接控制;keep-alive/close
Referer:记录本次请求的来源链接
Content-Type:用于表示正文的数据格式
Content-length:用于表示正文的长度–http解决粘包问题的关键字段
Location:用于指定重定向的新链接地址,与3××搭配使用
cookie与session:涉及的头部字段请求头Cookie,响应头Set-Cookie
http协议是一个无状态协议
1.一个客户端登陆之后,服务端验证登录,成功后,通过Set-Cookie字段设置cookie信息(用户信息,状态…)返回给客户端
2.客户端收到响应后,将Set-Cookie字段的cookie信息保存起来,下次请求服务器的时候从cookie文件中读取出cookie信息,通过Cookie字段发送给服务器。
cookie是一个维护http通信状态的技术–但是存在安全隐患。
解决方案:session
session是服务端针对每个客户端所建立的会话,当客户端登录成功后,创建会话,在会话中记录客户端用户信息以及状态…,通过Set-Cookie字段将session_id返回给客户端
session_id每次请求都会发生变化,并且用户的隐私信息一直保存在服务器防止泄露。
cookie与session的区别:
cookie是维护http通信状态的技术,将关键信息保存在客户端,每次请求服务器时,读取出来发送给服务端(存在安全隐患)
session是解决cookie安全隐患的技术,将关键信息保存在服务器,将sessionid发送给客户端,作为cookie保存起来,解决了cookie泄密的风险。
空行
空行:\r\n
是与头部最后一个字段的结尾\r\n组成连续的\r\n\r\n作为特殊标志
作为http头部结尾的标志
正文
针对不同的传输所提交的数据或服务端响应的数据,以什么格式我们自己来定。
http服务器的搭建
http是一个应用层协议,只是应用程序如何沟通的一种数据格式约定,在传输层是基于tcp实现的 http客户端实际上就是一个tcp客户端;http服务器实际上就是一个tcp服务器,只不过http客户端与服务端的通信使用的是http协议来约定数据格式而已。
简单的http服务器的搭建:
1.搭建tcp服务端
2.获取新建连接
3.使用新建连接,等待接收数据(http协议的请求数据)
4.接收过程:先接受http头部,解析头部-Content-Length确定正文长度
5.接收指定长度的正文
6.根据请求方法以及资源路径确定客户端的请求目的
7.进行具体对应的业务处理
8.组织http协议格式的响应数据,对客户端进行回复
9.如果是短连接,则直接关闭套接字,如果是长连接,则继续等待接收数据
#include "tcpsocket.hpp"
#include<sstream>
int main(int argc, char *argv[])
{
//通过程序运行参数执行服务端要绑定的地址
//./tcp_srv 192.168.2.2 9000
if(argc != 3)
{
printf("usage: ./tcp_src 10.106.200.199 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;
}
//------------------------------------------------------http服务端接收数据----------------------------------------------
std::string buf;
clisock.Recv(&buf);
std::cout << "request:[" << buf << "]\n";
std::string body = "<html><body><h1>Hello ysy</h1></body></html>";
std::stringstream ss;
//首行
ss << "HTTP\1.1 200 OL\r\n";
//头部
ss << "Connection: close\r\n";
ss << "Content-Length:" << body.size() << "\r\n";
ss << "Content-Type: text/html\r\n";
//空行
ss << "\r\n";
//正文
ss << body;
clisock.Send(ss.str());
clisock.Close();
//------------------------------------------------------http服务端接收数据----------------------------------------------
}
//6.关闭套接字
lst_sock.Close();
return 0;
}
注意:http服务器编写完毕之后
云服务器:记得设置安全组策略,开启对应端口
虚拟机:记得关闭防火墙 sudo systemctl stop firewalld
//302重定向
std::string buf;
clisock.Recv(&buf);
std::cout << "request:[" << buf << "]\n";
std::string body = "<html><body><h1>Hello ysy</h1></body></html>";
std::stringstream ss;
//首行:302重定向
ss << "HTTP\1.1 302 OL\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();
HTTPS协议
HTTPS协议:并不是一个新的协议,而是在HTTP协议基础上进行了一层加密
https协议就是基于ssl进行加密实现加密传输
HTTPS=HTTP+SSL:
目的:实现数据的安全传输
安全传输:需要考虑两个问题
1.身份验证问题:防止伪装
2.数据加密问题:防止监听
身份验证实现:
原理:使用第三方权威机构进行身份验证(CA证书)
CA认证:通信双方在通信前先到权威机构请求给自己颁发一个CA证书。CA证书(权威机构信息,自己的信息,…),通信两方建立连接后,在通信之前先将证书发送给对方,收到对方的证书后,查看这个权威机构是否是自己新人的权威机构,如果是,则到权威机构进行这个公司的身份验证。验证通过后进行通信,不通过可以自行选择是否添加信任。
身份验证通过,但是通信依然有可能被监听,存在风险,因此需要加密传输。
加密传输实现:
①.对称加密:加解密使用相同的秘钥。
优点:加解密效率高
缺点:秘钥一旦被劫持则加密形同虚设
②.非对称加密:加密和解密使用不同的秘钥
思路:通信中,每一端在通信前都可以生成一对秘钥(公钥和私钥),在通信前,将公钥发送给对方,对方使用收到的公钥进行加密数据然后传输,自己收到数据后,使用私钥进行解密。
实现算法:RSA加密算法
优点:安全度更高
缺点:加解密效率低下
③.混合加密:
通信双方,先使用非对称加密保护对称秘钥协商过程。对称秘钥协商完毕后,使用对称秘钥加密传输。
https加密流程:(将身份验证(CA认证)与加密传输(混合加密)合在一起使用)
1.通信双方生成一对秘钥,使用公钥信息到权威机构生成一个CA证书
2.通信建立连接后,数据传输前,将证书先传输给对方
3.双方收到对方的证书后,进行解析,得到机构信息以及公钥信息
4.对机构信息进行验证
5.身份验证通过后,使用公钥加密数据(随机数+对称加密算法列表),发送给对方
6.双方收到对方使用公钥加密的数据后,使用私钥进行解密
7.给对方也发送一个随机数+对称加密算法列表
8.通过双方的随机数与算法列表生成一个对称秘钥
9.使用这个协商的对称秘钥加密通信
传输层
应用层协议目的是了解指定协议的实现便于我们以后使用
传输层:负责应用程序之间的数据传输–TCP/UDP
了解协议的实现,体会协议的特性,理解对于上层程序编程的影响
UDP协议
UDP协议格式:
16位源端-对端端口:用于描述识别通信两端进程
16位数据报(UDP)长度:能够存储最大数字65535–udp报文总大小最大不能超过64k
16位校验和:采用二进制反码求和算法–校验接收方接收到的数据与发送方发送的数据是否一致。
协议特性:
无连接:通信时不需要建立连接,只要知道对方地址就可以直接发送数据
不可靠:不保证数据安全、有序到达对端。
面向数据报:无连接的,不可靠的,无序的,有最大长度限制的传输方式
1.ucp传输时,sendto发送的数据会被发送到发送缓冲区中,而udp协议并不会等待,而是直接对数据封装头部,进行发送
2.udp传输时,收到的数据会被放到接收缓冲区中,而udp保证数据是整条交付,不会出现半条或多条交付。
编程影响:
1.不保证安全到达:需要会在应用层使用tcp所使用的一些机制实现
2.不保证有序到达:需要在应用层进行包序管理
3.udp报文有最大长度限制:报文最大长度小于64k,因此发送大块数据的时候,需要在应用层进行数据分包进行发送
4.udp实现的是整条交付:因此接收方revcfrom所给予的buf(定义的缓冲区)足够大,能够一次性取出一条数据。
TCP协议
tcp协议格式:
16位源端-对端端口:描述识别两端
32位序号-确认号:进行包序管理
4位头部长度:以4字节为单位,表示tcp报头最大有60字节,最小20字节
6位保留:
6位标志位:
URG:紧急指针是否有效;
ACK:表示响应;
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走;
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段;
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段;
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段;
16位窗口大小:实现华东窗口机制
16位校验和:校验接收的数据与发送的数据是否一致
16位紧急指针:标识哪部分数据是紧急数据
协议特性:面向连接,可靠传输,面向字节流
面向连接:连接管理+状态管理
连接管理:
三次握手连接建立> 四次挥手断开连接:
问题:
1.为什么握手是三次,而挥手是四次?
握手:tcp是一个全双工通信,两端都必须确认对方是否具有数据收发的能力。两次不安全,四次没必要。
挥手:fin包只能表示对方不再发送数据了,不代表对方不再接收数据,因此有可能被动关闭方还会继续发送数据,因此这种情况下就不能直接发送fin包,而是等待上层用户不在发送数据了,调用close或者shutdown才会发送fin包。因此被动关闭方的ack和fin并不是一起发送的。
2.tcp三次握手失败了,两端是如何处理的?
服务端回复ack+syn后,如果超时得不到ack回复,则会发送rst重置连接报文,然后释放新建套接字。
3.TIME_WAIT有什么用?
TIME_WAIT是主动关闭方在断开连接过程中进行最后一次ack回复后进入的状态。如果主动关闭方回复最后一次ack后直接释放资源,就有可能在新的客户端中使用原来的这个地址信息,在上次ack丢失的情况下收到重传的fin对新连接造成影响。TIME_WAIT更多的是为了保护客户端启动不会使用刚关闭的套接字地址信息,进而不会受到上个套接字的通信遗留问题影响。
详细说明:如果被动关闭方没有收到最后一次回复,则会重传fin;假设没有TIME_WAIT,主动关闭方最后一次恢复后直接释放资源。这时候如果客户端重新启动了一个套接字使用的刚好是之前的地址信息,这个套接字就有可能收到重传的这个fin。被动关闭方一致等待最后一个ack,但是这时候如果新的客户端启动发送syn,则与状态对应不上。
TIME_WAIT等待2个MSL时间之后,释放资源,等待两个msl是为了保证能够对重传的fin进行处理,以及保证本次通信的所有数据都消失在网络中不会对新的连接造成影响。
4.一台主机上出现了大量的TIME_WAIT是什么原因?如何解决?
TIME_WAIT是套接字主动关闭连接,最终进入的状态,因此一台主机出现大量TIME_WAIT意味着大量的主动关闭了套接字。
解决方法:
①.减少TIME_WAIT等待时间
②.使用套接字选项:地址复用
int setsockopt(int fd, int level, int optname, void* optval, int optlen);
level:SOL_SOCKET;optname:SO_REUSEADDR – 地址复用
5.一台主机上出现了大量的CLOSE_WAIT是什么原因?如何解决?
close_wait是被动关闭方,在收到fin包进行ack回复之后进入的状态。这个状态是等待上层用户层调用close/shutdown(wr)后发送fin包的一个状态。
如果主机上出现了大量的close_wait的连接,那么意味着可能在代码中没有关闭套接字。
TCP连接保活机制:
通信两端在长时间没有数据通信的情况下,服务端会每隔一段时间向客户端发送一个保活探测数据包(要求对方进行回复),若连续多次没有收到回复,则认为连接已经断开。
默认7200s,每隔75s,9次无回复则断开
这些数值可以通过设置套接字选项进行设置。
连接断开对于程序的影响:recv返回0,send会触发SIGPIPE异常
可靠传输:保证数据有序,安全到达对端
1.面向连接
2.协议字段中的序号与确认序号,进行包序管理,实现有序传输。
3.确认应答机制:接收方针对收到的每一条数据进行确认回复
4.超时重传机制:发送数据等待超时时无确认回复后,认为数据丢失进行重传。
5.协议字段的校验和字段:校验数据一致性,不一致则丢弃,发送重传请求
6.避免丢包:
①.发送方发送数据过快,过多,导致接收方接收缓冲区满溢导致丢包
解决方案:滑动窗口机制–进行流量控制
每一方都会有一个:接收窗口,发送窗口
发送窗口:后沿:发送起始序号;前沿:结束发送位置。前沿减后沿不能大于对方的窗口大小
接收窗口:后沿:起始接收序号;前沿:结束接受位置。前沿减后沿不能大于剩余空间大小
停等协议:收到确认回复后才会发送下一条
回退n步协议:从丢失的数据开始进行重新传输
选择重传协议:哪条丢了就重传哪条
②.传输过程中,网络状态突然不好,导致大量丢包重传
解决方案:拥塞机制 – 以慢启动快增长的形式进行传输
实现原理:发送方维护一个拥塞窗口,用于限制当前所能发送的数据大小,而这个拥塞窗口以指数层级增长,实现网络探测,防止传输过程中网络状态突然变差继续大量发送导致的大量丢包。
7.性能的提升:避免无谓的一些性能损失
确认序号:是告诉发送方确认序号之前的所有数据都已经接收成功,避免因为中间的某个确认回复丢失而导致的重传。
快速重传机制:接收方在接收数据时,先接收到了后发的数据,则认为前边的数据有可能丢失了,则连续间隔发送三条前边数据的重传请求(确认序号为丢失的数据起始序号),发送方收到连续三条重传请求,主要目的是为了减少超时等待时间。三条重传请求是为了防止延迟到达的情况。
延迟应答机制:接收方收到数据后,延迟确认回复
接收方接收数据后如果立即进行回复,大概率窗口大小都会变小,延迟应答是为了在延迟期间上层可能将数据取出,尽量保证窗口大小,保证传输吞吐量。
捎带应答机制:将确认回复的信息,放到即将要发送的数据报头中,捎带一块传输给对方,尽可能减少纯报头的确认回复(一个报头至少20字节)
TCP如何实现可靠传输:
1.可靠传输:面向连接,包序管理,确认应答,超时重传,校验和
2.避免丢包:滑动窗口,拥塞机制
3.提高性能:延迟应答,捎带应答,延迟发送
UDP如何实现可靠传输:
在应用层用过tcp实现可靠传输的机制来实现
面向字节流:基于连接,可靠的,有序的,双向的一种字节流(以字节为单位)
TCP要发送的数据都会被放到发送缓冲区中,通信时tcp会从缓冲去中取出合适大小的数据(不大于mss大小),封装头部进行发送。
好处:传输比较灵活
坏处:会产生粘包问题 – 将多条数据当做一条进行处理-无法分辨数据边界
本质原因:tcp并不维护数据边界
解决方案:则需要程序员在应用层进行数据边界管理–区分数据边界
具体技术:特殊字符,数据定长,应用层头部加上数据长度字段
UDP不存在粘包问题。数据整条交付