套接字(socket) + 模拟ssh (subprocess) + 黏包 (struct) + hashlib 模块
套接字 (socket)
1. socket 层
2. TCP协议 和 UDP协议
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
3. 基于TCP协议的 socket
tcp是基于链接的,必须先启动服务端,然后再启动客户端取链接服务端.
socket使用前,需要导入. import socket
示例 :
登录验证, 在客户端进行登录, 在服务端进行验证, 返回登录结果
# 在服务端存储的用户名和密码,目前暂时存储在文件 account.txt 中
chen:123
zhang:123
liu:456
du:456
# sever 端
import socket
sock=socket.socket() # TCP协议 # 创建socket对象
IP_PORT=("192.168.13.22",8899) # 地址和端口用括号
sock.bind(IP_PORT) # 绑定
sock.listen(5) # 设置等待数为5
while 1:
conn,addr=sock.accept() # 等待连接,返回 conn全收发管道(套接字对象) 和 addr客户端地址信息
连接对象conn和客户端地址信息addr
while 1:
try: # 需要进行异常处理 避免客户端主动或意外关闭程序
data=conn.recv(1024).decode("utf8") # 接受数据,收到的是字节,要解码
user,pwd=data.strip().split("|")
flag=False
with open("account","r") as f: # 文件操作
for line in f:
username,password=line.strip().split(":")
if username==user and password==pwd:
flag=True
break
if flag:
conn.send(b"success") # 发送的必须是字符串且为字节型
else:
conn.send(b"fail")
except Exception as e: # 捕获异常,当客户端主动退出程序时
break # 捕获异常后,服务端也退出循环,否则会报错
# client 端
import socket
sock=socket.socket() # TCP
sock.connect(("192.168.13.22",8899))
while 1:
user=input("用户名>>>>")
pwd=input("密码>>>>")
val=("%s|%s"%(user,pwd)).encode("utf8")
sock.send(val) # 发送用户名和密码
response=sock.recv(1024) # 接收返回的值 为字节型的
print(response.decode("utf8"))
if response.decode("utf8")=="success":
print('登录成功!')
break
else:
print("用户名或者密码错误!")
continue
4. 基于UDP协议的 socket
udp是无连接的,启动服务之后可以直接接受消息,不需要提前建立连接
简单示例 :
# sever 端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr) # 对话(接收与发送)
udp_sk.close() # 关闭服务器套接字
# client 端
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
总结 : (基于tcp)
1. socket 对象使用时,服务端和客户端要注意 收发配合好,两边不能同时收,或者同时发.
2. socket 中 send() 发送的内容不能为空, 有时候需要用 if来判断.
3. 客户端和服务端要保证双方退出,强迫关闭一个现有的连接,如果客户端主动退出程序(红按钮),或者用break退出循环程序,服务端还处在接收的状态,需要用try,处理异常,捕获到异常后,要break退出循环,不再接收. 注意,try处理异常是针对windows系统的. linux系统和其他系统不需要用try处理异常.
模拟ssh (subprocess)
ssh 是指在客户端输入命令去操作服务端.
知识补充 : subprocess 模块
subprocess是Python 2.4中新增的一个模块,它允许你生成新的进程,连接到它们的 input/output/error 管道,并获取它们的返回(状态)码。这个模块的目的在于替换几个旧的模块和方法,如:
- os.system
- os.spawn*
♥♥♥ 示例 : 使用 subprocess 模块可以运行 cmd(终端) 命令. 类似于os.system()
# 远程执行命名程序
import subprocess
res=subprocess.Popen("dir",
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
con = res.stderr.read() # 如果命令错误可以获取错误结果
ret = res.stdout.read() # 获取的是字节型的,dir命令后要展示的内容,
print(ret)
# print(ret.decode('utf-8')) # 报错
print(ret.decode('gbk')) # 由于window系统终端中用的是gbk编码的,因此需要用gbk解码

'''
ret结果:
b' \xc7\xfd\xb6\xaf\xc6\xf7 E \xd6\xd0\xb5\xc4\xbe\xed\xca\xc7 DATA\r\n \xbe\xed\xb5
\xc4\xd0\xf2\xc1\xd0\xba\xc5\xca\xc7 26D4-A412\r\n\r\n E:\\E \xd7\xca\xc
ret编码后结果:
驱动器 E 中的卷是 DATA
卷的序列号是 26D4-A412
E:\E 资料\PythonStudy\workplace\code\ssh 的目录
2018/09/04 17:26 <DIR> .
2018/09/04 17:26 <DIR> ..
2018/09/04 12:29 444 hashlib模块.py
2018/09/04 12:49 810 ssh_client.py
2018/09/04 17:07 1,530 ssh_server.py
2018/09/04 12:03 122 struct模块.py
2018/09/04 17:26 547 subprocess模块.py
2018/09/04 09:59 19 __init__.py
2018/09/04 11:42 <DIR> __pycache__
6 个文件 3,472 字节
3 个目录 1,951,513,604,096 可用字节
'''
总结:
1. dir 在终端是查看文件夹中的内容列表
2. windows系统中,终端使用的是gbk编码,因此解码的时候需要用 .decode('gbk')
linux系统终端用的是 utf-8
3. 关于上述 res.stderr.read( ) res.stdout.read( ) 只能从管道中读取一次,读取两次的话,第二次是空值.
黏包 (struct)
1. 什么是黏包?
黏包就是socket对象, .send() 发送的时候, 如果有连续的两次及以上的发送, 由于send()没有阻塞, 发送的数据在对方那里会在一起存储,无法分开,就是黏包现象.
或者说是,同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。
2. 黏包形成的原因
连续发送, 发送间隔时间短, 数据量小
3. 解决黏包的方法(3)中
a. time.sleep() ,两个send()之间使用,一般不要用,效率太低,太low
b. 利用阻塞,服务端连续发送之间 添加recv, 客户端 send, 避免黏包
c. 利用 struct 模块
- 将数据的长度压缩成固定的字节,发送给服务端
- 服务端接收后再解压获取长度
- 通过循环, 再recv接收数据
知识补充 : struct 模块
struct 模块可用于处理网络通信中的黏包问题.
struct.pack( ) 压缩 , 压缩后是字节型的.
struct.unpack( ) 解压, 解压后是元组, 通过索引[0] 取得原值即可.
♥ 数字在某个范围之内压缩后的字节长度都是4
import struct
res=struct.pack("i",1234543) # pack 是压缩, i模式是压缩数字,
# 返回的是字节型的,且无论数字是多少(范围之内)为4个字节
print(res) # b'o\xd6\x12\x00'
print(len(res)) # 4
obj=struct.unpack("i",res) # unpack 是解压, i模式解压的是数字, 返回的是元组
print(obj) # (1234543,)
print(obj[0]) # 1234543 , 索引[0] 取值,即可得到原来的值
使用 struct 模块 解决黏包示例 :
在客户端中通过输入命令(可以使终端命令,也可以是自定义命令),远程控制服务端, 在数据发送中会出现黏包的问题.
# sever 端 import socket import subprocess server = socket.socket() server.bind(('192.168.13.22',8008)) server.listen(5) while True: print("server is working.....") conn,addr = server.accept() # 字节类型 while True: # 针对window系统 异常处理 try: cmd = conn.recv(1024).decode("utf8") # 阻塞 if cmd == b'exit': break res=subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) out=res.stdout.read() # 读取的是字节型的 err=res.stderr.read() if err: # send发送,需要判断是否为空 import struct header_pack = struct.pack("i", len(err)) conn.send(header_pack) conn.send(err) else: #构建报头 import struct header_pack = struct.pack("i",len(out)) # 将获取的out的长度压缩 conn.send(header_pack) # 发送压缩后的 数据的长度 值 conn.send(out) # 发送数据 except Exception as e: break conn.close()
# client 端 import socket import struct sk = socket.socket() sk.connect(('192.168.13.22',8008)) while 1: cmd = input("请输入命令:") sk.send(cmd.encode('utf-8')) # 字节 if cmd=="": continue if cmd == 'exit': break header_pack=sk.recv(4) data_length=struct.unpack("i",header_pack)[0] # unpack解压后为元组,通过[0],取值 ''' 黏包后,长度和数据在一起,关键是获取 数据长度的字节数,压缩后去字节4即可. b'xxx/xxx/xxx/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ''' # 取数据的值,每次只能取1024,通过循环取数据,当取后的数据长度<总长度 就要recv()取值 recv_data_length=0 recv_data=b"" while recv_data_length < data_length: data=sk.recv(1024) recv_data_length+=len(data) recv_data+=data print(recv_data.decode("gbk")) # windows终端中用gbk编码,需要用gbk解码. sk.close()
hashlib 模块
1. 为字符串添加摘要
对字符串加密,这是不可逆的算法,并且 update() 属于追加摘要.
import hashlib md5 = hashlib.md5() md5.update(b'hellochen') print(md5.hexdigest()) # 获取密文 # 6e92e6e4207d0f85e6f73e7f7eb10f8a md5 = hashlib.md5() md5.update(b'hello') md5.update(b'chen') # update()是追加, print(md5.hexdigest()) # 获取密文 结果与上面密文一样 # 6e92e6e4207d0f85e6f73e7f7eb10f8a
2. 对比文件中内容是否一致
同样的字符串,用 hashlib 中MD5或she...加密之后,得到的密文是一样的(md5是32位).通过对比密文来判断文件中的内容是否一致.
update() 中内容为什么会是追加的,用处是什么?
文件中内容特别大,不会用 f.read() 将所有的内容读取到内存中占用大量的内存,当一行行读取的时候, 可以将一行行的数据追加到update() 中,之后再获取密文就可以了.
md5=hashlib.md5() with open("ssh_client.py","rb") as f: for line in f: md5.update(line) # update中是字节,所以用rb模式,读取字节 print(md5.hexdigest()) # f.read()