4.21
今日内容
- 基于tcp实现远程执行命令
- tcp的粘包问题:自定义协议
- socketserver实现并发
- 阿里云服务器
tcp远程命令
客户端
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
msg = input('input command:')
if len(msg) == 0:continue
client.send(msg.encode('utf-8')) # windows系统用gbk
cmd_res = client.recv(1024) # 本次接收最大1024字节
print(cmd_res)
服务端
from socket import *
import subprocess
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, clienr_addr = server.accept()
while True:
try:
res = conn.recv(1024)
if len(res) == 0:
break
# 使用subprocess模块,返回正确结果和错误结果
obj = subprocess.Popen(
res.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res = obj.stdout.read()
stderr_res = obj.stderr.read()
conn.send(stdout_res + stderr_res)
except Exception as e:
print(e)
break
conn.close()
服务端做的两件事
- 循环地从半连接池中取出链接,于其建立双向链接,拿到链接对象:accept
- 拿到链接对象,于其进行通信循环
服务端应该满足两个特点
- 一直对外提供服务
- 并发地服务多个链接
粘包问题
- 对于结果比较大的命令,如
ps aux
,客户端一次最多只能接收1024字节 - 那么socket一次会收不完,但是数据已经接收到客户端缓存里。
- 接受完1024字节客户端又接着让用户输入,再进行一次通信。客户端的命令能够正确发送给服务端,也能正确返回数据。
- 因为上一次的数据socket没有收干净,所以client.recv会继续收上一次通信的后1024个字节,显示上一次通信的结果,于是以后所有命令都乱了
粘包问题出现的原因
- tcp是流式协议,数据像水流一样无法区分地连在一起
- 收数据没收干净,有残留,会跟下一次结果混在一起
解决粘包的核心法则就是每次都收干净,没有任何残留
增大recv里的数字没有意义,因为缓存是有限的,超过缓冲区的大小就不能接收了
udp不会粘包:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
不能用udp协议解决粘包问题
解决粘包的思路
客户端
- 先收固定长度的头,解析出数据的描述信息,包括数据的总大小
- 根据解析出的描述信息,接收真实的数据
- recv_size=0,循环接收,每接收一次,recv_size+=接收的长度
- 直到recv_size=total_size,结束循环
header=client.recv(4)
total_size=struct.unpack('i',header)[0]
recv_size = 0
while recv_size < total_size:
recv_data=client.recv(1024)
recv_size+=len(recv_data)
print(recv_data.decode('utf-8'),end='')
else:
print()
服务端
struct模块:把数据转换成固定长度
- 先发头信息,使用struct模块,将数据长度转换成四个bytes字节
- 再发送真实的数据
# 接上面服务端
# ...
total_size = len(stdout) + len(stderr)
header = struct.pack('i',total_size)
conn.send(header)
conn.send(stdout)
conn.send(stderr)
这样就可以改写上面服务端的 conn.send(stdout_res + stderr_res)
改成正确结果和错误结果分开send,客户端接受完正确结果,才会接收错误结果
传输文件
header里不可能只放数据大小,在传输文件加头的时候可以放进其他属性:写一个header_字典,转成json格式,先发送header字典的长度,再发header字典,再发真实数据
服务端
# 服务端
import json,struct
# 制作头
header_dic = {
'file_name':'a.txt',
'total_size':total_size,
'md5':md5
}
json_str = json.dumps(header_dic)
json_str_bytes = json_str.encode('ut-8')
# 得到头的长度
header_size = struct.pack('i',len(json_str_bytes))
# 先把头的长度发去,固定四个字节
conn.send(head_size)
# 发送头
conn.send(json_str_bytes)
# 发送真实的数据
客户端
# 1. 先收四个字节,得到头字典的长度
x = client.recv(4)
header_len = struct.unpack('i',x)[0]
# 2. 接收字典
# 3. loads接开字典,得到数据长度,接收数据
基于socketserver实现并发
服务端要做两件事:
- 循环地从半连接池中取出链接请求并与其建立双向链接,拿到链接对象conn
- 拿到链接对象,与其进行通信循环
import socketserver
class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
print(self.request)
print(self.client_address)
socketserver.ThreadingTCPServer(('127.0.0.1',8888),MyRequestHandle)
s.serve_forever()
# serve forever等同于
# while True: conn,client_addr=server.accept()
# 启动一个线程(conn,client)
部署服务端到阿里云
- 客户端发包要connect的是公网地址
- 远程链接可以用web界面操作
- windows用 Xshell
- 新建py文件,放进去服务端,绑定
0.0.0.0
- wget:下载python3
- 添加安全组,开放端口
作业 单例
单例:让实例化类得到对象的时候,多个对象都指向同一个内存地址。调用多次类,也只产生一个示例
类,元类,装饰器,__new__
,模块导入实现
p1 = People('deimos',21,'male')
p2 = People('deimos',21,'male')
p3 = People('deimos',21,'male')
p1 is p2
# False
打开同一个文件,链接MySQL的时候会使用到
模块方法:模块只会被导入一次,所以只有一个内存空间