●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
📌📌📌为您导航📌📌📌
- 1.HTTP协议
-
- 1.1URL
- 1.2HTTP协议格式
-
- 1.2.1HTTP请求格式
- 1.2.2HTTP响应格式
- 1.3HTTP封装和解包
- 1.4HTTP方法
- 1.5HTTP状态码
- 1.6cookie和session
- 1.7HTTP报文属性
- 1.8HTTP简易实现
- 2.传输层
-
- 2.1再谈端口号
- 2.2端口号范围划分
- 2.3常见查看网络状态命令
- 2.4UDP协议
-
- 2.4.1UDP协议段格式
- 2.4.2UDP的特点
- 2.4.3UDP的缓冲区
- 2.4.4基于UDP的应用层协议
- 2.5TCP协议
-
- 2.5.1TCP协议段格式
- 2.5.2TCP缓冲区
- 2.5.3超时重传机制
- 2.5.4连接管理机制
1.HTTP协议
1.1URL
平时我们俗称的 “网址” 其实就是说的 URL。
前面说过,IP + Port
可以唯一的确定网络中的一个进程,但是我们无法唯一确定一个资源。我们所谓的网络资源,一定是存在于网络中的一台Linux机器上,Linux保存资源的方式,都是以文件的方式保存的,而Linux标识唯一 一个资源的方式是通过路径。所以我们就通过IP+Linux路径
来唯一确认一个网络资源,也就是URL。
那URL由什么组成?
来看下面这个例子
http://www.xxx.com:80/dir/index.htm?uid=1#ch1
http
:使用的协议
www.xxx.com
:服务器地址
80
:端口号
dir/index.htm
:带层次的文件路径
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了。因此这些字符不能随意出现。
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
例如:
A7FFE8B4
转义后
%A7%FF%E8%B4
1.2HTTP协议格式
1.2.1HTTP请求格式
首先我们要知道的是,无论是请求还是响应,http基本上都是按照行为单位
进行构建请求或者响应的。
请求行+请求报头+空行我们称为http请求报头
。
请求正文我们称为有效载荷
。
1.2.2HTTP响应格式
响应行+响应报头+空行我们称为http响应报头
。
响应正文我们称为有效载荷
。
1.3HTTP封装和解包
首先,http的读取和发送都是把协议内容处理成一个大字符串
,例如:
xxxx\n
yyyy\n
zzzz\n
处理后
“xxxx\nyyyy\nzzzz\n”
http格式里有一个空行,这个空行将http一分为二,当读到空行,我们就知道我已经把报头部分给读完了。
也就是这样一个过程,先读第一行,把请求行读完了,然后不断往下读,直到读到空行,说明报头部分也被读完了,接下来读取正文部分,那么问题来了,读取正文的时候要怎么判定什么时候读取结束???实际上在报头中有一个Content-Length
属性,它的大小即为正文的大小,所以我们可以根据这个来决定什么时候读取结束。(没有正文情况不存在Content-Length)在这里还要在补充一个http报头里经常会出现的属性:Content-Type
,它描述的是正文的类型。
1.4HTTP方法
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪的路径 | 1.1 |
我们可以通过GET / HTTP/1.0
这种方法来获取资源,这里特别注意/
并不是我们之前所说的根目录,而是web根目录。说的简单点,如果这样写默认请求的就是网站的首页
。
GET和POST的区别
GET:
如果提交参数(假设是网页中提交表单信息),GET是通过URL的方式进行提交的。例如:GET /a/b/xxxxxname=hello&passwd=123456 HTTP/1.1
。GET方法不私密,会将重要信息回显到URL的输入框。GET方法通过URL提交参数,而URL是有大小限制的。
POST:
如果提交参数(假设是网页中提交表单信息),POST是通过正文提交参数的。例如:name=hello&passwd=123456
这个会出现在正文内容里,所以说POST方法更私密(私密并不等于安全)。POST方法通过正文提交参数,一般正文没有大小限制。
1.5HTTP状态码
类别 | 原因短语 | |
---|---|---|
1xx | Informational(信息性状态码) | 接收的请求正在处理 |
2xx | Success(成功状态码) | 接求正常处理完毕 |
3xx | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4xx | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5xx | Server Error(服务器错误状态码) | 服务器处理请求出错 |
状态码实际上对我们来说是一个既熟悉又陌生的概念,我这样说这个概念可能大家会懵逼。
当你看到这张图,你就瞬间明白了状态码是什么。
最常见的状态码, 比如200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
在这里我们要重点来讲一讲3开头的重定向。
重定向分为两种:
1.永久重定向301
2.临时重定向302或307
永久重定向:
当一个网站搬迁时,你访问旧网址肯定是访问不上的,那么老用户可能也不知道新网址是什么,永久重定向就是当你访问旧网址的时候帮你直接跳转到新网站。如果旧网址有被你保存到书签里,网站还会自动帮你更新为新网址。
临时重定向:
当你在某个网站需要输入账号密码时,登录完成后会自动跳转回某个页面。
因为重定向是由浏览器给我们提供支持的,所以浏览器必须识别这些状态,并告诉我们应该跳转到哪一个页面去,所以HTTP的报头里还会有一个属性Location,就是用来表示新的网址的。
1.6cookie和session
在平时上网过程中不知道你有没有发现这样一种现象,比如你上b站,当你登录了一次之后,下一次再访问b站实际上就已经是登录状态了,不需要再手动登录,我们知道HTTP协议本身是一种无状态的协议
,所以HTTP不会保存你的登录状态。但HTTP可以提供一些技术支持来保证网站具有"会话保持"
功能,而cookie就是用来做会话保持的。
我们可以自己手动查看cookie的。
cookie
1.cookie其实是一个文件,该文件里面保存的是我们用户的私密信息
2.若网站有cookie,则HTTP在发起任何请求的时候,都会自动在报文中携带该cookie信息。
cookie有分为文件版
和内存版
。文件版即使把页面关了信息也还存在,内存版当把页面关闭就会自动销毁信息。
cookie虽好,但单纯使用cookie是具有一定的安全隐患的。如果别人盗取了cookie文件,那么就可以以我的身份来获取我的资源。所以就有了一个新的技术:session。
session技术的核心手段就是把用户的私密信息保存在服务器端。
HTTP报文属性里保留了session的id,这个session id具有唯一标识session文件的功能,可以通过session id来找到服务器磁盘上对应的session文件。
1.7HTTP报文属性
报文里的属性上面我们已经提到了很多,在这里做一个总结。
Content-Type
: 数据类型(text/html等)。
Content-Length
: 正文的长度。
Host
: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上。
User-Agent
: 声明用户的操作系统和浏览器版本信息。
referer
: 当前页面是从哪个页面跳转过来的。
location
: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问。
Cookie
: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能。
Connection
:链接方式,1.0使用的是短链接(一个请求,一个响应),1.1使用的是长链接(一个链接,一直保持不中断,通过减少频繁建立tcp链接来达到提高效率的目的)。keep-alive表示长链接,close表示短链接。
1.8HTTP简易实现
makefile
Http:Http.cpp
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f Http
为了方便后续使用,我们将之前的各种接口封装成一个类。
sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock, uint16_t port)
{
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(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error!" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if (listen(sock, 5) < 0)
{
cerr << "listen error !" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr *)&peer, &len);
if(fd >= 0)
{
return fd;
}
return -1;
}
static void Connect(int sock, std::string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
Http.cpp
#include "Sock.hpp"
#include <pthread.h>
#define SIZE 1024*10
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void *HandlerHttpRequest(void *args)
{
//Http协议,如果自己写的话,本质是,我们要根据协议内容,来进行文本分析!
int sock = *(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
char buffer[SIZE];
memset(buffer, 0 , sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer; //查看http的请求格式!
std::string http_response = "http/1.0 200 OK\n";
http_response += "Content-Type: text/plain\n"; //text/plain,正文是普通的文本
http_response += "\n"; //空行
http_response += "This is a test";
send(sock, http_response.c_str(), http_response.size(), 0);
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if( argc != 2 )
{
Usage(argv[0]);
exit(1);
}
//短链接版本(即一次请求,一次响应,每次都关闭套接字)
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for( ; ; )
{
int sock = Sock::Accept(listen_sock);
if(sock > 0)
{
pthread_t tid;
int *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
}
当你运行时,格式为./Http 端口号
。那怎样能请求呢?
打开浏览器,复制上你的公网IP,然后:端口号
,例如xx.xx.xx.xx:8888,此时你的云服务器就能获取到请求。
2.传输层
2.1再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序。
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看)。
2.2端口号范围划分
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。
- ssh服务器, 使用22端口
- ftp服务器, 使用21端口
- telnet服务器, 使用23端口
- http服务器, 使用80端口
- https服务器, 使用443端口
cat /etc/services //可以查看知名端口号
1024 - 65535: 操作系统动态分配的端口号。客户端程序的端口号, 就是由操作系统从这个范围分配的。
两个经典问题:
- 一个进程是否可以bind多个端口号?
可以。 - 一个端口号是否可以被多个进程bind?
不可以。端口号就是用来唯一标识主机内的一个进程。
2.3常见查看网络状态命令
netstat
语法:netstat [选项]
功能:查看网络状态
常用选项:
- n 拒绝显示别名,能显示数字的全部转化成数字。
- l 仅列出有在 Listen (监听) 的服务状态。
- p 显示建立相关链接的程序名。
- t (tcp)仅显示tcp相关选项。
- u (udp)仅显示udp相关选项。
- a (all)显示所有选项,默认不显示LISTEN相关。
pidof
在查看服务器的进程id时非常方便。
语法:pidof [进程名]
功能:通过进程名, 查看进程id
2.4UDP协议
2.4.1UDP协议段格式
16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度。
如果校验和出错, 就会直接丢弃。(这里说明了UDP是不可靠的)
2.4.2UDP的特点
无连接
: 知道对端的IP和端口号就直接进行传输, 不需要建立连接。不可靠
: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息。面向数据报
: 不能够灵活的控制读写数据的次数和数量。(应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并。
)
2.4.3UDP的缓冲区
UDP
没有真正意义上的发送缓冲区
。调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作。
UDP具有接收缓冲区
。 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致。如果缓冲区满了, 再到达的UDP数据就会被丢弃。
UDP的socket既能读, 也能写, 这个概念叫做全双工
。
2.4.4基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
自定义应用层协议:我们自己定义的协议。
2.5TCP协议
2.5.1TCP协议段格式
4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节)。也就是以4个字节为单位的,那我们算一算TCP头部能达到的最大长度是多少,4个bit位最大是1111也就是15,所以TCP头部最大长度为4 × 15 = 60。
而一个TCP(选项以上的部分是一个标准TCP)的标准长度是20字节,也就是说选项最多40字节,还能知道首部的这四位一般是表示20 / 4 = 5,也就是一般这四位是0101
。
16位窗口大小:
这个大小实际上是用来控制客户端给我传输数据的速度的,设想一种情况,要是客户端一直给我发送数据,服务端一直接收直到服务端的缓冲区满了,此时客户端还在发送数据,这一份数据没办法处理,只能丢弃。而16位窗口大小表明了接收缓冲区中剩余空间的大小,这可以告诉客户端服务端还有多大的接收能力,让客户端控制发送数据的速度。换句话说,16位窗口大小的本质就是流量控制
。
6个标志位:
ACK:
ACK机制,ACK全称acknowledge,也就是应答。我们前面说过TCP是一个可靠的传输协议,可靠就体现在这。
我们看到TCP协议段里有一个序号和一个确认序号,这两个是用来干嘛的?
在通信过程中,客户端向服务端发起请求,服务端向客户端响应,但这一过程并不能保证是可靠的,因为客户端发起响应,服务端并不能保证每一次都会应答,有可能因为网络等各种原因没有应答,那这样就没办法保证TCP的可靠性。TCP将每个字节的数据都进行了编号,即为32位序号,而32位确认序号的意思则是在这个确认序号之前的数据都已经收到了
,什么意思呢?
举个例子,例如服务端给客户端的确认序号是1001,那么意思就是1-1000序号的数据全部接收完毕。ACK机制不仅保证了数据能够被准确接收,而且还能保证发送的数据能够按发送的顺序被接收方所接收。
SYN:
这个标志是用来表明请求建立连接的。
这里就涉及到我们前面所讲的三次握手。
第一次握手:
客户端发送SYN请求和服务端建立连接。
第二次握手:
服务端发送ACK确认应答以及SYN请求和客户端建立连接。
第三次握手:
客户端发送ACK确认应答。
RST:
可能很多人会陷入一个误区,以为我们讲三次握手,那么三次握手一定能成功,这是不一定的。三次握手中最有可能出现问题的是第三次,因为第三次握手当客户端发送ACK后,此时不管服务端有没有接收到,客户端都认为连接已经建立完成,而服务端则认为还没有建立完成,这种情况就会导致建立失败,而为了解决这种情况,就有了标志位RST来重置连接,重置异常连接是连接异常的一种情况,只要是双方连接出现异常都可以发送RST来进行连接重置。
PSH:
告知对方,尽快将接收缓冲区中的数据进行向上交付。
URG:
如果想让一个数据尽快的被上传读到,就可以设置URG,表明该报文中携带了紧急数据,需要被优先处理,16位的紧急指针就是用来处理紧急数据的。
FIN:
最后这个标志位涉及到了四次挥手。
第一次挥手:
当客户端不想向服务端请求服务时,想要和服务端断开连接,此时客户端向服务端发送FIN。
第二次挥手:
服务端接收到客户端的FIN,会给客户端一个应答,发送ACK。
第三次挥手:
服务端也向客户端发送一个FIN请求断开连接。
第四次挥手:
客户端收到服务端的FIN,也给服务端一个应答,发送ACK。
2.5.2TCP缓冲区
TCP和UDP类似,也是有自己的缓冲区,不同的是,TCP既有接收缓冲区也有发送缓冲区。
在应用层进行sendto时不是直接把数据发到网络上,而是把数据拷贝到TCP的发送缓冲区,等到服务端调用recv再把数据拷贝到服务端的接收缓冲区。
为什么TCP/UDP需要有缓冲区?
- 提高应用层效率
- 只有TCP协议可以知道对方的网络状态,所以也只有TCP协议能知道如何发,什么时候发,发多少,出错了怎么处理,因为缓冲区的存在,所以可以做到应用层和TCP进行解耦。
2.5.3超时重传机制
听名字很好理解,就是超过某个时间还没送达数据就会重新传送。数据没送达对应两种情况
- 客户端给服务端发送数据时,可能因为网络拥堵等原因,数据无法到达服务端。如果客户端在一个特定时间间隔内没有收到服务端发来的确认应答,就会进行重发。
- 当我们发送对应的报文,发送方没有收到ACK,就一定是对方没有收到对应的报文数据吗?
也有可能是客户端没有接收到ACK,客户端就会认为数据丢了,这样就会造成服务端会收到很多重复数据。针对这种情况我们有必要去区分数据包是真丢了还是只是ACK没有接收到吗?
完全没必要,这种情况我们可以把它当成第一种情况一样处理,我们就认为数据包丢了,对于重复的数据包,TCP能够识别出来并根据序列号进行去重。
那么,超时的时间如何确定?
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间。
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。
- 如果仍然得不到应答, 等待 4*500ms 进行重传。依次类推, 以指数形式递增。
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
2.5.4连接管理机制
现在来看这张图你会发现这里的过程大部分前面都已经提过了。
对于三次握手,不知你是否思考过为什么是三次握手?
1.因为TCP是全双工的,只有经过第三次握手,才能确保双向都可以接收到对方发送的数据。
2.防止重复连接。在网络状况比较差的情况下,发送方可能会连续发送多次建立连接的请求。如果 TCP 握手的次数只有两次,那么接收方只能选择接受或者拒绝请求,但它并不清楚这次的请求是否是正常的请求。如果 TCP 是三次握手的话,那么客户端在接收到服务器端seq+1的消息之后,就可以判断当前的连接是否为历史连接,如果判断为历史连接的话就会发送RST给服务器端终止连接,如果判断当前连接不是历史连接的话就会发送SYN来建立连接。
类似地,为什么是四次挥手?
三次握手概括来说就是SYN SYN+ACK ACK
,四次握手则是FIN ACK FIN ACK
,你可能会疑惑,为什么四次挥手不能是FIN FIN + ACK ACK
。因为当客户端发送FIN给服务端时,仅仅代表客户端不会再发送数据报文了,但仍可以接收数据报文。此时服务端可能还有相应的数据报文需要继续发送(还没发送FIN),因此需要先发送ACK报文,告诉对方已经收到FIN,避免客户端过长时间未收到确认应答导致超时重传。等到服务端数据发送完之后,,服务端才会发送断开连接请求。
TIME_WAIT状态
当服务端发送FIN给客户端,客户端会进入一个TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。
为什么需要一个TIME_WAIT状态?
1.为了尽量保证历史发送的网络数据在网络中被清除。
2.可靠的终止TCP连接。假如ACK丢失,那么服务器将会重发FIN,客户端需要停留在TIME_WAIT状态以处理重复收到的FIN。
针对于第一点,举个例子,我们在绑定端口号时经常会遇到这种情况,当你绑定一个端口号你结束服务后,想马上再次绑定相同的端口号,此时会报错bind error
,就是因为虽然你解绑了端口号,但那份资源还没有被释放,所以没办法再次绑定。
为什么是TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的),同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN。这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK)。