目录
一、网络层面基础概念
1、osi 网络七层模型
应用层(应用层,表示层,会话层) 封装数据 依据不同的协议,封装对应格式的数据消息 HTTP [超文本传输协议] HTTPS[加密传输超文本传输协议] FTP [文件传输协议] SMTP [电子邮件传输的协议] 传输层: 封装端口 指定传输的协议(TCP协议/UDP协议) 网络层: 封装ip 版本ipv4 / ipv6 数据链路层: 封装mac地址 指定链路层的协议(arp协议(ip->mac)/rarp协议(mac->ip)) 物理层: 打成数据包,变成二进制字节流,通过网络进行传输.
2、交换机与路由器
# 交换机:对同一网段的不同机器之间进行数据转发的设备 [每一台机器和交换机相连,形成通信] 交换机从下到上拆2层,拆物理层和数据链路层,可以找到mac # 路由器:对不同网段的不同机器之间进行数据转发的设备 [每一个局域网和路由器相连,形成通信] 交换机从下到上拆3层,拆物理层和数据链路层和网络层,可以找到ip
3、arp协议原理
通过ip -> mac (arp地址解析协议)
1、主机A发送请求时,如果ARP列表没有mac,主机一开始会发送arp请求包(发出找mac的请求) 把mac标记一个全FF-FF-FF-FF-FF-FF的广播地址。 2、交换机接收到arp的广播包,从下到上进行拆包,拆2层,到数据链路层得到mac 发现mac是全F的广播地址,重新打包,交换机开始广播,所有连接在交换的设备都会收到arp广播包。 3、各大主机在接受arp请求包的时候,都会去对照自己本机当中的arp解析表(ip->mac) 如果没有,这个arp得请求包舍弃,如果有,会把自己的ip和mac封装在arp的响应包当中给交换机进行单播。 4、如果有,则首先从数据包中取出源主机的IP和mac地址写入到ARP列表中,如果以存在,则覆盖 然后将自己的ip和mac封装在arp的响应包当中给交换机进行单播。 5、源主机收到ARP响应包后,将目的主机的IP和mac地址写入arp列表,并利用此信息发送数据 如果源主机一直没有收到arp响应数据包,表示arp查询失败。
4、TCP三次握手
SYN 创建连接(0否1是) ACK 确认响应 FIN 断开连接 seq 随机数据包 第一次握手:主机A发送位码为syn=1,随机产生seq number=x的数据包到服务器,主机B由SYN=1知道,A要求建立联机; 第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=y的包; 第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
5、TCP四次挥手
1、客户端进程发出连接释放报文,并且停止发送数据。FIN=1,其序列号为seq=u,此时,客户端进入终止等待状态。
2、服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。
3、客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4、服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5、客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6、服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
常见面试题
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
【问题3】为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
二、利用socket模块实现TCP/UDP通信
1、TCP消息通信
server.py
# ### tcp 服务端
import socket
# 1.创建一个socket对象
sk = socket.socket()
# 让当前端口重复绑定多个程序(仅仅在测试阶段用)
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 2.在网络中注册主机(绑定ip和端口号)
sk.bind( ("127.0.0.1",9000) )
# 3.监听端口
sk.listen()
# 4.三次握手
# conn,addr = sk.accept()
# 5.收发数据
'''
数据类型:二进制的字节流
b修饰的字符串 => 代表的是二进制的字节流
里面的字符必须是ascii编码中的字符.不能是中文,否则报错
'''
"""
conn.send(b"i love you")
"""
while True:
conn,addr = sk.accept()
while True:
res = conn.recv(1024)
print(res.decode())
strvar = input("请输入服务端给客户端发送的消息:")
conn.send(strvar.encode())
# 退出
if strvar == "q":
break
# 6.四次挥手
conn.close()
# 7.退还端口
sk.close()
client.py
# ### tcp 客户端
import socket
# 1.创建一个socket对象
sk = socket.socket()
# 2.连接服务端
sk.connect( ("127.0.0.1",9000) )
# 3.收发数据
"""
res = sk.recv(1024) # 一次接受的最大字节数是1024
print(res)
"""
while True:
strvar = input("请输入您要发送的数据")
sk.send(strvar.encode())
res = sk.recv(1024)
if res == b"q":
break
print(res.decode())
# 4.关闭连接
sk.close()
2、UDP消息通信
udpserver.py
# ### 服务端
import socket
# type=socket.SOCK_DGRAM => 返回udp协议对象
# 1.创建udp对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2.绑定地址端口号
sk.bind( ("127.0.0.1",9000) )
# 3.接受消息(udp作为服务端的时候,第一次一定是接受消息)
while True:
# 接受消息
msg,cli_addr = sk.recvfrom(1024)
print(msg.decode("utf-8"))
print(cli_addr)
# 服务端给客户端发消息
message = input("服务端要发送的消息:")
sk.sendto(message.encode("utf-8"), cli_addr)
# 4.关闭连接
sk.close()
udpclite.py
# ### 客户端
import socket
# type=socket.SOCK_DGRAM => 返回udp协议对象
# 1.创建udp对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2.发送数据
while True:
# 发送数据
message = input("请输入客户端发送的消息")
sk.sendto(message.encode("utf-8") , ("127.0.0.1",9000) )
# 客户端接受服务端发过来的数据
msg, addr = sk.recvfrom(1024)
print(msg.decode("utf-8"))
# 3.关闭连接
sk.close()
三、利用hashlib模块进行加密校验
1、数据加密几种方式
import hashlib
import random
# 基本用法
# (1) 创建一个md5算法的对象
hs = hashlib.md5()
# (2) 把要加密的字符串通过update更新到hs对象中运算
hs.update("123456".encode("utf-8")) # 里面的数据时二进制字节流
# (3) 获取32位16进制的字符串
res = hs.hexdigest()
print(res , len(res))
# 加盐 (加key => Xboy_ 加一个关键字配合原字符串,让密码更加复杂,不容易破解)
hs = hashlib.md5("XBoy_^".encode())
hs.update("123456".encode())
res = hs.hexdigest()
print(res,len(res))
# 动态加盐
res = str(random.randrange(100000,1000000))
print(res)
hs = hashlib.md5(res.encode("utf-8"))
hs.update("123456".encode())
res = hs.hexdigest()
print(res)
# sha算法
"""
sha 算出来的十六进制的串是40位 加密稍慢 , 安全性稍高
md5 算出来的十六进制的串是32位 加密速度快,安全性一般
"""
hs = hashlib.sha1()
hs.update("123456".encode())
res = hs.hexdigest()
print(res, len(res))
hs = hashlib.sha512()
hs.update("123456".encode())
res = hs.hexdigest()
print(res, len(res))
# ### hmac
"""hmac 加密算法更加复杂,不容易破解"""
import hmac
# 必须指定盐
key = b"abc"
# 密码
msg = b"123456"
hm = hmac.new(key,msg)
res = hm.hexdigest()
print(res,len(res))
# 动态加盐
import os
"""
# 基本使用
# urandom 返回随机的二进制字节流, 参数:代表的长度
res = os.urandom(64)
print(res,len(res))
"""
key = os.urandom(32)
msg = b'123456'
hm = hmac.new(key,msg)
res = hm.hexdigest()
print(res,"<1>",len(res))
2、文件校验
# ### 文件校验
import hashlib
hm = hashlib.md5()
hm.update("123".encode())
res = hm.hexdigest()
print(res)
# (1) 针对于小文件进行内容校验
def check_md5(file):
with open(file, mode="rb") as fp:
hs = hashlib.md5()
hs.update(fp.read())
return hs.hexdigest()
res1 = check_md5("ceshi1.txt")
res2 = check_md5("ceshi2.txt")
print(res1,res2)
# (2) 针对于大文件进行内容校验
hs = hashlib.md5()
hs.update("今天是星期一".encode())
res = hs.hexdigest()
print(res) #a33fc073e6be76154e58874c4ac7cee1
hs = hashlib.md5()
hs.update("今天是".encode())
hs.update("星期一".encode())
res = hs.hexdigest()
print(res) # a33fc073e6be76154e58874c4ac7cee1
# 方法一
def check_md5(file):
hs = hashlib.md5()
with open(file,mode="rb") as fp:
while True:
# read(5) 一次最多读取5个字节,
content = fp.read(5)
# 如果有内容就进行计算
if content:
# 分批进行更新计算;
hs.update(content)
else:
break
return hs.hexdigest()
print("<=====>")
print(check_md5("ceshi1.txt"))
print(check_md5("ceshi2.txt"))
# 方法二
import os
def check_md5(file):
hs = hashlib.md5()
# 计算文件大小返回的字节的个数
file_size = os.path.getsize(file) # 计算文件大小
with open(file,mode="rb") as fp:
while file_size:
# 一次最多5个
content = fp.read(5)
hs.update(content)
# 按照实际的读取个数进行相减;
file_size -= len(content)
return hs.hexdigest()
print("<=====>")
print(check_md5("ceshi1.txt"))
print(check_md5("ceshi2.txt"))