目 录
socket通信循环、socket连接循环、半连接池
subprocess模块、struct模块、黏包问题
一、socket套接字简介
什么是套接字?
Socket又称”套接字”,应用程序通常通过”套接字”向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。
socket的作用是什么?
socket主要用于进程间的通信。
二、socket基础编程
1、socket 基础通信
import socket server = socket.socket() # 买手机 不传参数默认用的就是TCP协议 server.bind(('127.0.0.1',8080)) # bind((host,port)) 插电话卡 绑定ip和端口 server.listen(5) # 开机 半连接池 conn, addr = server.accept() # 接听电话 等着别人给你打电话 阻塞 data = conn.recv(1024) # 听别人说话 接收1024个字节数据 阻塞 print(data) conn.send(b'hello baby~') # 给别人回话
服务端
import socket client = socket.socket() # 拿电话 client.connect(('127.0.0.1',8080)) # 拨号 写的是对方的ip和port client.send(b'hello world!') # 对别人说话 data = client.recv(1024) # 听别人说话 print(data) client.close() # 挂电话客户端
2、socket 通信循环
socket循环通信就是实现客户端与服务端的实时通信,客户端发出一条消息,服务端能够实时返回信息。
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) conn, addr = server.accept() while True: data = conn.recv(1024) print(data) conn.send(data.upper())服务端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: msg = input('>>>:').encode('utf-8') client.send(msg) data = client.recv(1024) print(data)客户端
3、代码的健壮性
""" 代码健壮性是为了解决两个问题? 1 客户端断开连接,服务端报错: ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。 解决方法:异常处理 注意:如果是linu系统客户端异常退出的话,服务端不会报错!只会一直打印 b'': 解决方法:在服务端也判断一下,if len(msg) == 0: break 退出循环! 2 客户端输入为空:会导致出现服务端和客户端程序都没有反应 解决方法: 在客户端判断输入的数据长度是否为零,如果是的话就返回,即if len(msg)== 0: continue
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) conn, addr = server.accept() print(addr) # ('127.0.0.1', 64828) addr 是客户端的地址 while True: try: data = conn.recv(1024) # conn 是双向通道,客户端断开就意味这个conn 双向通道无效了,不能在recv取值 if len(data) == 0:break # 针对linux系统,客户端异常退出问题的解决方法 print(data) conn.send(data.upper()) except ConnectionResetError as e: print(e) break conn.close()服务端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: msg = input('>>>:').strip() if len(msg) == 0: continue msg = msg.encode('utf-8') client.send(msg) data = client.recv(1024) print(data)客户端
4、socket 连接循环
socket连接循环的原因?
3 连接循环的引出 当开出好几个客户端与服务端进行链接的时候只能有一个有结果,其他客户端无结果? 原因是当前服务端的conn只是与一个客户端的双向通道,不能与其他客户端进行通信交互,只有关闭链接的客户端才行,但是又出现服务端异常处理退出 因此为解决这个问题,在服务端进行循环链接,即 while True: conn, addr = server.accept() # 循环连接 while True: data = conn.recv(1024) ... """
服务端要满足两个要求:
(1) 服务端要固定有 IP 和端口,不能是动态变化的,否则客户找不到固定地址访问!
解决方法:server.bind(('127.0.0.1', 8080)) 绑定IP 和 端口
(2)服务端要保证24小时不间断提供服务!
解决方法:连接循环
""" 服务端要满足两个要求 1 因为服务端 固定ip和端口 ? 解决方法:server.bind(('127.0.0.1', 8080)) 绑定IP和端口 2 要24小时不间断提供服务? 解决方法:连接循环 """
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) # 半连接池 while True: conn, addr = server.accept() while True: try: data = conn.recv(1024) print(data) conn.send(data.upper()) except ConnectionResetError as e: print(e) break conn.close()服务端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: msg = input('>>>:').strip() if len(msg) == 0: continue msg = msg.encode('utf-8') client.send(msg) data = client.recv(1024) print(data)客户端
5、半连接池的概念
什么是半连接池?
半连接池其实就是一个容器,系统会自动将半连接放入这个容器中。
半连接池是在服务端,位置如下:
什么是半连接?
三次握手没有完成的称之为半连接数,即服务器没有时间回应你的握手请求。
产生半连接的原因:
1)恶意客户端故意不返回第三次握手信息,服务器就处于time_wait状态
洪水攻击用的就是这种原理
2)服务器没有时间处理你的握手请求
最大半连接数
在socket语法中listen()函数的括号中指定的就是最大半连接数
最大半连接数指的是同一时间接收请求的最大数目,超过的请求会被直接拒绝
三、socket编程推导及存在问题
1、subprocess模块回顾
""" subprocess 模块的作用: 可以帮助我们在程序中执行操作系统的命令,并拿到结果 """ import subprocess cmd = input('>>>:').strip() obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # 注意1:下面命令打印的结果是从系统传出的,window操作系统默认是gbk编码模式 # 注意2:obj.stdout.read() 得到的是二进制数据,所以需要转码 print(obj.stdout.read().decode('gbk')) # 正确命令返回的结果 print(obj.stderr.read().decode('gbk')) # 错误命令返回的结果
2、subprocess模块模仿终端命令
import socket import subprocess server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: conn, addr = server.accept() while True: try: cmd = conn.recv(1024) cmd = cmd.decode('utf-8') if len(cmd) == 0: break # 为linux系统解决客户端异常退出的问题 obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # obj.stdout.read() 出来的结果是二进制形式,所以可以直接拼接stdout+stderr res = obj.stdout.read() + obj.stderr.read() # 查看res的长度 print(len(res)) # 发送数据 conn.send(res) except ConnectionResetError as e: print(e) break conn.close()服务端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: cmd = input(">>>:").strip() if len(cmd) == 0: continue cmd = cmd.encode('utf-8') client.send(cmd) # 接收来自服务端返回的数据,但是返回的数据是操作系统的数据要用gbk模式转码 data = client.recv(1024) print(data.decode('gbk')) ''' 引出问题:如何将对方发过来的数据收干净? '''客户端
3、黏包问题
黏包现象主要发生在TCP连接, 基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来,根本不知道该文件的字节流从何处开始,在何处结束.
两种黏包现象:
1 连续的小包可能会被优化算法给组合到一起进行发送
2 第一次如果发送的数据大小2000B接收端一次性接受大小为1024,这就导致剩下的内容会被下一次recv接收到,导致结果错乱
解决黏包现象的两种方案:
方案一:
由于双方不知道对方发送数据的长度,导致接收的时候,可能接收不全,或者多接收另外一次发送的信息内容,所以在发送真实数据之前,要先发送数据的长度,接收端根据长度来接收后面的真实数据,但是双方有一个交互确认的过程
方案二:struct模块。
第一种黏包现象
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) conn, addr = server.accept() data = conn.recv(5) print(data) data = conn.recv(5) print(data) data = conn.recv(5) print(data) """ 结果: b'helloworldbaby~' b'' b'' 原因: TCP协议内部有优化算法,能够将数据量小的且时间间隔比较短的数据组合在一起发送给对方 通俗解释解决方法: 1 找到一个固定长度的探子,然后从探子中找到数据 2 如何将一个数据打包成固定长度? 引出struct模块 """服务端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) client.send(b'hello') client.send(b'world') client.send(b'baby~')客户端
第二种黏包现象 -- 例:subprocess模块模仿终端命令程序
问题表现形式:当输出 tasklist 命令之后在输出 dir命令时会继续出现tasklist 命令的结果
import socket import subprocess server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: conn, addr = server.accept() while True: try: cmd = conn.recv(1024) cmd = cmd.decode('utf-8') if len(cmd) == 0: break # 为linux系统解决客户端异常退出的问题 obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # obj.stdout.read() 出来的结果是二进制形式,所以可以直接拼接stdout+stderr res = obj.stdout.read() + obj.stderr.read() # 查看res的长度 print(len(res)) # 发送数据 conn.send(res) except ConnectionResetError as e: print(e) break conn.close()服务端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: cmd = input(">>>:").strip() if len(cmd) == 0: continue cmd = cmd.encode('utf-8') client.send(cmd) # 接收来自服务端返回的数据,但是返回的数据是操作系统的数据要用gbk模式转码 data = client.recv(1024) print(data.decode('gbk'))客户端
4、struct模块
import struct res1 = 'wewrwerefdvdfsjlaklkjlkjlkjld' print(len(res1)) ''' struct.pack() 后面跟的参数有两个(模式,字符串的长度) struct.pack()要求第二个参数必须是整数 所以我们第二个参数传入字符串的长度 len(res1) ''' res2 = struct.pack('i',len(res1)) print(len(res2)) # res2 是一个字符串 # res3 = struct.unpack('i',res2)[0] print(res3) # 因为struct.unpack()出来就是一个整数,所以不需要再加len()方法 """ 关于struct模块中pack和unpack方法的总结: struct.pack(参数1:模式,参数2:整型数字即字符串长度) ——> 得到结果是一个字符串 struct.unpack(参数1:模式,参数2:字符串) ——> 得到结果是一个整型(字符串长度) """
5、解决黏包问题(发送的数据过大或过小)----struct模块
要求:会默写完整版的模仿终端程序
import socket import subprocess import struct import json server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn, addr = server.accept() while True: try: cmd = conn.recv(1024) cmd = cmd.decode('utf-8') # 注意1:PIPE是管道的意思,管道的特点是:只能read一次,之后在read就不能读出内容 # 注意2:subprocess获取的数据,拿完一次就没有了,不能重复获取 ----所以方式2 时会报错,无法读到数据 obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) res = obj.stdout.read() + obj.stderr.read() # obj.stdout.read() 得到的是二进制数据 ''' # 1 制作报头 header = struct.pack('i',len(res)) # 2 发送报头 conn.send(header) # 3 发送真实数据----两种方式,都可以读出数据 # 方式1 conn.send(res) # 方式2---- 基于TCP协议通信,是流式协议,当时时间间隔比较短时会打包发送 # conn.send(obj.stdout.read()) # conn.send(obj.stderr.read()) ''' d = {'name':'jason','file_size':len(res),'info':'xxx'} # 将字典转为json格式的字符串,字符串才可以len,得到长度 json_d = json.dumps(d) # 1 制作一个字典的报头 header = struct.pack('i',len(json_d)) # 2 发送字典的报头 conn.send(header) # 3 发送字典 conn.send(json_d.encode('utf-8')) # 4 发送真实数据 conn.send(res) except ConnectionResetError as e: print(e) break conn.close()服务端
import socket import struct import json client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: cmd = input('>>>: ').strip() if len(cmd) == 0:continue cmd = cmd.encode('utf-8') client.send(cmd) ''' # 1 接收报头 header = client.recv(4) # 2 解析报头获取真实数据长度, real_size= struct.unpack('i',header)[0] # 注意:在解析报头的时候一定要加上索引[0] !!!! # 3 循环接收数据长度 ------原因:直接接收服务端传来的数据的话,如果数据量过大会导致内存爆炸 recv_size = 0 real_data = b'' while recv_size < real_size: data = client.recv(1024) recv_size += len(data) # 分段加载数据长度 real_data += data # 分段拼接字符串 print(real_data.decode('gbk')) # 服务端是操作系统传来的数据,所以一定要解码 ''' # 1.先接受字典报头 header_dict = client.recv(4) # 2.解析报头 获取字典的长度 dict_size = struct.unpack('i', header_dict)[0] # 解包的时候一定要加上索引0 # 3.接收字典数据 dict_bytes = client.recv(dict_size) dict_json = json.loads(dict_bytes.decode('utf-8')) # 4.从字典中获取信息 print(dict_json) recv_size = 0 real_data = b'' while recv_size < dict_json.get('file_size'): # real_size = 102400 data = client.recv(1024) real_data += data recv_size += len(data) print(real_data.decode('gbk')) """ 以上代码虽然可以解决数据量过大导致的客户端无法一次性接收的问题, 但是还存在两个问题: 1 real_data = b'' 我们使用字符串拼接的形式来分段接收数据,但是当数据过大的时候依然会导致内存压力 2 在服务端 struct模块中的 ‘i’模式有可能无法接收过大的数据 解决方法: 将服务端要发送的数据装入一个字典中,这样既可以获得数据,还可以添加一些信息,如{'name':xxx, 'file_size':file_size,'info':ooo} """客户端
四、作业 ---- 发送大文件
要求:会默写
import socket import os import json import struct server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn,addr = server.accept() while True: try: header_len = conn.recv(4) # 解析字典报头 header_len = struct.unpack('i',header_len)[0] # 再接收字典数据 header_dic = conn.recv(header_len) real_dic = json.loads(header_dic.decode('utf-8')) # 获取数据长度 total_size = real_dic.get('file_size') # 循环接收并写入文件 recv_size = 0 with open(real_dic.get('file_name'),'wb') as f: while recv_size < total_size: data = conn.recv(1024) f.write(data) recv_size += len(data) print('上传成功') except ConnectionResetError as e: print(e) break conn.close()服务端
import socket import json import os import struct client = socket.socket() client.connect(('127.0.0.1',8080)) while True: # 获取电影列表 循环展示 MOVIE_DIR = r'D:\python脱产10期视频\day25\视频' movie_list = os.listdir(MOVIE_DIR) # print(movie_list) for i,movie in enumerate(movie_list,1): print(i,movie) # 用户选择 choice = input('please choice movie to upload>>>:') # 判断是否是数字 if choice.isdigit(): # 将字符串数字转为int choice = int(choice) - 1 # 判断用户选择在不在列表范围内 if choice in range(0,len(movie_list)): # 获取到用户想上传的文件路径 path = movie_list[choice] # 拼接文件的绝对路径 file_path = os.path.join(MOVIE_DIR,path) # 获取文件大小 file_size = os.path.getsize(file_path) # 定义一个字典 res_d = { 'file_name':'性感荷官在线发牌.mp4', 'file_size':file_size, 'msg':'注意身体,多喝营养快线' } # 序列化字典 json_d = json.dumps(res_d) json_bytes = json_d.encode('utf-8') # 1.先制作字典格式的报头 header = struct.pack('i',len(json_bytes)) # 2.发送字典的报头 client.send(header) # 3.再发字典 client.send(json_bytes) # 4.再发文件数据(打开文件循环发送) with open(file_path,'rb') as f: for line in f: client.send(line) else: print('not in range') else: print('must be a number')客户端
五、总结
""" socket套接字 TCP 1.最简易的版本的客户端与服务端之间通信 2.通信循环 recv() 阻塞 3.连接循环 accept() 阻塞 4.TCP粘包问题 5.struct模块 对数据进行打包处理 固定长度 pack unpack 服务端 1.生成一个字典 2.制作该字典的报头 json序列化 编码 统计长度 3.发送字典的报头 4.发送字典 5.最后发真实数据 客户端 1.先接受固定长度的4个字节字典报头 2.解析获取字典数据的长度 unpack(...)[0] 3.接受字典数据 解码 反序列化 4.接受真实数据 ps:为什么要多加一个字典 1.打包的数据大小有限 2.可以携带更多的信息 """