讲述具体的各层协议,自顶向下讲述。
文章目录
应用层
我们程序员写的满足日常需求的网络程序,都是在应用层。
再谈协议
协议是一种"约定",socketapi的接口,在读写数据时,都是按比特位的方式来发送接收的。如果我们要传输一些"结构化的数据"怎么办呢?
结构化数据直接发不好发,结构化数据本质是多条数据,我们可以将多条数据转成一条完整数据统一发出去。对端接收之后再把一条消息转成多条消息。
#include<iostream>
struct stu{
char name[100];
char sex;
int age;
int high;
int weight;
};
网络版计算器
概念
例如,我们需要实现一个服务器版的加法器,我们需要客户端把要计算的两个加数发过去。然后由服务器进行计算,最后再把结果返回给客户端。
约定方案一: 客户端发送一个形如"1+1"的字符串; 这个字符串中有两个操作数,都是整形;两个数字之间会有一个字符是运算符, 运算符只能是+;数字和运算符之间没有空格; …
约定方案二: 定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做"序列化"和"反序列化”。
注意:方案二也存在问题,因为不同环境的结构体内存对齐是不同的。现在demo是演示为主。并且进行基于短连接(服务周期完直接断开,服务端先断开)完成对应的计算。
代码实现
制定协议
:
基于应用层的自定义协议代码
#ifndef __PROTOCOL_HPP__
#define __PROTOCOL_HPP__
#include<iostream>
typedef struct request{
int x;//left
int y;//right;
char op//+-*/%
}request_t;
typedef struct response{
int code; //0,1,2,3
int result;
}response_t;
#endif
server.hpp
#ifndef __SERVER_HPP
#define __SERVER_HPP
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string>
#include<map>
#include<stdlib.h>
#include<functional>
#include<unistd.h>
#include"Protocol.hpp"
#include<sys/wait.h>
#define NUM 5
#endif
class Server{
private:
int _port;
int _lsock;
std::map<char,std::function< void(struct request&,struct response&)>> opMap=
{
{'+',[&](struct request& requ,struct response& resp ){ resp.result = requ.x+requ.y; }},
{'-',[&](struct request& requ,struct response& resp ){ resp.result = requ.x-requ.y; }},
{'*',[&](struct request& requ,struct response& resp ){ resp.result = requ.x*requ.y; }},
{'/',[&](struct request& requ,struct response& resp ){
if(requ.y==0) resp.code = 1;
else resp.result = requ.x/requ.y;
}
}
};//声明时给缺省
public:
Server(int port)
:_port(port),
_lsock(-1)
{
}
void ServerInit()
{
_lsock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if( bind(_lsock ,(struct sockaddr*)&local,sizeof(local) ) < 0 )
{
std::cerr << "bind error "<< std::endl;
exit(1);
}
if( listen(_lsock,NUM) < 0 )
{
std::cerr << "listen error "<<std::endl;
exit(2);
}
}
void service(int sock)
{
/*短连接*/
struct request requ;
recv( sock , &requ, sizeof(requ), 0 );
struct response resp;
resp.code = 0; //默认为0
std::cout<<"get a task:"<<std::endl;
std::cout<<requ.x << " "<<requ.op << " "<<requ.y <<std::endl;
std::cout<<resp.code<<" "<<resp.result<<std::endl;
if(!opMap.count((requ.op))) resp.code = 3;//输入的符号不存在
else{
auto res = opMap[requ.op];
res(requ,resp);
}
//auto func= [&](struct request& requ,struct response& resp ){ resp.result = requ.x+requ.y; };
// func(requ,resp);
// std::cout<<resp.code<<" "<<resp.result<<std::endl;
send( sock, &resp , sizeof(resp), 0 );
close(sock);
}
void start()
{
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
while(true)
{
int sock = accept( _lsock, (struct sockaddr*)&end_point,&len );
if( sock < 0 )
{
std::cerr <<" accept false " <<std::endl;
continue;
}
std::cout <<" get a link ..."<<std::endl;
if( fork() == 0 )
{
if( fork() > 0 )
{
exit(0);
}
close(_lsock);
//孙子进程处理逻辑
service(sock);
exit(0);
}
close(sock);
waitpid( -1 ,nullptr,0);
}
}
~Server()
{
close(_lsock);
}
};
Client.hpp
#ifndef __CLIENT_HPP
#define __CLIENT_HPP
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string>
#include<stdlib.h>
#include<functional>
#include<unistd.h>
#include"Protocol.hpp"
#include<sys/wait.h>
#define NUM 5
#endif
class Client{
private:
std::string _ip;
int _port;
int _sock;
public:
Client(std::string ip,int port)
:_ip(ip),
_port(port),
_sock(-1)
{}
void ClientInit()
{
_sock = socket(AF_INET, SOCK_STREAM,0 );
}
void service(int sock)
{
/*短连接*/
struct request requ;
recv( sock , &requ, sizeof(requ), 0 );
struct response resp;
resp.code = 0; //默认为0
send( sock, &resp , sizeof(resp), 0 );
}
void start()
{
struct sockaddr_in local;
socklen_t len = sizeof(local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
if(connect(_sock,(struct sockaddr*)&local , sizeof(local) ) < 0 )
{
std::cerr<<" connect error "<<std::endl;
exit(4);
}
struct request requ;
std::cout <<"x:";fflush(stdout);
std::cin>>requ.x;
std::cout<<"op:";fflush(stdout);
std::cin>>requ.op;
std::cout<<"y:";fflush(stdout);
std::cin>>requ.y;
send(_sock , &requ, sizeof(requ),0);
std::cout<<"#############"<<std::endl;
struct response resp;
recv(_sock , &resp, sizeof(resp),0);
if(resp.code != 0 )
{
std::cout<<"exit code is "<<resp.code<<std::endl;
}
else std::cout<<"result :"<<resp.result<<std::endl;
}
~Client()
{
close(_sock);
}
};
小结
我们有没有定协议
我们写的代码本质是基于应用层的一个自定义协议的代码。
如何体现序列化与反序列化的?
看到代码并没有做序列化但是对端直接就通过结构体拿出x和y了。原因是我们send过去的是一个结构体request,通过二进制流发送。当client收到request的时候可以看成二进制流,只不过这个二进制流的大小和空间布局和request结构体的结构化数据刚好吻合,碰巧可以直接使用。
因此没有严格意义上手动做序列化和反序列化。
但是这种写法是严重不推荐的。比如qq服务器在linux64位编写,而我们的客户端如果是win32下编译生成的。如果结构体中包含数组,指针等就会导致对齐方式,结构体大小不同,直接反序列就会出问题。
实际中我们一般使用json或者xml进行序列化,或者自定义协议比如说接下来的http。
传输层抓包——tcpdump
tcpdump
:传输层的基本都可以抓
参数:
-i
:只抓某个端口的数据
any
:发送到当前主机的都抓
-n
:将主机名等显示成数字
-nn
:将更多的信息显示成数字
port
:指定端口
tcp/ucp/icmp
:指定抓包的协议
sudo tcpdump -i any tcp port 8080
sudo tcpdump -i any -nn tcp port 8080
这里可以演示出三次握手和四次挥手,以及传输的过程。
就是上一份笔记中的tcp传输协议图。
三次握手
:
四次挥手
:
HTTP协议
HTTP是应用层协议,由我们自己定。
虽然在之前的小结部分讲到应用层协议是我们程序员自己定的,但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用。HTTP(超文本传输协议)就是其中之一。
认识URL
平时我们俗称的"网址"其实就是说的URL。
https://sports.qq.com/a/20220207/007059.htm
https
为协议名:http
默认绑定端口为80;https
默认绑定端口为443。相比http多了加密机制。
blog.csdn.net
为域名:对应IP地址。
/a/20220207/007059.htm
:第一个
/
/
/基本不是根目录,是web根目录,可以是linux服务器上的任何一个路径,web服务部署在哪个路径下一般该
/
/
/就是web根目录。直接请求
/
/
/其他什么都没有就是获取web根目录的index.html首页。
htm和html
是网页:网页是文件。
上述URL本质是把远端服务器上的文件数据拿到本地浏览器,让浏览器解释。我们之前的实现将数据从服务器拿过。文件传输过程就是客户端传入路径,服务器打开对应文件,读取文件,将文件内容写到套接字里,send给客户端
http://user:pass@www.example.jp:80/dir/index.htm?uid=1#ch1
一般情况下请求服务器端口号绝对不能省略,我们平时用的省略了是因为浏览器知道我们使用的协议,而知名协议比如http和https是强绑定的,文字和数字之间已经形成等价关系了。
互联网行为:
- 把服务器的数据拿下来。
- 把自己的数据上传到服务器。
?
后跟的是这次URL请求的参数。用于客户端数据提交使用。在搜索引擎中搜索的关键词为wd
,即参数。采用key=value
形式传参。
urlencode和urldecode
像/ ? :
等这样的字符,已经被url当做特殊意义理解了。因此这些字符不能随意出现。比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
转义的规则如下:将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
客户端发送的时候将对普通符号直接编码,特殊符号转义编码,这个动作叫做urlencode
。
服务器端对接收的符号解释回来,如果是特殊符号则转换回来,这个动作叫做urldecode
。
例如:"+“被转义成了”%2B"。
要理解的是,如果我们向服务器提交某些关键字时,倘若该关键字内部包含了某些特殊符号,我们是需要对该特殊符号进行转化的,这个转化叫urlencode
,到了服务器端再转回来叫urldecode
。
HTTP协议格式
HTTP协议叫做超文本传输协议。
基本特征
总的基本状态:
- 无连接
- 无状态
- 简单快速
无连接
HTTP底层是基于TCP,TCP本身是面向连接的。HTTP是建立在TCP之上的,HTTP本身并不关心TCP通信相关的所有细节,TCP面向连接和HTTP无关,HTTP只关心数据能可靠地发送过去。
- HTTP本身的无连接性和TCP的连接性两者没关系。
- HTTP的无连接性重点体现在一旦TCP连接建立,不需要HTTP在应用层再互相建立连接。TCP连接建立好了,HTTP的请求直接发。因此不用协商HTTP通信相关细节。
**TCP建立连接和http无关,http直接向对象发送http request即可。**之前的网络计算器代码也是这样体现的,上层只有request和response的交互。
具体细节会问,在抓包工具中的抓取https协议中提及。TCP本身的打开和关闭由我们手动close控制。http无连接重点体现在客户端向服务器发requset,服务器回response,但是底层的tcp连接关闭与否取决于http协议。http协议为了更好地复用连接,可以选择短链接或者长连接。keep-alive(长连接),close(短连接)。
无状态
TCP协议有状态,比如发送SYN,同步发送,发送ACK等状态。(在TCP协议图上有)
应用层的HTTP使用的时候并不关心TCP的状态,只关心TCP能不能用。
HTTP无状态最典型的特征是HTTP是给用户用的,服务器和客户端双方在用户实际访问网站时并不记得用户曾经来过,并不关心用户有哪些数据上传,当前是什么状态。HTTP协议只关心用户发送了request,服务器就应该构建response,解释出来就可以。
而实际生活中注册用户,登录后在网站中随意跳转,网站总是认识我的。体现在如果是一个非注册用户打开知乎网页是打不开的,而注册用户可以打开。并且点开浏览痕迹等发现网站是记住我的。这个功能并不是HTTP做的。
HTTP本身是无状态的,并不会记录任何用户信息,关心request<->response,记录用户的基本信息的技术是cookie+session
简单快速
简单的体现:
HTTP超文本传输协议是基于短链接进行文本(html,img,css,js…)传输,所谓短链接就是客户端一请求服务器响应请求处理请求,服务器响应完response之后服务器把连接关掉,一来一回就结束了。[http/1.0:短连接]
[http/1.1:长连接]
快速的体现:
底层用的tcp协议来进行数据本身的发送,本身传送的内容相当一部分可以包含文本资源,音频资源等。
特征小结
cookie部分在最后对应章节。
HTTP构成
request和response是数据,这两份数据底层交互采用的是tcp。服务器拿到request要进行解析,所有的协议解析本质是进行数据分析,而数据分析的前提是数据可靠完整地发送。而tcp本身是面向字节流的,意味着上层从tcp的缓冲区时读取数据时怎么知道该http请求已经发送完了,response也是如此。
互联网行为:
- 把服务器的数据拿下来。
- 把自己的数据上传到服务器。
两者行为对应HTTP请求方法的GET和POST。
所有的协议都要解决报头和有效载荷分离的问题,以及向上交付的问题。而HTTP属于应用层不必再交付。其解决报头和有效载荷分离的方法如下:
空行为HTTP协议报头的分离,Content-Length为请求正文的分离。
按行读取,一直读读到空行说明HTTP请求协议报头读完了,空行之后为请求正文,请求报头中的Content-Length指明了请求正文的长度。因此空行之后为请求正文起点,正文长度也知道因此可以读取整个HTTP报文。
Server解析Client的Request报头如此解析,Client解析Server的Response也是如此解析。
HTTP抓包工具——Fiddler
简单原理
抓取https协议
注意,默认下载的是只能抓取http协议的,解决方案
GET方法
请求
GET 请求方法
https://www.baidu.com/:域名+默认端口号,直接请求 / / /其他什么都没有就是获取web根目录的index.html首页。
HTTP/1.1:HTTP版本号
host——请求的主机
Connection:这次https采取的连接规则,长连接。TCP本身的打开和关闭由我们手动close控制。http无连接重点体现在客户端向服务器发requset,服务器回response,但是底层的tcp连接关闭与否取决于http协议。http协议为了更好地复用连接,可以选择短链接或者长连接。keep-alive(长连接),close(短连接)。
User-Agent:浏览器本身的版本,用户计算机的平台相关信息等。
Accept-Encoding:接收的编码方案。
Accpet-Language:接收的语言
Cookie:浏览器默认在发起请求时自动会提交在本地的一些cookie信息。客户端给服务器。
响应
version:HTTP/1.1
状态码:200
状态码描述:ok
Connection:keep-alive。浏览器发出的Conection是告诉服务器自身支持长连接,如果服务器本身响应的也是keep-alive,为服务器告诉浏览器自身也支持,双方之后通信用长连接。
Content-Encoding:gzip,内容编码压缩方案
Content-Type:text/html;charset=uft-8。给予的资源类型,使用的编码。
Server:BWS/1.1 使用的特定web服务器。
Set-Cookie:服务器向客户端写入cookie信息。
Transfer-Encoding:chunked,服务器本身支持的传输数据的方案,按块传输。
底下的正文由于HTTPS采用了加密所以看不懂。
PUT方法
请求
其实可以发现很多网站的账号和密码是在请求正文中是裸露的。
因此在同一个局域网让别人登录一些网站是可以用Fiddler直接抓包拿到用户和密码的。
响应
这里的返回码状态描述有些网站其实是没有带的。越靠近HTTP端的协议约束力越弱,
总结
GET方法通过URL传参,POST方法通过正文传参。GET不安全,POST方法也不安全。只能说POST比GET更私密。因为POST方法传参不会把数据回显到URL中,直接把数据暴露出来看,但是也要以正文方式传参,除非对数据进行加密。
URL统一资源定位符的长度是有上限的。正文部分理论上长度是没有限制的。
- GET方法不安全,POST方法比GET方法更私密
- GET方法传参,参数长度受限;POST方法传参,理论上可以传无限的数据。
telnet直接请求HTTP
一张网页背后就是左下方的代码。html,css,js本质上是被浏览器解释过才形成我们平时看到的网页。而且我们获得网页信息比浏览器获得的乱得多,因为线上的高频网页会被压缩处理。
可以看到Content-Length此时是9508字符,如果要求排版,带更多的回车和空格,那么Content-Length更长传输效率就会更低,所以一般来说高频使用的网页都会这么压缩处理。
爬虫的简单原理
套接字+connect连接对应服务器,构建request,发送给服务器,服务器自动会把response响应回来。这个动作就叫爬虫。用浏览器获取叫网页请求。用自己写的程序客户端请求就叫爬虫。c++要写一定代码,而py提供了很多线程的http库帮助进行向服务器发送request。
linux下也有现成的工具,比如wget
。底层用的就是创建套接字,connect服务器,构建requset,send出去,再拿resv把response读取过来写到文件里。
而现在很多网站为了避免频繁被爬是通过HTTP常见header中的User-Agent
(其中包含了浏览器版本信息),如果是一个用户通过浏览器访问就携带User-Agent
服务器就知道这个是用户,如果是直接用爬虫就不会包含User-Agent
就可能被服务器拒绝。但是这种操作可以直接加上User-Agent
骗过反爬。但是机器爬的一定会有痕迹,比如有规律性,周期性,ip地址访问次数。
HTTP的方法
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源。资源:文本(html/css/js),图片,音频,视频…。通过HTTP获得资源具体获得什么资源和网站提供的服务有关。 | 1.0,1.1 |
POST | 传输实体主体 | 1.0,1.1 |
PUT | 传输文件(用户向服务器中发送资源)。大部分服务器把PUT禁掉,不允许随意往服务器放资源,而百度云盘对应的就是PUT方法。 | 1.0,1.1 |
HEAD | 获得报文首部,不要正文 | 1.0,1.1 |
DELETE | 删除文件。一般也被禁止。 | 1.0,1.1 |
OPTIONS | 询问支持的方法。 | 1.1 |
TRACE | 追踪路径。 | 1.1 |
CONNECT | 要求用隧道协议连接代理。 | 1.1 |
LINK | 建立和资源之间的联系。 | 1.0 |
UNLINE | 断开连接关系。 | 1.0 |
HTTP的状态码
类别 | 原因短语 | |
---|---|---|
1XX | Information(信息型状态码) | 接受的请求正在处理 |
2XX | Success(成功状态码)。200——ok。 | 请求正常处理完毕 |
3XX | Redirection(重定向状态码)。比如刚登上A网站就被跳转至B网站。 | 需要进行附加操作以完成请求。 |
4XX | Client Error(客户端错误状态码)。404——客户端请求的资源不在服务器上,保证客户端请求的资源为合法资源。403——权限相关码。 | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码)。500——服务器错误,创建线程错误,没有资源响应。 | 服务器处理请求出错 |
最常见的状态码, 比如200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)。
HTTP常见Header
常见Header
这部分内容出现在请求报头和响应报头。
key | 含义 |
---|---|
Content-Type | 数据类型(text/html) |
Content-Length | Body的长度 |
Host | 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上 |
User-Agent | 声明用户的操作系统和浏览器版本信息。能提供判定用户身份的相关信息。 |
referer | 当前页面是从哪个页面跳转过来的 |
location | 搭配3xx状态码使用,告诉客户端接下来要去哪里访问。通常会被写入response告诉客户端下次请求别访问自己去访问其他网站。 |
Cookie | 用于在客户端存储少量信息. 通常用于实现会话(session)的功能 |
Cookie
但是将用户名和密码保存到本地浏览器,万一中毒了浏览器的浏览痕迹和cookie被窃取,账号密码和信息就被窃取了,对方访问同样的资源就不用登录了。
光光一个Cookie是不够安全的,补充的办法就是Session。
登录完成:
此时点击相关资源都是直接访问。清除所有cookie。
此时刷新发现用户已经退出。要重新访问。
Session
单单使用cookie,敏感信息保存在本地;session的敏感信息存在server。
单单使用cookie,存储的是个人账号密码;使用session时cookie存储是sid。
- 不一定知道sid是哪个网站的,若知道是哪个网页那也最多只能访问一样的网页,个人信息和账号密码没有泄露。
- 敏感信息在服务器上存放,服务器安全级别更高。
因此使用cookie+session相对更安全。