文章目录
1.socket简述
1.1 socket模块简述
python内置模块,用于网络编程,可以实现在网络之间进行数据传输。
通常包含客户端和服务端两个部分,通过网络进行数据的传输。
1.2 socket模块主要参数和方法
- socket实例化
- 绑定端口: bind
- 监听:listen
- 阻塞等待连接:accept
- 连接:connect
- 发送数据:send sendall
- 接收数据:recv
- 断开连接 close
1.3 例子
服务端:
import socket
# 1.监听本机的IP和端口
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('123.206.15.88', 8001)) # IP,端口
sock.listen(5) # 支持排队等待5人
while True:
# 2.等待,有人来连接(阻塞)
conn, addr = sock.accept() # 等待客户端来连接(阻塞)
# 3.等待,连接者发送消息(阻塞)
client_data = conn.recv(1024) # 等待接收客户端发来数据
print(client_data.decode('utf-8')) # 字节
# 4.给连接者回复消息
conn.sendall("hello world".encode('utf-8'))
# 5.关闭连接
conn.close()
# 6.停止服务端程序
sock.close()
客户端:
import socket
# 1. 向指定IP发送连接请求
client = socket.socket()
client.connect(('123.206.15.88', 8001)) # 向服务端发起连接(阻塞)10s
# 2. 连接成功之后,发送消息
client.sendall('hello'.encode('utf-8'))
# 3. 等待,消息的回复(阻塞)
reply = client.recv(1024)
print(reply)
# 4. 关闭连接
client.close()
2.OSI 7层模型
- 七层分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层,在发送的过程中会在每一层都加一层封装,在接收到数据后会逆向的在每一层分别去掉这些封装。
2.1 发送数据
假设,你在浏览器上输入了一些关键字,内部通过DNS找到对应的IP后,再发送数据时内部会做如下的事:
-
应用层:规定数据的格式。
"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n"
-
表示层:对应用层数据的编码、压缩(解压缩)、分块、加密(解密)等任务。
"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
-
会话层:负责与目标建立、中断连接。
在发送数据之前,需要会先发送 “连接” 的请求,与远程建立连接后,再发送数据。当然,发送完毕之后,也涉及中断连接的操作。
-
传输层:建立端口到端口的通信,其实就确定双方的端口信息。
数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8') 端口: - 目标:80 - 本地:6784
-
网络层:标记目标IP信息(IP协议层)
数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8') 端口: - 目标:80 - 本地:6784 IP: - 目标IP:110.242.68.3(百度) - 本地IP:192.168.10.1
-
数据链路层:对数据进行分组并设置源和目标mac地址
数据:"POST /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8') 端口: - 目标:80 - 本地:6784 IP: - 目标IP:110.242.68.3(百度) - 本地IP:192.168.10.1 MAC: - 目标MAC:FF-FF-FF-FF-FF-FF - 本机MAC:11-9d-d8-1a-dd-cd
-
物理层:将二进制数据在物理媒体上传输。
2.2 接收数据
- 物理层: 接收二进制数据,根据具体的二进制信息定位目标
- 数据链路层:确定目标 端口信息 解包
- 网络层:
- 传输层
- 会话层
- 表示层
- 应用层
2.3 开发内容
在开发过程中其实只能体现:应用层、表示层、会话层、传输层,其他层的处理都是在网络设备中自动完成的。
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('110.242.68.3', 80)) # 向服务端发送了数据包
key = "你好"
# 应用层
content = "GET /s?wd={} http1.1\r\nHost:www.baidu.com\r\n\r\n".format(key)
# 表示层
content = content.encode("utf-8")
client.sendall(content)
result = client.recv(8196)
print(result.decode('utf-8'))
# 会话层 & 传输层
client.close()
3.UDP
3.1 简述:
UDP(User Data Protocol)用户数据报协议, 是⼀个⽆连接的简单的⾯向数据报的传输层协议。 UDP不提供可靠性, 它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。 由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快。常用在语音通话,视频通话,实时游戏画面等对速度要求高,对是否传输到位要求相对不高的场合。
3.2 实例
服务端:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8002))
while True:
data, (host, port) = server.recvfrom(1024) # 阻塞
print(data, host, port)
server.sendto("好的".encode('utf-8'), (host, port))
客户端:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 主要体现在 client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
text = input("请输入要发送的内容:")
if text.upper() == 'Q':
break
client.sendto(text.encode('utf-8'), ('127.0.0.1', 8002))
data, (host, port) = client.recvfrom(1024)
print(data.decode('utf-8'))
client.close()
4.TCP
4.1 简述
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。 常见有:网站、手机APP数据获取等。
4.2 示例
-
服务端
import socket # 1.监听本机的IP和端口 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: # 2.等待,有人来连接(阻塞) conn, addr = sock.accept() # 3.等待,连接者发送消息(阻塞) client_data = conn.recv(1024) print(client_data) # 4.给连接者回复消息 conn.sendall(b"hello world") # 5.关闭连接 conn.close() # 6.停止服务端程序 sock.close()
-
客户端
import socket # 1. 向指定IP发送连接请求 client = socket.socket() client.connect(('127.0.0.1', 8001)) # 2. 连接成功之后,发送消息 client.sendall(b'hello') # 3. 等待,消息的回复(阻塞) reply = client.recv(1024) print(reply) # 4. 关闭连接 client.close()
4.3 三次握手和四次挥手
网络中的双方想要基于TCP连接进行通信,必须要经过:
-
创建连接,客户端和服务端要进行三次握手。
# 服务端 import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: conn, addr = sock.accept() # 等待客户端连接 ...
# 客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 8001)) # 发起连接
以上为连接过程,在这个连接的过程中经历了三次握手:
客户端 服务端
1. SYN-SENT --> <seq=100><CTL=SYN> --> SYN-RECEIVED
2. ESTABLISHED <-- <seq=300><ack=101><CTL=SYN,ACK> <-- SYN-RECEIVED
3. ESTABLISHED --> <seq=101><ack=301><CTL=ACK> --> ESTABLISHED
At this point, both the client and server have received an acknowledgment of the connection. The steps 1, 2 establish the connection parameter (sequence number) for one direction and it is acknowledged. The steps 2, 3 establish the connection parameter (sequence number) for the other direction and it is acknowledged. With these, a full-duplex communication is established.
-
传输数据
在收发数据的过程中,只有有数据的传送就会有应答(ack),如果没有ack,那么内部会尝试重复发送。
-
关闭连接,客户端和服务端要进行4次挥手。 代码体现的是conn.close(),但是tcp协议内部要经历四次挥手,具体就是如下体现
import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: conn, addr = sock.accept() ... conn.close() # 关闭连接 sock.close()
import socket client = socket.socket() client.connect(('127.0.0.1', 8001)) ... client.close() # 关闭连接
TCP A TCP B 1. FIN-WAIT-1 --> <seq=100><ack=300><CTL=FIN,ACK> --> CLOSE-WAIT 2. FIN-WAIT-2 <-- <seq=300><ack=101><CTL=ACK> <-- CLOSE-WAIT 3. TIME-WAIT <-- <seq=300><ack=101><CTL=FIN,ACK> <-- LAST-ACK 4. TIME-WAIT --> <seq=101><ack=301><CTL=ACK> --> CLOSED
5.消息和文件上传实例
5.1 消息传输实例
-
服务端
import socket # 1.监听本机的IP和端口 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) # 127.0.0.1 或 查看自己局域网本地IP地址 sock.listen(5) while True: # 2.等待,有人来连接(阻塞) conn, addr = sock.accept() print("有人来连接了...") # 3.连接成功后立即发送 conn.sendall("欢迎使用xx系统,请输入您想要办理的业务!".encode("utf-8")) while True: # 3.等待接受信息 data = conn.recv(1024) if not data: break data_string = data.decode("utf-8") # 4.回复消息 conn.sendall("你说啥?".encode("utf-8")) print("断开连接了") # 5.关闭与此人的连接 conn.close() # 6.停止服务端程序 sock.close()
-
客户端
import socket # 1. 向指定IP发送连接请求 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8001)) # 2.连接成功后,获取系统登录信息 message = client.recv(1024) print(message.decode("utf-8")) while True: content = input("请输入(q/Q退出):") if content.upper() == 'Q': break client.sendall(content.encode("utf-8")) # 3. 等待,消息的回复 reply = client.recv(1024) print(reply.decode("utf-8")) # 关闭连接,关闭连接时会向服务端发送空数据。 client.close()
5.2 案例:文件上传
-
服务端
import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) # 127.0.0.1 或 查看自己局域网本地IP地址 sock.listen(5) conn, addr = sock.accept() # 接收文件大小 data = conn.recv(1024) total_file_size = int(data.decode('utf-8')) # 接收文件内容 file_object = open('xxx.png', mode='wb') recv_size = 0 while True: # 每次最多接收1024字节 data = conn.recv(1024) file_object.write(data) file_object.flush() recv_size += len(data) # 上传完成 if recv_size == total_file_size: break # 接收完毕,关闭连接 conn.close() sock.close()
-
客户端
import time import os import socket client = socket.socket() client.connect(('127.0.0.1', 8001)) file_path = input("请输入要上传的文件:") # 先发送文件大小 file_size = os.stat(file_path).st_size client.sendall(str(file_size).encode('utf-8')) print("准备...") time.sleep(2) print("开始上传..") file_object = open(file_path, mode='rb') read_size = 0 while True: chunk = file_object.read(1024) # 每次读取1024字节 client.sendall(chunk) read_size += len(chunk) if read_size == file_size: break client.close()
-
文件传输的部分主要用于上传与下载文件的相关操作,与消息的不同点在于增加了文件的打开,写入,关闭的操作。其余的部分是相同的。
6.粘包
6.1 应用场景
两台电脑在进行收发数据时,其实不是直接将数据传输给对方。
- 对于发送者,执行
sendall/send
发送消息时,是将数据先发送至自己网卡的 写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。 - 对于接受者,执行
recv
接收消息时,是从自己网卡的读缓冲区获取数据。
所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。
6.2 解决方案
每次发送的消息时,都将消息划分为 头部(固定字节长度) 和 数据 两部分。例如:头部,用4个字节表示后面数据的长度。
- 发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。
- 接收数据,先读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,)
6.3 案例
服务端:
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 socket
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()
7.非阻塞与IO多路复用
7.1 非阻塞
默认我们的TCP通讯程序是阻塞的,需要等待对面的消息,可以通过:
sock.setblocking(False) # 加上就变为了非阻塞
示例:
# ################### 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('密密麻麻'.encode('utf-8'))
client.close()
- 非阻塞的结果:如果代码变成了非阻塞,程序运行时一旦遇到
accept
、recv
、connect
就会抛出 BlockingIOError 的异常。这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。
7.2 IO多路复用
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求,例如:
# ################### 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 ] 放所有的连接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)
#最多花0.05s时间查一下inputs内的server是否有client给其发数据,或者有连接变化的情况,没有检测到的话,r就是空列表 0.05s轮询一次
# 每隔0.05s查看一下有没有新的连接,有新的连接就查一下conn
print('*'*40)
print("r:",r)
print("w:",w)
#
for sock in r:
# 有新连接时才会在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多路复用 + 非阻塞,可以实现让TCP的客户端同时发送多个请求,例如:去某个网站发送下载图片的请求。
import socket
import select
import uuid
import os
client_list = [] # socket对象列表
for i in range(5):
client = socket.socket()
client.setblocking(False)
try:
# 连接百度,虽然有异常BlockingIOError,但向还是正常发送连接的请求
client.connect(('47.98.134.86', 80))
except BlockingIOError as e:
pass
client_list.append(client)
recv_list = [] # 放已连接成功,且已经把下载图片的请求发过去的socket
while True:
# w = [第一个socket对象,]
# r = [socket对象,]
r, w, e = select.select(recv_list, client_list, [], 0.1)
for sock in w:
# 连接成功,发送数据
# 下载图片的请求
sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
recv_list.append(sock)
client_list.remove(sock)
for sock in r:
# 数据发送成功后,接收的返回值(图片)并写入到本地文件中
data = sock.recv(8196)
content = data.split(b'\r\n\r\n')[-1]
random_file_name = "{}.png".format(str(uuid.uuid4()))
with open(os.path.join("images", random_file_name), mode='wb') as f:
f.write(content)
recv_list.remove(sock)
if not recv_list and not client_list:
break
"""
优点:
1. 可以伪造除并发的现象。
"""
基于 IO多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中
- IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。
- 非阻塞,socket的connect、recv过程不再等待。
注意:IO多路复用只能用来监听 IO对象 是否发生变化,常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。
在Linux操作系统化中 IO多路复用 有三种模式,分别是:select,poll,epoll。(windows 只支持select模式)
监测socket对象是否新连接到来 or 新数据到来。
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
补充:socket + 非阻塞+ IO多路复用(IO操作对象都可以监测 + 文件)。
- 回调机制:callback的解释,不同于轮询的方式,轮询是服务端依次询问客户端有没有数据变化,这样就可能造成每次轮询都会有一定的延时,这样实时性可能较差,回调的机制就是当客户端发生变化时告诉server。换到这里就是哪个conn变化了,哪个IO变化了就会反馈给epoll,这样的效率就高了。