一、OSI七层模型
-
应用层:对顶数据的格式;
-
表示层:对因数层数据的编程、压缩(解压)、分块、加密(解密)、等任务;
-
会话层: 负责建立目标、中断连接;
在发送数据之前, 需要先发送"连接"的请求, 与远程建立连接后, 在发送数据, 当然发送完毕之后, 也涉及终端连接的操作。
-
传输层:建立端口到端口的通信,其实就是去欸的那个双方的端口信息
-
网络层: 标记目标IP信息(IP协议)
-
数据链路层:对数据进行分组并设置源和目标的mac地址
-
物理层: 将二进制数据在媒体桑传输。
二、UDP和TCP协议
UDP示例如下:
-
服务端
import socket server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server.bind(('127.0.0.1', 9000)) while True: data,(host, prot) = server.recvfrom(1024) print(data, host, port) server.sendto('好的'.encode('utf-8'), (host, port))
-
客户端
import socket client = socket.socket(socket.AF_INET, socket.SCOK_DGRAM) while True: text = inpurt('请输入要发送的内容') if text.upper() == 'Q': break client.sendto(text.encode('utf-8'), ('127.0.0.1', 9001)) client.close()
TCP协议示例:
-
服务端
import socket sock = socket.socket(socket.AF_INET,SOCK_STRAM) sock.bind(('127.0.0.1', 9000)) sock.listen(5) while True: conn,addr = sock.accept() client_data = conn.recv(1024) print(client_data) conn.sendall(b'hello world') conn.close() sock.close()
-
客服端
import socket client = socket.socket() client.connect(('127.0.0.1', 9000)) client.sendall(b'hello') reply = client.recv(1024) print(reply) client.close()
-
TCP与UDP协议的区别
三、TCP三次握手与四次挥手
-
三次握手
第一次握手: 客户端发送一个TCP的SYN标志位置1的包致命客服端打算连接服务器的端口, 以及初始化序号X, 保存在报头的序号字段里.
第二次握手: 服务器发回确认包(ACK)应答, 即SYN表示为何ACK标志为均为1同时, 将确认序号设置为客户端ISN加1, 即为X+1
第三次握手: 客服端再次发送确认包(ACK)SYN标志位为0, ACK标志位为1, 并且把服务器发来的ACK的序号字段+1, 放在确定字段中发送给对方并且在数据端放些ISN的+1
PS:三次握手是建立一个TCP连接, 需要客户端和服务端总共发送三个包
-
四次挥手
第一次挥手: 客户端发送报文, 报文中指定一个序列表, 此时客户护短处于FIN_WAIT状态
第二次挥手: 服务端收到FIN之后, 会发送ACK报文, 并且客户端的序列表值为+1, 作为ACK报文的序列号至, 表明已经收到客户端报文, 此时服务端处于CLOSE_WAIT状态
第三次挥手: 如果服务端也想端口连接了和客服端的第一次挥手一样, 发给FIN报文, 且指定一个序列号, 次数服务端出去LAST_ACK的状态
第四次挥手: 客户端收到FIN之后, 一样发送一个ACK报文作为应答, 且把服务端的序列号值+1作为自己的ACK 报文的序列号, 此时可断的出去TIME_WAIT状态, 需要一阵子来确保服务端收到到自己的ACK报文之后才会进行CLOSE状态
四、粘包
-
什么是粘包
对于发送者: 执行send/sendall发送消息时, 是将数据线发送到自己的网络的缓冲去, 再由缓冲区将数据发送给对象网卡的读缓冲去
对于接收者: 执行recv接收消息时, 是从自己网卡读缓冲去获取数据
所以, 如果发送者连续快速发送了两条信息, 接收者在读取时会认为是一条信息, 这样两个包就会粘在一起
-
示例:
# 客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 9000)) client.sendall('正在吃'.encode('utf-8')) client.sendall('翔'.encode('utf-8')) client.close() # 服务端 import socket sock = socket.socket(socket.AF_INET, socket_SOCK_STREAM) sock.bind(('127.0.0.1', 9000)) sock.listen(3) conn, addr = sock.accept() client_data = conn.recv(1024) print(client_data.decode('utf-8')) conn.close() sock.close()
-
如何解决粘包的问题
每次发送消息时, 都将消息划分为头部(固定字节长度) 和 数据量部分, 例如: 头部, 用四个四节表示后面长度.
- 发送数据, 先发送数据的长度, 在发送数据(或拼接起来在发送)
- 接收数据, 先读4个字节就可以知道自己这个数据包中的数据长度, 在根据长度读取到数据.
对头部需要一个数据自并固定4个字节, 这个功能可以借助Python的struct包来实现.
import struct # ########### 数值转换为固定4个字节,四个字节的范围 -2147483648 <= number <= 2147483647 ########### v1 = struct.pack('i', 199) print(v1) # b'\xc7\x00\x00\x00' for item in v1: print(item, bin(item)) # ########### 4个字节转换为数字 ########### v2 = struct.unpack('i', v1) # v1= b'\xc7\x00\x00\x00' print(v2) # (199,)
- 解决粘包示例
服务端
import socket
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
conn, addr = sock.accept()
# 固定读取4字节
header1 = conn.recv(4)
data_length1 = struct.unpack('i', header1)[0] # 数据字节长度 21
has_recv_len = 0
data1 = b""
while True:
length = data_length1 - has_recv_len
if length > 1024:
lth = 1024
else:
lth = length
chunk = conn.recv(lth) # 可能一次收不完,自己可以计算长度再次使用recv收取,指导收完为止。 1024*8 = 8196
data1 += chunk
has_recv_len += len(chunk)
if has_recv_len == data_length1:
break
print(data1.decode('utf-8'))
# 固定读取4字节
header2 = conn.recv(4)
data_length2 = struct.unpack('i', header2)[0] # 数据字节长度
data2 = conn.recv(data_length2) # 长度
print(data2.decode('utf-8'))
conn.close()
sock.close()
- 客服端
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8001))
# 第一条数据
data1 = 'alex正在吃'.encode('utf-8')
header1 = struct.pack('i', len(data1))
client.sendall(header1)
client.sendall(data1)
# 第二条数据
data2 = '翔'.encode('utf-8')
header2 = struct.pack('i', len(data2))
client.sendall(header2)
client.sendall(data2)
client.close()
五、阻塞与非阻塞
默认情况下我们编写的网络编程的代码都是阻塞的(等待),阻塞主要体现在:
# ################### socket服务端(接收者)###################
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
# 阻塞
conn, addr = sock.accept()
# 阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))
conn.close()
sock.close()
# ################### socket客户端(发送者) ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
client.sendall('alex正在吃翔'.encode('utf-8'))
client.close()
改成非堵塞
# ################### socket服务端(接收者)###################
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False) # 加上就变为了非阻塞
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
# 非阻塞
conn, addr = sock.accept()
# 非阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))
conn.close()
sock.close()
# ################### socket客户端(发送者) ###################
import socket
client = socket.socket()
client.setblocking(False) # 加上就变为了非阻塞
# 非阻塞
client.connect(('127.0.0.1', 8001))
client.sendall('alex正在吃翔'.encode('utf-8'))
client.close()
如果代码变成了非阻塞,程序运行时一旦遇到 accept
、recv
、connect
就会抛出 BlockingIOError 的异常。
这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。
非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。
六、IO多路复用
I/O多路复用指:通过一种机制,可以监视多个描述符,
一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
# ################### socket服务端 ###################
import select
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False) # 加上就变为了非阻塞
server.bind(('127.0.0.1', 8001))
server.listen(5)
inputs = [server, ] # socket对象列表 -> [server, 第一个客户端连接conn ]
while True:
# 当 参数1 序列中的socket对象发生可读时(accetp和read),则获取发生变化的对象并添加到 r列表中。
# r = []
# r = [server,]
# r = [第一个客户端连接conn,]
# r = [server,]
# r = [第一个客户端连接conn,第二个客户端连接conn]
# r = [第二个客户端连接conn,]
r, w, e = select.select(inputs, [], [], 0.05)
for sock in r:
# server
if sock == server:
conn, addr = sock.accept() # 接收新连接。
print("有新连接")
# conn.sendall()
# conn.recv("xx")
inputs.append(conn)
else:
data = sock.recv(1024)
if data:
print("收到消息:", data)
else:
print("关闭连接")
inputs.remove(sock)
# 干点其他事 20s
"""
优点:
1. 干点那其他的事。
2. 让服务端支持多个客户端同时来连接。
"""
# ################### socket客户端 ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
while True:
content = input(">>>")
if content.upper() == 'Q':
break
client.sendall(content.encode('utf-8'))
client.close()
# ################### socket客户端 ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
while True:
content = input(">>>")
if content.upper() == 'Q':
break
client.sendall(content.encode('utf-8'))
client.close() # 与服务端断开连接(四次挥手),默认会想服务端发送空数据。
基于 IO多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中
- IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。
- 非阻塞,socket的connect、recv过程不再等待。
注意:IO多路复用只能用来监听 IO对象 是否发生变化,常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。