##内容回顾
#1.socket 套接字,本质上是一个模块,里面封装了一些网络通讯协议 是处于传输层和应用层之间的一个抽象层,实际在OSI中并不存在 也就是没有socket也能能够通讯 ,但是这样一来 我们必须完全按照OSI规定的各种协议来编码 这是一个重复,复杂的过程,为了提高开发效率,就出现了socket模块,专门帮我们封装了传输层以下的一堆协议 有socket模块之后 当我们需要编写网络通讯的程序时,就不需要在关心具体的协议细节,直接使用socket提供的功能接口即可 “”“ 门面设计模式,隐藏复杂的内部细节,仅提供最简单的使用接口 本质就是封装 ”“” #2.TCP通讯 网络通讯一定分为两端,服务器和客户端 服务器: 1.创建socket对象 server = socket.socket() 2.绑定一个固定的ip和端口 server.bind ip必须是本机ip 端口1024-65535 不要使用常见的端口 web:80 / 8080 mysql 3306 ssh:22 ftp:21 3.开始监听客户端的到来 server.listen 4.接收客户端的链接请求 server.accept # 阻塞直到客户链接到来 没有新连接则不可能执行该函数 5.收发数据 需要循环 recv 收 收多少字节数 send 发 只能发二进制数据 客户端: 1.创建socket对象 client = socket.socket() 2.链接服务器 client.connect((ip,port)) 3.收发数据 通常需要循环 send 发 只能发二进制数据 recv 收 收多少字节数 4.断开链接 client.close() #3.常见的异常 1.一方异常下线,另一方还要收发数据 ,ConnectionResetError 在send recv的地方 加上try 客户端也需要异常处理 2.端口占用异常 重复运行服务器 之前的进程没有正确关闭 关闭和打开端口 都是操作系统负责 在一些极端情况下 可能应用程序已经正确接收并且通知了操作系统要关闭端口 但是操作系统 没及时处理 2.1 更换端口 2.2 查后台进程 杀掉它 2.3 重启服务器电脑 #4循环通讯
##半连接数
“”“ 三次握手没有完成 称之为半连接 原因1 恶意客户端没有返回第三次握手信息 原因2 服务器没空及时处理你的请求 socket中 listen(半连接最大数量) ”“” #理解 import socket server = socket.socket() server.bind(("127.0.0.1",1688)) server.listen(5) # backlog # 了解4 # 最大半连接数 本质就是一个数组 未完成链接的socket 就会被加入到数组中 , # 每一次执行accept 就会挑一个来完成三次握手 ,如果达到最大限制 额外的客户端将直接被拒绝 # 我们可以调整内核参数来修改 最大等待时长 如果超时 客户还是没有回复第三次握手信息 就直接删除
##粘包问题(TCP)和操作系统缓存理解
TCP流式协议, 指的是数据与数据之间没有明确的分界线,导致不能正确读取数据, 就像水 一杯水和一杯牛奶倒在一起了! #要理解粘包问题,需要先了解TCP协议传输数据时的具体流程 “”“ 应用程序无法直接操作硬件,应用程序想要发送数据则必须将数据交给操作系统,而操作系统需要需要同时为所有应用程序提供数据传输服务,也就意味着,操作系统不可能立马就能将应用程序的数据发送出去,就需要为应用程序提供
一个缓冲区,用于临时存放数据,,具体流程如下: ##### 发送方: 当应用程序调用send函数时,应用程序会将数据从应用程序拷贝到操作系统缓存,再由操作系统从缓冲区读取数据并发送出去 ##### 接收方: 对方计算机收到数据也是操作系统先收到,至于应用程序何时处理这些数据,操作系统并不清楚,所以同样需要将数据先存储到操作系统的缓冲区中,当应用程序调用recv时,实际上是从操作系统缓冲区中将数据拷贝到应用程序的过程 上述过程对于TCP与UDP都是相同的不同之处在于: ##### UDP: UDP在收发数据时是基于数据包的,即一个包一个包的发送,包与包之间有着明确的分界,到达对方操作系统缓冲区后也是一个一个独立的数据包,接收方从操作系统缓冲区中将数据包拷贝到应用程序 这种方式存在的问题: 1.发送方发送的数据长度每个操作系统会有不同的限制,数据超过限制则无法发送 2.接收方接收数据时如果应用程序的提供的缓存容量小于数据包的长度将造成数据丢失,而缓冲区大小不可能无限大
##### TCP: 当我们需要传输较大的数据,或需要保证数据完整性时,最简单的方式就是使用TCP协议了 与UDP不同的是,TCP增加了一套校验规则来保证数据的完整性,会将超过TCP包最大长度的数据拆分为多个TCP包 并在传输数据时为每一个TCP数据包指定一个顺序号,
接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全都是二进制数据,且每次收到的二进制数据之间没有明显的分界
”“” ######重点来了####### “”“ 粘包 仅发生在TCP协议中 1. 发送端 发送的数据量小 并且间隔短 会粘 2. 接收端 一次性读取了两次数据的内容 会粘 3. 接收端 没有接收完整 剩余的内容 和下次发送的粘在一起 无论是那种情况,其根本原因在于 接收端不知道数据到底有多少 解决方案就是 提前告知接收方 数据的长度 ”“”
##粘包解决方案
“”“ 先发长度给对方 再发真实数据 #发送端 1.使用struct 将真实数据的长度转为固定的字节数据 2.发送长度数据 3.发送真实数据 接收端 1.先收长度数据 字节数固定 2.再收真实数据 真实可能很长 需要循环接收 发送端和接收端必须都处理粘包 才算真正的解决了 案例: 远程CMD程序
“”“
需求:
基于TCP开发一款远程CMD程序
客户端连接服务器后 可以向服务器发送命令
服务器收到命令后执行 无论执行成功还是失败 将执行结果返回给客户端
执行系统指令使用subprocess模块完成
”“” ”“” #案例 远程CMD程序 ————————————服务器.py———————————————————— import socket import subprocess import struct from 二_CMD程序 import smallTool server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(("127.0.0.1",1688)) server.listen() # back while True: # socket,addr一个元组 客户端的ip和port client,addr = server.accept() print("客户端链接成功!") # 循环收发数据 while True: try: cmd = smallTool.recv_data(client) if not cmd: break print(cmd) p = subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # 不要先读err错误信息 它会卡主 原因不详 linux不会有问题 tasklist netstat - ano啥的 data = p.stdout.read() err_data = p.stderr.read() len_size = len(data) + len(err_data) print("服务器返回了: %s " % len_size) len_bytes = struct.pack("q",len_size) # 在发送真实数据前先发送 长度 client.send(len_bytes) # 返回的结果刚好就是二进制 # 发送真实数据 client.send(data + err_data) except ConnectionResetError as e: print("客户端了挂了!",e) break client.close() #server.close() ————————————客户端.py———————————————————— """ 客户端输入指令 服务器接收指令并执行 最后返回执行结果 """ import socket from 二_CMD程序 import smallTool import struct client = socket.socket() try: client.connect(("127.0.0.1",1688)) print("链接成功!") while True: msg = input("请输入要执行指令:").strip() if msg == "q": break if not msg: continue # 发送指令 # 先发长度 len_bytes = struct.pack("q",len(msg.encode("utf-8"))) client.send(len_bytes) # 在发指令 client.send(msg.encode("utf-8")) data = smallTool.recv_data(client) print(data.decode("GBK")) client.close() except ConnectionRefusedError as e: print("链接服务器失败了!",e) except ConnectionResetError as e: print("服务器挂了!", e) client.close() ————————————smalltool.py—————————————————— “”“ 说明:该模块作用于服务器和客户端读长度,读数据内容的公共代码,抽调出来的模块 返回的是:data 为二进制数据 ”“” import struct def recv_data(client): # 接收长度数据 固定位8个字节 len_bytes = client.recv(8) # 转换为整型 len_size = struct.unpack("q", len_bytes)[0] print("服务器返回了%s长度的数据" % len_size) # 再接收真实数据 # 问题 如果数据量太大 则不能 一次收完 必须循环一次收一部分 # 缓冲区大小 buffer_size = 1024 # 已接受大小 recv_size = 0 # 最终的数据 data = b"" while True: # 如果剩余数据长度 大于缓存区大小 则缓冲区有多大就读多大 if len_size - recv_size >= buffer_size: temp = client.recv(buffer_size) else: # 剩余数据长度 小于缓冲区大小 剩多少就收多少 temp = client.recv(len_size - recv_size) recv_size += len(temp) data += temp # 当已接受大小等于数据总大小则跳出循环 if recv_size == len_size: break # print(data.decode("gbk")) # windows执行指令返回的结果默认为GBK return data
##struct模块
# struct 是一个内置模块 作用是 将python中的类型 转为字节数据 并且长度固定 import struct # 将整数 转为固定长度的二进制 data = struct.pack("q",100000000000000000) print(data) print(len(data)) # 将二进制转换回整型 返回一个元组 res = struct.unpack("q",data)[0] print(res)
##引出自定义报头之前文件下载案例
-----------------------------服务器.py------------------------------------------------- """ #理解注释 粘包问题解决了,但是存在一个问题 当文件过大时,不能直接发给send发给操作系统 """ import socket import os import struct """ 发文件大小-----发文件长度-----发数据 """ server =socket.socket() server.bind(("127.0.0.1",1688)) server.listen() while True: client,addr = server.accept() print("客户端连接成功") f=None try: #先发送文件大小 path = r"D:python8期课堂内容day32视频2.半链接数.mp4" file_size=os.path.getsize(path) print(file_size) len_bytes=struct.pack("q",file_size) #发送长度 client.send(len_bytes) #循环发送文件数据,每次发送2048 f=open(path,'rb') while True: temp = f.read(2048) if not temp: break client.send(temp) print("文件发送完毕") except Exception as e: print("出问题了",e) finally: if f: f.close() client.close() -----------------------------客户端.py------------------------------------------------- import socket import struct client=socket.socket() try: client.connect(("127.0.0.1",1688)) print("连接成功") #第一步 先收长度 file_size=struct.unpack("q",client.recv(8))[0] #第二步 再收文件内容 #已收大小 recv_size=0 #缓冲区 buffer_size=2048 f=open("new.mp4",'wb') while True: if file_size - recv_size >= buffer_size: temp=client.recv(buffer_size) else: temp=client.recv(file_size - recv_size) f.write(temp) recv_size += len(temp) print("已接收大小:%s%%"%(recv_size/file_size *100)) if recv_size == file_size: break f.close() except ConnectionRefusedError as e: print("服务器连接失败") client.close()
##自定义报头
“”“ 当需要在传输数据时 传呼一些额外参数时就需要自定义报头 报头本质是一个json 数据 ,例如我们要实现文件上传下载,不光要传输文件数据,还需要传输文件名字,md5值等等,如何能实现呢? 具体过程如下: 发送端 1 发送报头长度 ,定义的报头包含所需的信息 2 发送报头数据 其中包含了文件长度 和其他任意的额外信息 3 发送文件内容 接收端 1.接收报头长度 2.接收报头信息 3.接收文件内容 ”“” #案例:优化上一个下载视频文件案例 ——————————————服务器.py—————————————————— import socket import os import struct import json """ 发文件大小-----发文件长度-----发数据 """ server =socket.socket() server.bind(("127.0.0.1",1688)) server.listen() while True: client,addr = server.accept() print("客户端连接成功") f=None try: path = r"D:python8期课堂内容day32视频2.半链接数.mp4" file_size = os.path.getsize(path) file_info={"file_name":"广东雨神.mp4","file_size":file_size} json_str=json.dumps(file_info).encode("utf-8") #发送报头长度 client.send(struct.pack("q",len(json_str))) #发送报头 client.send(json_str) #发文件 #循环发送文件数据,每次发送2048 f=open(path,'rb') while True: temp = f.read(2048) if not temp: break client.send(temp) print("文件发送完毕") except Exception as e: print("出问题了",e) finally: if f: f.close() client.close() ——————————————客户端.py—————————————————— import socket import struct import json client=socket.socket() try: client.connect(("127.0.0.1",1688)) print("连接成功") #第一步 先收报头长度 head_size=struct.unpack("q",client.recv(8))[0] #第二步 再收报头信息 head_str = client.recv(head_size).decode("utf-8") file_info=json.loads(head_str) print("报头数据:",file_info) file_size=file_info.get("file_size") file_name=file_info.get("file_name") #已收大小 recv_size=0 #缓冲区 buffer_size=2048 f=open(file_name,'wb') while True: if file_size - recv_size >= buffer_size: temp=client.recv(buffer_size) else: temp=client.recv(file_size - recv_size) f.write(temp) recv_size += len(temp) print("已接收大小:%s%%"%(recv_size/file_size *100)) if recv_size == file_size: break f.close() except ConnectionRefusedError as e: print("服务器连接失败") client.close()