socket
socket 是工作在应用层和传输层之间的抽象层
tcp 循环发消息
# ### tcp 客户端 import socket # 1.创建一个socket对象 sk = socket.socket() # 2.连接服务端 sk.connect( ("127.0.0.1",9000) ) # 3.收发数据 """ res = sk.recv(1024) # 一次接受的最大字节数是1024 print(res) """ while True: strvar = input("请输入您要发送的数据") sk.send(strvar.encode()) res = sk.recv(1024) if res == b"q": break print(res.decode()) # 4.关闭连接 sk.close()
# ### tcp 服务端 import socket # 1.创建一个socket对象 sk = socket.socket() # 让当前端口重复绑定多个程序(仅仅在测试阶段用) sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 2.在网络中注册主机(绑定ip和端口号) sk.bind( ("127.0.0.1",9000) ) # 3.监听端口 sk.listen() # 4.三次握手 # conn,addr = sk.accept() # 5.收发数据 ''' 数据类型:二进制的字节流 b修饰的字符串 => 代表的是二进制的字节流 里面的字符必须是ascii编码中的字符.不能是中文,否则报错 ''' """ conn.send(b"i love you") """ while True: conn,addr = sk.accept() while True: res = conn.recv(1024) print(res.decode()) strvar = input("请输入服务端给客户端发送的消息:") conn.send(strvar.encode()) # 退出 if strvar == "q": break # 6.四次挥手 conn.close() # 7.退还端口 sk.close() 调用类对象中的成员使用 对象.属性 对象.方法 类.属性 类.方法
三次握手,四次挥手后建立稳定的连接
TCP 收发数据
tcp粘包
解决粘包问题
import struct """ pack: 把任意长度的数字转化成居右4个字节的固定长度的字节流 unpack: 把4个字节值恢复成原本的数字,返回是元组 """ # i => int 要转化的当前数据时整型 """pack的数值范围不是无限的,上限大概在21个亿左右,不要超过这个值""" res = struct.pack("i",999999999) res = struct.pack("i",1234343433) res = struct.pack("i",2100000011) print(res) print(len(res)) # i => 把对应的数据转换成int, 最后返回元祖 tup = struct.unpack("i",res) print(tup[0]) # (2100000011,) """ #解决黏包场景: 应用场景在实时通讯时,需要阅读此次发的消息是什么 #不需要解决黏包场景: 下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓. """
class MyTCPClient: """客户端""" family = socket.AF_INET type = socket.SOCK_STREAM server_addr=(('127.0.0.1',9000)) def __init__(self,self.server_addr,connect=True): self.socket = socket.socket(self.family,self.type) if connect: try: self.socket.connect(server_addr) except: self.socket.close() raise def handler_send(self,args): """ 发送字典,处理粘包 args = { 'cmd' : cmd, 'user':user, 'psd':psd, 'filename': filename, 'src_dir': src_dir, 'dst_dir': dst_dir, 'size': size, } """ # 1.发送用户指令的字典 # 1.1 args处理成二进制 args_str = json.dumps(args) args_bytes = args_str.encode('utf8') # 1.2 先发长度 args_len = len(args_bytes) self.socket.send(struct.pack('i',args_len)) # 1.3 再发字典 self.socket.send(args_bytes) # 1.4 如果字典里头有size关键字,再发文件 if args.get('size'): size = args['size'] src_dir = args['src_dir'] with open(src_dir,'rb') as f: send_size = 0 while send_size < size: res = f.read(1024) self.socket.send(res) send_size += len(res) self.view_bar(send_size,size) print('发送完毕!') # 2.接收执行结果的字典 return self.handler_recv() def handler_recv(self): """ 接受指令,处理粘包,拿到字典 res_head_dic = { 'res' :'', 'filename' :'', 'size' :'', } """ while True: try: # 先收报头长度 head_struct = self.socket.recv(4) head_length = struct.unpack('i', head_struct)[0] # 收对应长度报头 head_bytes = self.socket.recv(head_length) head_str = head_bytes.decode('utf8') head_dic = json.loads(head_str) print('\033[31mServer: %s\033[0m' % head_dic['res']) # 如果head_dic中有filename和size,就进一步接收文件 if head_dic.get('size'): size = head_dic['size'] filename = head_dic['filename'] dst_dir = head_dic['dst_dir'] file_hash = head_dic['file_hash'] if not os.path.exists(dst_dir): # 验证保存路径是否存在 os.makedirs(dst_dir) file_dir = os.path.join(dst_dir,filename) with open(file_dir,'wb') as f: # 写入文件 recv_size = 0 while recv_size < size: res = self.socket.recv(1024) f.write(res) recv_size += len(res) self.view_bar(recv_size,size) # 显示接收进度 print() # 校验文件的一致性 dst_hash = self.get_md5_doc(file_dir, size) if not file_hash == dst_hash: print('文件接收不完整,请重发!') else: print('接收完毕!') return head_dic except Exception: break def get_cmd(self): """接受用户指令""" while True: self.help_info() cmd = input('【路径需加双引号】>>>:') if not cmd: continue cmd_li = cmd.split() order = cmd_li[0].strip() if hasattr(self,order): func = getattr(self,order) func(cmd) # 调对应方法把用户要发送的信息打包成字典 elif order == 'quit': self.close_connect(cmd) print('再见!') break else: print('程序暂不支持此操作!')
class MyTCPServer: family = socket.AF_INET type = socket.SOCK_STREAM request_queue_size = 5 user_info = {'user': None} dir_info = {'home': settings.DATA_DIR,'current_dir':settings.DATA_DIR} def __init__(self,server_addr,bind_and_activate = True): """创建socket对象,搭建服务器到监听状态""" self.socket = socket.socket(self.family,self.type) self.server_addr = server_addr if bind_and_activate: try: self.socket.bind(self.server_addr) self.socket.listen(self.request_queue_size) except: self.socket.close() raise def server_get_connect(self): """建立连接""" return self.socket.accept() def handler_recv(self,conn): """ 接收指令,处理粘包,拿到字典 head_dic = { 'cmd' :'', 'user' :'', 'psd' :'', 'filename' :'', 'src_dir': src_dir, 'size' :'', } """ while True: try: # 1.收到用户发来的指令字典 # 1.1 先收报头长度 head_struct = conn.recv(4) head_length = struct.unpack('i', head_struct)[0] # 1.2 收对应长度报头 head_bytes = conn.recv(head_length) head_str = head_bytes.decode('utf8') head_dic = json.loads(head_str) # 1.3 如果head_dic中有filename和size,就进一步接收文件 if head_dic.get('size'): # 2.调用函数处理用户指令,得到处理结果的字典 res_head_dic = self.send_file(head_dic,conn) # 3.发送结果字典 self.handler_send(res_head_dic, conn) else: # 2.order_handler分配给对应函数处理用户指令,得到处理结果的字典 res_head_dic = self.order_handler(head_dic, conn) # 3.发送结果字典 self.handler_send(res_head_dic,conn) except Exception: break def handler_send(self,args,conn): """ 发送字典,处理粘包 args = { 'res': res, 'filename':filename, 'src_dir':src_dir, 'dst_dir':dst_dir, 'size': size, } """ # args处理成二进制 args_str = json.dumps(args) args_bytes = args_str.encode('utf8') # 先发长度 args_len = len(args_bytes) conn.send(struct.pack('i', args_len)) # 再发字典 conn.send(args_bytes) # 如果字典里头有size关键字,再发文件 if args.get('size'): size = args['size'] src_dir = args['src_dir'] send_size = 0 with open(src_dir,'rb') as f: while send_size < size: res = f.read(1024) conn.send(res) send_size += len(res) def order_handler(self,head_dic,conn): """解析dict,拿到并处理用户指令""" cmd = head_dic['cmd'] order = cmd.split()[0] if hasattr(self, order): func = getattr(self, order) return func(head_dic,conn) elif cmd == 'quit': self.close_connect(head_dic,conn)
udp 用于即时交互
# ### 客户端 import socket # type=socket.SOCK_DGRAM => 返回udp协议对象 # 1.创建udp对象 sk = socket.socket(type=socket.SOCK_DGRAM) # 2.发送数据 msg = "大妹子,你好呀?" # sendto(二进制字节流, (ip,端口) ) sk.sendto(msg.encode() , ("127.0.0.1",9000) ) # 客户端接受服务端发过来的数据 msg,ser_addr = sk.recvfrom(1024) print(msg.decode()) print(ser_addr) # 3.关闭连接 sk.close() --------------------------------------------------------------- # ### 服务端 import socket # type=socket.SOCK_DGRAM => 返回udp协议对象 # 1.创建udp对象 sk = socket.socket(type=socket.SOCK_DGRAM) # 2.绑定地址端口号 sk.bind( ("127.0.0.1",9000) ) # 3.接受消息(udp作为服务端的时候,第一次一定是接受消息) msg,cli_addr = sk.recvfrom(1024) print(msg.decode()) print(cli_addr) # ('127.0.0.1', 56184) # 服务端给客户端发消息 msg = "我是老爷们,我不好!" sk.sendto(msg.encode(), cli_addr ) # 4.关闭连接 sk.close()
tcp 和 udp对比
TCP/UDP协议: TCP(Transmission Control Protocol)一种面向连接的、可靠的、传输层通信协议(比如:打电话) 优点:可靠,稳定,传输完整稳定,不限制数据大小 缺点:慢,效率低,占用系统资源高,一发一收都需要对方确认 应用:Web浏览器,电子邮件,文件传输,大量数据传输的场景 UDP(User Datagram Protocol)一种无连接的,不可靠的传输层通信协议(比如:发短信) 优点:速度快,可以多人同时聊天,耗费资源少,不需要建立连接 缺点:不稳定,不能保证每次数据都能接收到 应用:IP电话,实时视频会议,聊天软件,少量数据传输的场景 客户端和服务端在建立连接时: 三次握手 客户端和服务端在断开连接时: 四次挥手 SYN 创建连接 ACK 确认响应 FIN 断开连接 socket socket的意义:通络通信过程中,信息拼接的工具(中文:套接字) # 开发中,一个端口只对一个程序生效,在测试时,允许端口重复捆绑 (开发时删掉) # 在bind方法之前加上这句话,可以让一个端口重复使用 sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 黏包 # tcp协议在发送数据时,会出现黏包现象. (1)数据粘包是因为在客户端/服务器端都会有一个数据缓冲区, 缓冲区用来临时保存数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。 (2)在收发数据频繁时,由于tcp传输消息的无边界,不清楚应该截取多少长度 导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包 黏包出现的两种情况 #黏包现象一: 在发送端,由于两个数据短,发送的时间隔较短,所以在发送端形成黏包 #黏包现象二: 在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包 #总结: 发送端,包之间时间间隔短 或者 接收端,接受不及时, 就会黏包 核心是因为tcp对数据无边界截取,不会按照发送的顺序判断 黏包对比:tcp和udp #tcp协议: 缺点:接收时数据之间无边界,有可能粘合几条数据成一条数据,造成黏包 优点:不限制数据包的大小,稳定传输不丢包 #udp协议: 优点:接收时候数据之间有边界,传输速度快,不黏包 缺点:限制数据包的大小(受带宽路由器等因素影响),传输不稳定,可能丢包 #tcp和udp对于数据包来说都可以进行拆包和解包,理论上来讲,无论多大都能分次发送 但是tcp一旦发送失败,对方无响应(对方无回执),tcp可以选择再发,直到对应响应完毕为止 而udp一旦发送失败,是不会询问对方是否有响应的,如果数据量过大,易丢包 解决黏包问题 #解决黏包场景: 应用场景在实时通讯时,需要阅读此次发的消息是什么 #不需要解决黏包场景: 下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓. 模块 socketserver #网络协议的最底层就是socket,基于原有socket模块,又封装了一层,就是socketserver socketserver 为了实现tcp协议,server端的并发.