python之网络编程socket
一、网络基础
1.1软件开发的架构
- C/S架构
C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
- B/S架构
B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。
B/S是特殊的C/S
1.2物理设备
网卡 :是一个实际存在在计算机中的硬件,每一块网卡上都有一个全球唯一的mac地址
交换机 :是连接多台机器并帮助通讯的物理设备,只认识mac地址
arp协议(地址解析协议) :
交换机通过ip获取mac地址,通过广播查找,单播建联
路由器:完成局域网与局域网之间的通信
1.3ip地址
-
ipv4协议 4位的点分十进制 32位2进制表示
0.0.0.0 - 255.255.255.255
-
ipv6协议 6位的冒分十六进制 128位2进制表示
0:0:0:0:0:0-FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
公网ip
每一个ip地址要想被所有人访问到,那么这个ip地址必须是你申请的
内网ip
192.168.0.0 - 192.168.255.255
172.16.0.0 - 172.31.255.255
10.0.0.0 - 10.255.255.255
网关ip
一个局域网的网络出口,访问局域网之外的区域都需要经过路由器和网关
网段
指的是一个地址段 x.x.x.0 x.x.0.0 x.0.0.0
子网掩码
判断两台机器是否在同一个网段内的
255.255.255.0
端口:定位一台机器上的一个服务
0-65535
ip + port 确认一台机器上的一个应用
二、传输协议
2.1TCP协议
TCP(传输控制协议),面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。
TCP支持的应用协议:Telnet(远程登录)、FTP(文件传输协议)、SMTP(简单邮件传输协议)。TCP用于传输数据量大,可靠性要求高的应用。
可靠 慢 全双工通信
# 建立连接的时候 : 三次握手
# 断开连接的时候 : 四次挥手
# 在建立起连接之后
# 发送的每一条信息都有回执
# 为了保证数据的完整性,还有重传机制
# 长连接 :会一直占用双方的端口
# 能够传递的数据长度几乎没有限制
# 三次握手
# accept接受过程中等待客户端的连接
# connect客户端发起一个syn链接请求
# 如果得到了server端响应ack的同时还会再收到一个由server端发来的syc链接请求
# client端进行回复ack之后,就建立起了一个tcp协议的链接
# 三次握手的过程再代码中是由accept和connect共同完成的,具体的细节在socket中没有体现出来
# 四次挥手
# server和client端对应的在代码中都有close方法
# 每一端发起的close操作都是一次fin的断开请求,得到'断开确认ack'之后,就可以结束一端的数据发送
# 如果两端都发起close,那么就是两次请求和两次回复,一共是四次操作
# 可以结束两端的数据发送,表示链接断开了
2.2UDP协议
UDP(用户数据报协议,User Data Protocol)
- 面向非连接的(正式通信前不必与对方建立连接,不管对方状态就直接发送,像短信,QQ),不能提供可靠性、流控、差错恢复功能。UDP用于一次只传送少量数据,可靠性要求低、传输经济等应用。
- UDP支持的应用协议:NFS(网络文件系统)、SNMP(简单网络管理系统)、DNS(主域名称系统)、TFTP(通用文件传输协议)等。
tcp协议和udp协议的特点:
tcp是一个面向连接的流式的全双工通信,比较慢,开销大,但是数据传输可靠,能够传递的数据长度几乎没有限制。
# 邮件 文件 http web
udp是一个面向数据报的无连接通信,比较快,数据传输不可靠,能够传递的数据长度有限度,能完成一对一、一对多、多 对一、多对多的高效通讯协议
# 即时聊天工具 视频的在线观看
2.3osi七层模型
人们按照分工不同把互联网协议从逻辑上划分了层级:
三、套接字(socket)
工作在应用层和传输层之间的抽象层,帮助我们完成了所有信息(协议数据、格式等)的组织和拼接
3.1基于TCP协议的socket
server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接,可加多少链接参数
conn,addr = sk.accept() #接受客户端链接,阻塞事件
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
client端
import socket
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1',8898)) # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
sk.close() # 关闭客户套接字
3.2基于UDP协议的socket
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
server端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr) # 对话(接收与发送)
udp_sk.close() # 关闭服务器套接字
client端
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
计算机上的存储 / 网线上数据的传输 --> 二进制
# send
# str-encode(编码)->bytes
# recv
# bytes-decode(编码)-> str
四、TCP粘包现象
4.1什么是粘包现象
情况一 发送方的缓存机制
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合到一起,产生粘包)
情况二 接收方的缓存机制
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
# 发生在发送端的粘包
# 由于两个数据的发送时间间隔短+数据的长度小
# 所以由tcp协议的优化机制将两条信息作为一条信息发送出去了
# 为了减少tcp协议中的“确认收到”的网络延迟时间
# 发生在接收端的粘包
# 由于tcp协议中所传输的数据无边界,所以来不及接收的多条
# 数据会在接收放的内核的缓存端黏在一起
# 本质: 接收信息的边界不清晰
总结
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
4.2解决粘包方法
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
ret = struct.pack('i',100000) #返回字节类型,i表示数字内容
print(ret) #b'\xa0\x86\x01\x00'
rst=struct.unpack('i',ret) #返回元组
print(rst) #(100000,)
解决方法:自定义协议
自定义协议1:消息发送的协议
首先发送报头
# 报头长度4个字节
# 内容是 即将发送的报文的字节长度
# struct模块,pack 能够把所有的数字都固定的转换成4字节
再发送报文
自定义协议2:文件发送的协议
先发送报头字典的字节长度
再发送字典(字典中包含文件的名字、大小。。。)
再发送文件的内容
客户端-发送端:
import struct
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
msg = b'hello'
byte_len = struct.pack('i',len(msg))
sk.send(byte_len) # 1829137
sk.send(msg) # 1829139
msg = b'world'
byte_len = struct.pack('i',len(msg))
sk.send(byte_len)
sk.send(msg)
sk.close()
客户端-接收端:
import struct
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
conn,_ = sk.accept()
byte_len = conn.recv(4)
size = struct.unpack('i',byte_len)[0]
msg1 = conn.recv(size)
print(msg1)
byte_len = conn.recv(4)
size = struct.unpack('i',byte_len)[0]
msg2 = conn.recv(size)
print(msg2)
conn.close()
sk.close()
五、非阻塞io模型
IO模型:
- 阻塞io模型
- 非阻塞io模型
- 事件驱动io
- io多路复用
- 异步io模型
非阻塞io模型server端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.setblocking(False) #设置链接为非阻塞
sk.listen()
conn_l = []
del_l = []
while True:
try:
conn,addr = sk.accept() # 不再阻塞,接收不到连接报错
print(conn)
conn_l.append(conn)
except BlockingIOError:
for c in conn_l:
try:
msg = c.recv(1024).decode('utf-8') #收不到消息报错
if not msg:
del_l.append(c)
continue
print('-->',[msg])
c.send(msg.upper().encode('utf-8'))
except BlockingIOError:pass
for c in del_l:
conn_l.remove(c)
del_l.clear()
sk.close()
虽然非阻塞,提高了CPU的利用率,但是耗费CPU做了很多无用功
所有网络方面的并发框架,模块,都是基于socket的非阻塞io模型 + io多路复用实现的
六、socketserver模块
直接实现tcp协议,可并发的server端
import socketserver
class Myserver(socketserver.BaseRequestHandler):
def handle(self): # 自动触发了handle方法,并且self.request == conn
msg = self.request.recv(1024).decode('utf-8')
self.request.send('1'.encode('utf-8'))
msg = self.request.recv(1024).decode('utf-8')
self.request.send('2'.encode('utf-8'))
msg = self.request.recv(1024).decode('utf-8')
self.request.send('3'.encode('utf-8'))
server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),Myserver)
server.serve_forever()
七、验证客户端的合法性
验证客户端的合法性再下发消息,防止位置客户端恶意链接。
import socket
import time
import hmac
def get_secret(secret_key,randseq):
h = hmac.new(secret_key,randseq)
res = h.digest() #直接返回bytes类型
return res
def chat(sk):
while True:
sk.send(b'hello')
msg = sk.recv(1024).decode('utf-8')
print(msg)
time.sleep(0.5)
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
secret_key = b'lhlhlk'
randseq = sk.recv(32)
hmaccode = get_md5(secret_key,randseq)
sk.send(hmaccode)
chat(sk)
sk.close()
import os
import hmac
import socket
def get_secret(secret_key,randseq):
h = hmac.new(secret_key,randseq)
res = h.digest()
return res
def chat(conn):
while True:
msg = conn.recv(1024).decode('utf-8')
print(msg)
conn.send(msg.upper().encode('utf-8'))
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
secret_key = b'lhlhlk'
while True:
conn,addr = sk.accept()
# 获取随机字节串
randseq = os.urandom(32)
conn.send(randseq)
hmaccode = get_md5(secret_key,randseq)
ret = conn.recv(16)
print(ret)
if ret == hmaccode:
print('是合法的客户端')
chat(conn)
else:
print('不是合法的客户端')
conn.close()
sk.close()
八、io多路复用
操作系统提供,为了提高网络操作的并发效果,并且减少CPU的使用率才被推出的一个工具
-
windows: select
-
linux: select poll epoll
为什么叫IO多路复用 :操作系统提供的 网络对象监听的代理
select 机制:
操作系统会帮助程序对需要监听的所有对象进行轮询,一旦有数据来,就会把对应的对象返回
由于底层数据结构的问题,select能够代理的网络对象是有限的1024个,随着监听的对象增多 效率也会降低
poll 机制:
轮询 随着监听的对象增多 效率也会降低
对底层数据结构进行了优化 能够代理的网络对象没有限制了
epoll 机制:--nginx采用此机制
不再采用轮询的机制 采用的是回调机制
无论监听多少个网络对象,效率都是一样的
兼容模块:import selectors
示例代码:
import socket
import select
sk = socket.socket()
sk.setblocking(False)
sk.bind(('127.0.0.1',9000))
sk.listen()
rlst = [sk]
while True:
rl,wl,el = select.select(rlst,[],[])
print('-->',rl)
for obj in rl:
if obj is sk:
conn,_ = obj.accept()
rlst.append(conn)
else:
msg = obj.recv(1024)
if msg:
print(msg)
obj.send(msg)
else:
rlst.remove(obj)