- socket套接字
- 通信循环
- 链接循环
- 半连接池
- 粘包问题
- UDP协议
需求:编写一个cs架构的程序 实现数据交互
操作OSI七层是所有cs架构的程序都需要经历的过程 所以有固定的模块
socket套接字是一门技术;socket模块>>>:提供了快捷方式 不需要自己处理每一层
以后我们写软件连socket的影子都看不到,因为被封装起来
socket是最底层的原理,很多框架都封装了,其实我们不需要深入研究
socket模块cs架构的软件无论是在编写还是运行,都应该先考虑服务端
服务端:
import socket server = socket.socket() # 买手机 """ 通过查看源码,括号内不写参数默认就是基于网络的遵循TCP协议的套接字 """ server.bind(('192.168.199.229',8888)) # 查电话卡 server.listen(5) # 开机 # 半连接池(先写,后面讲) sock,addr = server.accept() # 等待接听电话,没人来就原地等待(程序阻塞) print(addr) # 客户端的地址 data = sock.recv(1024) # 接受1024个字节,听别人说话 print(data.decode('utf8')) # 二进制格式需要解码 sock.send('hello'.encode('utf8')) """基于网络传输必须是bytes类型,需要编码,后面加encode""" sock.close() # 挂电话 server.close() # 关机View Code
客户端:
import socket client = socket.socket() # 买手机 产生一个socket对象 client.connect(('192.168.199.229',8888)) # 直接拨号 根据服务端的地址链接 # 数据交互 client.send(b'hello') # 给服务端发送消息 data = client.recv(1024) # 接受服务端回复的消息 print(data.decode('utf8')) # 解码是因为是二进制格式 client.close() # 关闭客户端View Code
服务端与客户端首次交互:一边是recv那么另一边必须是send,两边不能相同,否则就'冷战'了!!!
通信循环基于上面的代码发现的问题:
只能发送一次消息就交互结束了1.先解决消息固定的问题:input
服务端:
import socket server = socket.socket() # 买手机 """ 通过查看源码,括号内不写参数默认就是基于网络的遵循TCP协议的套接字 """ server.bind(('192.168.199.229',8888)) # 查电话卡 server.listen(5) # 开机 # 半连接池(先写,后面讲) sock,addr = server.accept() # 等待接听电话,没人来就原地等待(程序阻塞) print(addr) # 客户端的地址 data = sock.recv(1024) # 接受1024个字节,听别人说话 print(data.decode('utf8')) # 二进制格式需要解码 msg = input('请回复消息>>>:').strip() sock.send(msg.encode('utf8')) """基于网络传输必须是bytes类型,需要编码,后面加encode""" sock.close() # 挂电话 server.close() # 关机View Code
客户端:
import socket client = socket.socket() # 买手机 产生一个socket对象 client.connect(('192.168.199.229',8888)) # 直接拨号 根据服务端的地址链接 # 数据交互 msg = input('请输入消息>>>:').strip() client.send(msg.encode('utf8')) # 给服务端发送消息 data = client.recv(1024) # 接受服务端回复的消息 print(data.decode('utf8')) # 解码是因为是二进制格式 client.close() # 关闭客户端View Code
2.再解决通信循环的问题:循环
服务端:
import socket server = socket.socket() # 买手机 """ 通过查看源码,括号内不写参数默认就是基于网络的遵循TCP协议的套接字 """ server.bind(('192.168.199.229',8888)) # 查电话卡 server.listen(5) # 开机 # 半连接池(先写,后面讲) sock,addr = server.accept() # 等待接听电话,没人来就原地等待(程序阻塞) print(addr) # 客户端的地址 while True: data = sock.recv(1024) # 接受1024个字节,听别人说话 print(data.decode('utf8')) # 二进制格式需要解码 msg = input('请回复消息>>>:').strip() sock.send(msg.encode('utf8')) """基于网络传输必须是bytes类型,需要编码,后面加encode""" sock.close() # 挂电话 server.close() # 关机View Code
客户端:
import socket client = socket.socket() # 买手机 产生一个socket对象 client.connect(('192.168.199.229',8888)) # 直接拨号 根据服务端的地址链接 # 数据交互 while True: msg = input('请输入消息>>>:').strip() client.send(msg.encode('utf8')) # 给服务端发送消息 data = client.recv(1024) # 接受服务端回复的消息 print(data.decode('utf8')) # 解码是因为是二进制格式 client.close() # 关闭客户端View Code 链接循环
基于上面的代码发现的问题:
1.发送消息为空会‘冷战’解决:在客户端统计长度判断即可
客户端:
import socket client = socket.socket() # 买手机 产生一个socket对象 client.connect(('192.168.199.229',8888)) # 直接拨号 根据服务端的地址链接 # 数据交互 while True: msg = input('请输入消息>>>:').strip() if len(msg) == 0: continue client.send(msg.encode('utf8')) # 给服务端发送消息 data = client.recv(1024) # 接受服务端回复的消息 print(data.decode('utf8')) # 解码是因为是二进制格式 client.close() # 关闭客户端View Code 2.反复重启服务器可能会报错>>>: address in use
解决:在服务端加入两行代码
服务端:
import socket server = socket.socket() # 买手机 from socket import SOL_SOCKET,SO_REUSEADDR """ 通过查看源码,括号内不写参数默认就是基于网络的遵循TCP协议的套接字 """ server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('192.168.199.229',8888)) # 查电话卡 server.listen(5) # 开机 # 半连接池(先写,后面讲) sock,addr = server.accept() # 等待接听电话,没人来就原地等待(程序阻塞) print(addr) # 客户端的地址 while True: data = sock.recv(1024) # 接受1024个字节,听别人说话 print(data.decode('utf8')) # 二进制格式需要解码 msg = input('请回复消息>>>:').strip() sock.send(msg.encode('utf8')) """基于网络传输必须是bytes类型,需要编码,后面加encode""" sock.close() # 挂电话 server.close() # 关机View Code 3.链接循环
如果是windows,客户端异常退出之后服务端会直接报错
解决:异常处理
服务端:
import socket server = socket.socket() # 买手机 from socket import SOL_SOCKET,SO_REUSEADDR """ 通过查看源码,括号内不写参数默认就是基于网络的遵循TCP协议的套接字 """ server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('192.168.199.229',8888)) # 查电话卡 server.listen(5) # 开机 # 半连接池(先写,后面讲) sock,addr = server.accept() # 等待接听电话,没人来就原地等待(程序阻塞) print(addr) # 客户端的地址 while True: try: data = sock.recv(1024) # 接受1024个字节,听别人说话 if len(data) == 0: break print(data.decode('utf8')) # 二进制格式需要解码 msg = input('请回复消息>>>:').strip() if len(msg) == 0: msg = '太忙' sock.send(msg.encode('utf8')) """基于网络传输必须是bytes类型,需要编码,后面加encode""" except Exception: break sock.close() # 挂电话 server.close() # 关机View Code
如果是mac或者linux,服务端会接收到一个空消息
解决:len 判断
客户端如果异常断开 服务端代码应该重新回到accept等待
目前我们的服务端只能实现一次服务一个人,不能做到同时服务多个 学了并发才可以实现
半连接池listen(5)
py文件默认同一时间只能运行一次 如果想单独分开运行多次:
Edit Configurations >>> Allow parallel run 允许并行
这个‘5’就是最大的等待人数
把一开始的停掉之后,后面等待的就会陆陆续续的可以进来
粘包问题服务端:
import socket server = socket.socket() server.bind(('127.0.0.1',9999)) server.listen(5) coon,addr = server.accept() data = coon.recv(1024) print(data) data1 = coon.recv(1024) print(data1) data2 = coon.recv(1024) print(data2)View Code
客户端:
import socket client = socket.socket() client.connect(('127.0.0.1',9999)) client.send(b'hello') client.send(b'big') client.send(b'pig')View Code
这里服务端的打印结果为:
b'hellobigpig' b'' b''View Code
得出结论:TCP协议的特点
会将数据量比较小并且时间间隔比较短的数据整合到一起发送
并且还会受制于recv括号内的数字大小(核心问题!!!)
流式协议:跟水流一样不间断
问题产生的原因:因为recv括号内我们不知道即将要接收的数据到底多大
如果每次接收的数据我们都能够精确的知道它的大小 那么肯定不会出现黏包
解决粘包问题方向:精准获取数据的大小 struct模块
先看代码演示:
import struct data1 = 'hello world!' print(len(data1)) # 12 res1 = struct.pack('i', len(data1)) # 第一个参数是格式 写i就可以了 print(len(res1)) # 4 ret1 = struct.unpack('i', res1) print(ret1) # (12,) data2 = 'hello baby baby baby baby baby baby baby baby' print(len(data2)) # 45 res2 = struct.pack('i', len(data2)) print(len(res2)) # 4 ret2 = struct.unpack('i', res2) print(ret2) # (45,)View Code
pack可以将任意长度的数字打包成固定长度
unpack可以将固定长度的数字解包成打包之前数据真实的长度
举例子:
import struct data_dict = { 'name':'动漫.zip', 'pwd':'2222', 'size':'00000000000000' } print(len(data_dict)) # 3 res = struct.pack('i',len(data_dict)) print(len(res)) # 4View Code 解决粘包问题的代码演示:
解决粘包问题的思路:
- 先将真实数据打包成固定长度的包
- 将固定长度的包先发给对方
- 对方接收到包之后再解包获取真实数据长度
- 接收真实数据长度
服务端:
import os import struct import socket import json server = socket.socket() server.bind(('127.0.0.1',9999)) server.listen(5) coon,addr = server.accept() while True: data_dict = { 'file_name':'aaa.txt', 'file_desc':'封了一个月', 'file_size':os.path.getsize(r'1.py') } # 先打包字典 dict_json_str = json.dumps(data_dict) dict_bytes = dict_json_str.encode('utf8') dict_package_header = struct.pack('i',len(dict_bytes)) # 发送报头 coon.send(dict_package_header) # 发送字典数据 coon.send(dict_bytes) # 发送真实数据 with open(r'1.py','rb')as f: for line in f : coon.send(line)View Code
客户端:
import socket import struct import json client = socket.socket() client.connect(('127.0.0.1',9999)) # 先接收固定长度的字典的报头 dict_header_len = client.recv(4) # 解析出字典的真是长度 dict_real_len = struct.unpack('i',dict_header_len)[0] # 接受字典数据 dict_data_bytes = client.recv(dict_real_len) dict_data = json.loads(dict_data_bytes) print(dict_data) # 循环接受文件数据 不要一次性接受 recv_size = 0 with open(dict_data.get('file_name'),'wb')as f: while recv_size < dict_data.get('file_size'): data = client.recv(1024) recv_size += len(data) f.write(data)View Code
打印结果为:
{'file_name': 'aaa.txt', 'file_desc': '封了一个月', 'file_size': 715}View Code
recv括号内的数字尽量不要写太大 1024 2048 4096足够了
字典数据很难突破上面的数值
所以针对大文件的接收应该采用循环的形式一次接受一点点
UDP协议服务端:
import socket server = socket.socket(type=socket.SOCK_DGRAM) # 手动自己指定UDP协议,默认是TCP协议 server.bind(('127.0.0.1',8888)) """UDP协议不需要半连接池,不需要等待""" msg,add = server.recvfrom(1024) """一般情况下UDP协议很少用来发大的数据""" """也不需要考虑黏包问题""" print('msg>>>:',msg.decode('utf8')) # msg是内容,需要解码 print('add>>>:',add) server.sendto(b'hello',add) # sendto 给谁发View Code
客户端:
import socket client = socket.socket(type = socket.SOCK_DGRAM) # 直接发送即可 server_add = ('127.0.0.1',8888) # 查找通讯录跟谁进行交互 client.sendto(b'hello',server_add) msg,add = client.recvfrom(1024) print('msg>>>:',msg.decode('utf8')) # msg是内容,需要解码 print('add>>>:',add)View Code
执行之后的结果:
服务端:
msg>>>: hello
add>>>: ('127.0.0.1', 64341)
客户端:
msg>>>: hello
add>>>: ('127.0.0.1', 8888)
基于UDP收发消息,消息都会带着给你发的人的地址
UDP协议跟TCP协议不同在于需要自己携带一个地址,一个是recvfrom,一个是recv.
并且没有所谓的粘包问题
服务端:
import socket server = socket.socket(type=socket.SOCK_DGRAM) # 手动自己指定UDP协议,默认是TCP协议 server.bind(('127.0.0.1',8888)) """UDP协议不需要半连接池,不需要等待""" msg,add = server.recvfrom(1024) msg1,add1 = server.recvfrom(1024) msg2,add2 = server.recvfrom(1024) """一般情况下UDP协议很少用来发大的数据""" """也不需要考虑黏包问题""" print(msg) print(msg1) print(msg2)View Code
客户端:
import socket client = socket.socket(type = socket.SOCK_DGRAM) # 直接发送即可 server_add = ('127.0.0.1',8888) # 查找通讯录跟谁进行交互 client.sendto(b'hello',server_add) client.sendto(b'hello1',server_add) client.sendto(b'hello2',server_add)View Code
服务端打印结果:发一次收一次
b'hello'
b'hello1'
b'hello2'
UDP协议一般不会用来做大数据的数据交互,QQ微信就是基于UDP协议
基于UDP协议实现简易版本的QQ服务端:
import socket server = socket.socket(type=socket.SOCK_DGRAM) # 手动自己指定UDP协议,默认是TCP协议 server.bind(('127.0.0.1',8888)) while True: msg,add = server.recvfrom(1024) print(add) print(msg.decode('utf8')) back_msg = input('回复消息>>>:').strip() server.sendto(back_msg.encode('utf8'),add)View Code
客户端:
import socket client = socket.socket(type = socket.SOCK_DGRAM) # 直接发送即可 server_add = ('127.0.0.1',8888) # 查找通讯录跟谁进行交互 while True: msg = input('请输入聊天内容>>>:').strip() msg = '来自客户端1的消息:%s'%msg client.sendto(msg.encode('utf8'),server_add) msg,add = client.recvfrom(1024) print(msg.decode('utf8'),add)View Code
也可以支持多个客户端