一、互联网协议
- 互联网协议是指国际互联网通信之间统一的标准,类似utf-8,国际通用语言英语
- 网络就是物理链接介质+互联网协议
- 按照功能不同,人们将互联网协议分为osi七层或tcp/ip五层或tcp/ip四层
这种分层就好比是学习英语的几个阶段,每个阶段应该掌握专门的技能或者说完成特定的任务,比如:1、学音标 2、学单词 3、学语法 4、写作文
二、TCP/IP五层模型
1.物理层
物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
2.数据链路层
数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
数据链路层的功能:定义了电信号的分组方式
以太网协议
早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet
ethernet规定
- 一组电信号构成一个数据包,叫做‘帧’
- 每一数据帧分成:报头head和数据data两部分
head包含:(固定18个字节)
发送者/源地址,6个字节
接收者/目标地址,6个字节 - 数据类型,6个字节
data包含:(最短46字节,最长1500字节) - 数据包的具体内容
head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
mac地址
head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
广播
有了mac地址,同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址)
ethernet采用最原始的方式,广播的方式进行通信,即计算机通信基本靠吼
3.网络层
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址
IP协议
- 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
- 范围0.0.0.0-255.255.255.255
- 一个ip地址通常写成四段十进制数,例:172.16.10.1
- 回环地址(127.0.0.1) 又称为本机地址,平时我们用127.0.0.1来尝试自己的机器服务器好使不好使
4.传输层
传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,迅雷等多个应用程序,
那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。
传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为系统占用端口
传输层有两种协议,TCP和UDP,见下图
TCP协议
可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
- 为什么tcp是可靠的数据传输呢?
最可靠的方式就是只要不得到确认,就重新发送数据报,直到得到对方的确认为止。 - tcp的3次握手和4四挥手:
TCP协议建立C端和S端会通过3次握手建立双方之间的连接
TCP协议断开C端和S端会通过4次回收断开双方之间的连接(因为C端发送断开请求时,S端数据并未确定传输结束,在答复C端FIN=1请求时ASK=1无法同时发出FIN=1请求)
UDP协议
不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
- UDP协议不同于TCP协议,在数据传输之前不需要建立链接,C端只负责将带有IP和端口信息的数据发出,不管对方是否接收到
- 当遇到网络故障等问题时,UDP协议下的数据传输可能没有传到,但它并不会再次发出
对比
TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!
三、SOCKET(套接字)
1.什么是SOCKET
由于TCP协议中的规则相当复杂,所以SOCKET把tcp/ip协议层的各种数据封装、数据发送、接收等通过代码已经封装好了,方便用户调用
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。
SOCKET原理:
套接字家族分为两种:
- AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信 - AF_INET
AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET
2.简单的套接字通信
服务端代码:
import socket
#1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #socket实例化传入2个参数:套接字的家族名称,套接字传输数据方式(TCP协议用的是流水式)
#2、绑定手机卡
phone.bind(('127.0.0.1',8081)) #0-65535:0-1024给操作系统使用,IP和端口要以元组的形式传入
#3、开机
phone.listen(5) #5代表最大连接数
#4、等电话链接
print('starting...')
conn,client_addr=phone.accept() #phone.accept()接受到的数据是元组,conn代表链接(即电话线),client_addr代表客户端的地址
#5、收,发消息
data=conn.recv(1024) #1、单位:bytes 2、1024代表最大接收1024个bytes
print('客户端的数据',data)
conn.send(data.upper())
#6、挂电话
conn.close()
#7、关机
phone.close()
注意:在服务端中产生了2个套接字的对象:phone和conn,phone负责监听,conn负责收发消息
客户端代码:
import socket
#1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#2、拨号
phone.connect(('127.0.0.1',8081)) #对应服务端的accept
#3、发,收消息
phone.send('hello'.encode('utf-8')) #底层物理层是进行二进制传输的,所以必须要发bytes类型
data=phone.recv(1024)
print(data)
#4、关闭
phone.close()
有的时候会遇到如下报错:
原因是因为上一次通信的端口还未回收,可以通过加入一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
3.加上通信循环
服务端代码:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #生成一个电话对象
phone.bind(("127.0.0.1",1)) #绑定IP和端口
phone.listen(5) #开始监听
conn,client_addr = phone.accept() #收到请求,生成conn的收发对象
while True:
data = conn.recv(1024) #收到数据
print(data)
conn.send(data.upper()) #发出数据
conn.close()
phone.close()
客户端代码:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",1))
while True:
msg = input(">>>").strip()
phone.send(msg.encode("utf-8"))
data = phone.recv(1024)
print(data)
phone.close()
BUG解析:
服务端中conn是一个S端与C端双向链接的通道,如果中途C端终止会出现S端陷入死循环,Linux系统会不断地收到空数据,Window系统会直接报错ConnectionResetError,处理方案如下:
while True: #通信循环
try:
data=conn.recv(1024)
if not data:break #适用于linux操作系统
print('客户端的数据',data)
conn.send(data.upper())
except ConnectionResetError: #适用于windows操作系统
break
4.实现服务端可以对多个客户端提供服务
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8083)) #0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True: # 链接循环
conn,client_addr=phone.accept()
print(client_addr)
while True: #通信循环
try:
data=conn.recv(1024)
if not data:break #适用于linux操作系统
print('客户端的数据',data)
conn.send(data.upper())
except ConnectionResetError: #适用于windows操作系统
break
conn.close()
phone.close()
链接循环——在创建链接conn前加上循环
这样做实现了服务端一个个的与客户端通信的功能,但是不能并发进行
4.模拟SSH远程执行命令
服务端代码:
import socket
import subprocess
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #生成一个电话对象
phone.bind(("127.0.0.1",1)) #绑定IP和端口
phone.listen(3) #开始监听
while True:
print("starting...")
conn,client_addr = phone.accept() #收到请求,生成conn的收发对象
while True:
cmd = conn.recv(1024) #收到数据
print(cmd)
if not cmd:break
obj = subprocess.Popen(cmd.decode("gbk"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
# subhprocess.Popen生成了一个对象,shell=True代表打开一个命令解释器(cmd)
# stdout代表命令执行成功将数据存入subprocess.PIPE里
# stderr代表命令执行失败将数据存入subprocess.PIPE里
stdout = obj.stdout.read() #将数据读出来
stderr = obj.stderr.read()
conn.send(stdout+stderr) #发出的数据为bytes格式
conn.close()
phone.close()
客户端代码:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",1))
while True:
cmd = input(">>>").strip()
if not cmd:break
phone.send(cmd.encode("utf-8"))
data = phone.recv(1024)
print(data.decode("gbk"))
phone.close()
四、粘包现象
1.什么是粘包?
粘包现象——在套接字中resv(1024)中1024代表一次数据接收1024个bytes的长度,那么当send数据超过1024时,例如1025字节数据中的最后一个数据会暂存在操作系统的内存中,待下一次resv数据一并传入
从五层协议看,应用程序只负责应用层,数据交给操作系统后由操作系统代为执行后续几层功能:
- 不管是recv还是send都不是直接接收对方的数据,而是操作自己的操作系统内存
所以,并不是一次send就对应一次recv,可以send多次数据,只用一次resv接收 - resv接收数据分为两步:
wait data等待接收数据,耗时较长
copy data接收到数据后将数据从系统内存copy到应用程序内存中,速度较快 - send发送数据只有一步:
copy data将数据复制给操作系统内存,等候操作系统将数据按照TCP协议发出即可
2.粘包的底层原理
Nagle算法
按照TCP协议,如果send数据内容较小,时间间隔较短,每次传输会发生网络I/O延迟(input/output),这样是非常不高效的
Nagle算法是将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包传输,那么到了接收端就会发生两次传输的数据包接连起来的情况
示例代码:
服务端代码:
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()
res = conn.recv(1024)
print(res.decode("utf-8"))
客户端代码:
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))
client.send("hello".encode("utf-8"))
client.send("world".encode("utf-8"))
服务端输出的结果是:helloworld,会将客户端两次发送的数据"hello"和"world"粘合起来
但是当给客户端两次发送之间加入time.sleep(1)延迟一秒的时候,两次数据就不会粘合,因为1秒对于I/O延迟来说算很长了
3.Struct模块介绍
struct 模块是一个可以将任意大小的数字转换成一个固定长度编码的模块
struct.pack(“i”,data)
import struct
res = struct.pack("i",123)
print(res,type(res),len(res))
输出结果:
b'x\x00\x00\x00' <class 'bytes'> 4
可见struct.pack(“i”,123)得到的结果是一个bytes类型,并且长度固定为4个字节
但是这个转化对数字的大小范围有一定的要求
i 模式转换的数字较小,转化之后的结果只有4个字节
q 模式转换的数字范围较大,转换之后的结果有8个字节
struct.unpack(“i”,data)
import struct
res = struct.pack("i",120)
obj = struct.unpack("i",res)
print(obj)
输出结果:
(120,)
通过struct.unpack(“i”,data)反解可以得到pack打包的数据,它是一个元组形式
4.解决粘包问题(简单版)
思路解析——本质上是在发送数据之前,将数据的长度做一个固定长度的报头发送对方,对方在resv(***)的时候可以推算需要循环几次收完数据
服务端代码:
import socket
import struct
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()
#第一步:先收报头(肯定为4个字节长度)
header = conn.recv(4)
#第二步:从报头中解析出对真实数据的描述信息(数据的长度)
data_len = struct.unpack("i",header)[0] #注意unpack拿到的数据是一个元组形式
#第三部:接收真实数据
resv_len = 0
resv_data = b""
while resv_len < data_len :
res = conn.recv(1024)
resv_len += len(res) #收到数据的长度增加
resv_data += res #收到新的数据与老的数据相连
print(resv_data.decode("utf-8"))
客户端代码:
import socket
import struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))
#第一步:制作固定长度的报头
data = input(">>>").strip().encode("utf-8")
data_len = len(data)
header = struct.pack("i",data_len)
#第二步:把报头发给服务端
client.send(header)
#第三步:发送真实数据
client.send(data)
5.解决粘包问题(终极版)
上面简单版的解决粘包问题存在两个缺陷:
- 报头内容只有数据长度,有时我们还需要文件名,md5密码等信息
- struct模块的i格式对于数字长度有要求,文件长度过长无法转换成4字节的报头
基于以上做以下优化:
思路——利用字典将文件名等信息加入报头,利用json将字典改为字符串格式,再转换bytes类型取得报头的长度,先传输报头,反解后获得字典中文件大小的key,再进行真实文件的传输
服务端代码:
import socket
import struct
import json
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()
#第一步:先收报头的长度(肯定为4个字节长度)
obj = conn.recv(4)
header_len = struct.unpack("i",obj)[0]
#第二步:收报头
header_bytes = conn.recv(header_len)
#第三步:从报头中解析出对真实数据的描述信息(数据的长度)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
data_len = header_dict["data_len"]
#第四部:接收真实数据
resv_len = 0
resv_data = b""
while resv_len < data_len :
res = conn.recv(1024)
resv_len += len(res) #收到数据的长度增加
resv_data += res #收到新的数据与老的数据相连
print(resv_data.decode("utf-8"))
客户端代码:
import socket
import struct
import json
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))
#第一步:制作固定长度的报头
data = input(">>>").strip().encode("utf-8")
data_len = len(data)
header_dict = {"filename":"Kerwin.txt","md5":"xxxxdxxx","data_len":data_len}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8")
header_len = struct.pack("i",len(header_bytes))
#第二步:把报头数据的长度发给服务端
client.send(header_len)
#第三步:把报头发给服务端
client.send(header_bytes)
#第四步:发送真实数据
client.send(data)
五、文件传输
1.基本功能实现
以下代码实现基础是将服务端与客户端创建在两个单独文件夹内,为避免两端同时进行wb和rb模式报错,实际最好加入filename的不同路径(共享文件夹和下载文件夹)
服务端代码:
import socket
import struct
import json
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()
#第一步:先收报头的长度(肯定为4个字节长度)
obj = conn.recv(4)
header_len = struct.unpack("i",obj)[0]
#第二步:收报头
header_bytes = conn.recv(header_len)
#第三步:从报头中解析出对真实数据的描述信息(数据的长度)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
data_len = header_dict["data_len"]
filename = header_dict["filename"]
#第四部:通过wb的模式不断从客户端收到数据写入文件内
with open(filename,"wb") as f:
resv_len = 0
while resv_len < data_len :
line = conn.recv(1024)
f.write(line) #收到新的数据与老的数据相连
resv_len += len(line) #收到数据的长度增加
print("总大小:%s 已下载:%s"%(data_len,resv_len)) #打印数据下载进度条
客户端代码:
import socket
import struct
import json
import os
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))
#第一步:收到命令
cmd = input(">>>").strip() #get 1.mp4
#第二步:解析命令,获取相应命令信息
filename = cmd.split(" ")[1] #文件名:1.mp4
#第三步:制作固定长度的报头
data_len = os.path.getsize(filename) #通过os模块获取文件大小
header_dict = {"filename":filename,"md5":"xxxxdxxx","data_len":data_len}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8")
header_len = struct.pack("i",len(header_bytes))
#第四步:把报头数据的长度发给服务端
client.send(header_len)
#第五步:把报头发给服务端
client.send(header_bytes)
#第六步:通过rb打开文件,每行读取内容发送给服务端
with open(filename,"rb") as f:
for line in f :
client.send(line)
六、基于UDP协议的套接字(数据报)
1.代码示例
客户端代码:
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #SOCK_DGRAM是data gram缩写(数据报),不同于TCP协议的数据流
client.sendto(b"hello",("127.0.0.1",10086))
服务端代码:
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(("127.0.0.1",10086))
while True:
data = server.recvfrom(1024)
print(data)
输出结果:
(b'hello', ('127.0.0.1', 65370))
特点:
- UDP协议套接字不需要conn.listen,conn.accept等监听和接收链接的操作,因为它不存在双向链接
- UDP协议的接收命令是recvfrom,发送命令是sendto
- UDP协议的发送方只负责发送数据,一旦发送就会从内存中删除,当出现网络问题时,服务端没收齐数据时就会发送数据丢失,所以为不可靠的方式
- 当recvfrom的字节量小于sendto的数据量时,windows系统会报错:
2.TCP协议与UDP协议对比:
tcp基于链接通信
- 基于链接,则需要listen(backlog),指定连接池的大小
- 基于链接,必须先运行的服务端,然后客户端发起链接请求
- 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
- 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)
udp无链接
- 无链接,因而无需listen(backlog),更加没有什么连接池之说了
- 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失
- recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
- 只有sendinto发送数据没有recvfrom收数据,数据丢失
总结
TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!
TCP协议常用情景:远程操控、文件传输
UDP协议常用情景:QQ聊天,查询信息
七、目录结构规范
(1)bin,bin下面主要是存放程序的执行入口。一般看见bin,启动程序都会在这个下面
(2)core,core下面主要是存放程序中使用的逻辑,核心代码,可以用包的来构成,这个包下面存放不同的功能模块,比如登陆认证,数据解析等程序的核心代码都是写到这个core的下面
(3)conf,程序的配置文件,程序中使用到的一些固定的变量,譬如listen监听的ip,端口这些都是固定的,就可以直接写到配置文件中,然后在程序中就是使用变量的引入,这样的好处就是以后修改的时候只需要修改配置文件,比如链接数据库的信息,全部都是放到这个文件加下
(4)doc,这个文件夹下存放文件的一些文件信息
(5)log,程序的日志信息
(6)db,db文件专门用来存放程序中使用的一切数据来源
(7)readme,程序的使用介绍,以及程序的一些说明等