python 网络编程socket示例(远程ssh)

模拟ssh需要返回ssh命令执行的结果,我们用【客户端连接.recv(字节长度)】来接收服务器传回的命令执行结果,但是这个字节长度我们无从得知,定义的如果太小则结果接收不完全,定义的太大,大的上限无法衡量不说还会浪费资源,这时候我们是否会考虑先把执行结果的长度传回给客户端,再把执行结果传回,这样就能很好的解决这个问题,但是我们要怎么操作呢?

尝试一:服务器发送两次,客户端接收两次

1
2
3
4
5
6
# 服务器端
conn.send(b'执行结果长度')
conn.send(b'执行结果'
# 客户端
conn.recv(接收长度)
conn.recv(接收长度)

从客户端的代码已经可以看出问题了,又引出接收的长度要定义多少了,不光有这个问题执行后我们可以发现,服务端紧挨的两次send发送,将发送结果合成了一个大的发送包,两个发送数据合在一起了,即:产生了黏包。

这时候我们来看下面向流的通信特点

TCPtransport control protocol传输控制协议)是面向连接的,面向流的,提供高可靠性服务。

收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。

这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。

可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

尝试二:通过延迟发送

我们可以引入time包,利用sleep延迟两次发送间隔达到两次发送的效果,这种方法时可行的,但是一定程度上降低了程序的运行效率,比较低效

1
2
3
4
5
6
7
# 服务器端
conn.send(b'执行结果长度')
time.sleep(1)
conn.send(b'执行结果'
# 客户端
conn.recv(接收长度)
conn.recv(接收长度)

方法三:使用struct

struct模块中最主要的三个函数式pack()unpack()

pack(fmt, v1) —— 根据所给的fmt描述的格式将值v1转换为一个字符串。

unpack(fmt, bytes) —— 根据所给的fmt描述的格式将bytes反向解析出来,返回一个元组。

这里我们主要用到以上两个方法,这两个方法的格式化的模式很多种,我们这里只用一个“i”就够了,这个‘i’模式可以讲一个整数转换为一个长度为4的字节;反解即将一个4位字节转换为一个整数。着这样即使发生了黏包,我们依然可以提取前四位解码,来获得接收的长度,也就达到了我们的目的

1
2
3
4
5
6
7
# 服务器端
data_lens = struct.pack('i', 执行结果长度)
conn.send(data_lens)
conn.send(b'执行结果'
# 客户端
data_lens = struct.unpack('i',conn.recv(4))
conn.recv(data_lens)

最后放一个模拟ssh的实例代码给大家,其中用到了struct防止黏包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 服务器端
import socket
import subprocess
import struct
server = socket.socket()
ip_port = ('10.10.10.103', 9999)
server.bind(ip_port)
# 监听连接,最大等待数
server.listen(3)
print('服务器开始运行……')
# 接收客户端连接,保持服务器持续运行
while 1:
# 接收客户端连接句柄、地址
conn, addr = server.accept()
print(addr, '已成功连接----------------------')
# 保持客户端连接交互
while 1:
# 使用异常捕获,作用是当客户端出现异常关闭等情况时,服务器自动与其断开连接
try:
# 接收客户端传来的命令并解码
cmd = conn.recv(1024).decode('utf8')
# 执行客户端传来的命令,并将执行结果保存至res
res = subprocess.Popen(cmd,
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
# 执行后的错误消息
err = res.stderr.read()
# 执行后的结果
out = res.stdout.read()
# 获取执行后的结果长度
out_lens = len(out)
# 获取执行后的错误消息长度
err_lens = len(err)
# 判断命令行是否有误
if err:
# 将错误消息的长度格式化为长度为4的字节
struct_data = struct.pack('i', err_lens)
print('服务器错误长度:', err_lens)
# 发送错误消息长度
conn.send(struct_data)
# 发送错误消息
conn.send(err)
else:
# 将执行结果的长度格式化为长度为4的字节
struct_data = struct.pack('i', out_lens)
print('服务器数据长度:', out_lens)
# 发送执行结果的长度
conn.send(struct_data)
# 发送执行结果
conn.send(out)
except Exception as e:
break
print(addr, '已退出连接----------------------')
conn.close()

server.close()
# 客户端
import socket
import struct
# ip_port = ('10.10.10.97', 3096)
ip_port = ('10.10.10.103', 9999)
client = socket.socket()
# 连接服务器
client.connect(ip_port)
# 与服务器保持持久连接
while 1:
# 接收用户输入的命令
shell_str = input('请输入您要执行的命令行:')
# 如果输入exit则断开与服务器的连接
if shell_str == 'exit':
break
# 发送命令至服务器
client.send(shell_str.encode('utf8'))
# 接收服务器返回的消息
# 接收前4位字节,并将其解码(前四位即服务端传来的消息长度),目的在于防止消息长度和消息内容混淆,即黏包
data_lens = struct.unpack('i', client.recv(4))[0]
# 用来接收服务器传来的消息内容
recive_data = b''
# 用来保存累计接收的数据长度
total_data = 0
# 累计接收的数据长度小于数据总长度意味着数据未接收完,则继续接收
while total_data < data_lens:
# 每次以1024的长度接收服务器传回的消息
item_data = client.recv(1024)
# 拼接每次传回的消息
recive_data +=item_data
# 累加每次传回的消息长度
total_data += len(item_data)

# 打印服务器传来的消息总长度和接收的总长度
print('客户端里接收你的长度为:', data_lens, total_data)
# 打印服务器传来的字节类型的消息,并解码
print(recive_data.decode('utf8'))
client.close()