1 基于 Tcp 的远程调用命令实现
很多人应该都使用过 Xshell 工具, 这是一个远程连接工具, 通过上面的知识, 就可以模拟出 Xshell 远程连接服务器并调用命令的功能
Tcp 服务端代码如下:
- import socket,subprocess
- ip_port = ("127.0.0.1",8000)
- tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- tcp_server.bind(ip_port)
- tcp_server.listen(5)
- print("the server has started")
- while True:
- try:
- conn,addr = tcp_server.accept()
- cmd = conn.recv(1024)
- if not cmd:break
- res = subprocess.Popen(cmd.decode("utf-8"),shell=True,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE,
- stderr=subprocess.PIPE)
- error = res.stderr.read()
- if error:
- cmd_res = error
- else:
- cmd_res = res.stdout.read()
- if not cmd_res:
- cmd_res = "the reply is".encode("gbk")
- conn.send(cmd_res)
- except Exception:
- break
Tcp 客户端代码如下:
- import socket,subprocess
- ip_port = ("127.0.0.1",8000)
- tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- tcp_client.connect(ip_port)
- while True:
- cmd = input(">>>>").strip()
- if not cmd:continue
- if cmd ==quit:break
- tcp_client.send(cmd.encode("utf-8"))
- cmd_res = tcp_client.recv(1024)
- print("the reply is",cmd_res.decode("gbk"))
- tcp_client.close()
但在用上面两个代码进行模拟 Xshell 远程调用的过程中, 可能会出现一个问题: 粘包
2 粘包
在数据信息传输过程中, 发动数据的速度可能是 3kb/s, 但是接收数据的速度可能是 10kb/s, 甚至更快, 应用程序看到的数据都是一个整体或可以说是一个流 (stream), 一条数据信息有多少字节, 应用程序是不可见的 Tcp 是面向连接, 面向流的协议, 这也就成为 Tcp 容易出现粘包现象的原因基于 Tcp 的套接字客户端像服务端上传文件的时候, 文件的内容是按照一个个小的字节段传输的, 接收方根本不知道什么时候开始, 什么时候结束
而 Udp 将数据信息分成一个个小的数据段, 在提取数据的时候, 不能以字节为单位任意提取数据, 只能以数据段为单位进行提取所以只有 Tcp 才会出现粘包现象, 而 Udp 就不会
粘包问题主要造成原因就是接收方不知道数据消息之间的界限,(即开始点和终止点), 不知道一次所需提取的数据是多少, 所以造成的问题
Tcp 是基于数据流面向连接的协议, 因为收发的消息不能为空, 在服务端和客户端都必须添加空消息的处理机制, 防止程序运行过程中卡住 Udp 是基于数据报无连接的协议, 即使发送的是空消息, 但实质上它会将发送的空消息封装上消息头, 不会收到影响
在如下两种情况下很容易发生粘包现象:
(1) 发送数据时间的间隔太短, 数据量很小, 就会粘合在一种, 产生粘包现象
(2) 客户端发送数据后, 服务端只接受一部分, 再次发送其它数据的时候, 服务端会先从缓冲区提取上一次的遗留数据, 产生粘包
解决粘包问题的根本就在于: 要让接收方知道将要接收数据消息的长度基于上面的 Tcp 远程调用命令的程序, 该如何解决粘包问题?
服务端:
- import socket,subprocess,struct
- ip_port = ("127.0.0.1",8000)
- tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- tcp_server.bind(ip_port)
- tcp_server.listen(5)
- print("the server has started")
- while True:
- while True:
- conn,addr = tcp_server.accept()
- try:
- cmd = conn.recv(1024)
- if not cmd:break
- res = subprocess.Popen(cmd.decode("utf-8"),shell=True,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE,
- stderr=subprocess.PIPE)
- error = res.stderr.read()
- if error:
- cmd_res = error
- else:
- cmd_res = res.stdout.read()
- if not cmd_res:
- cmd_res = "the reply is".encode("gbk")
- length = len(cmd_res)
- send_length = struct.pack("i",length)
- conn.send(send_length,cmd_res)
- except Exception:
- break
客户端:
- import socket,subprocess,struct
- from functools import partial
- ip_port = ("127.0.0.1",8000)
- tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- tcp_client.connect(ip_port)
- while True:
- cmd = input(">>>>").strip()
- if not cmd:continue
- if cmd ==quit:break
- tcp_client.send(cmd.encode("utf-8"))
- recv_length = tcp_client.recv(4)
- length = struct.unpack("i",recv_length)[0]
- recv_msg = "".join(iter(partial(tcp_client.recv,1024),b""))
- print("the reply is",recv_msg.decode("gbk"))
- tcp_client.close()
3 socketserver 模块
在使用上面的程序进行通信时, 是没有达到并发的也就意味着, 当第一个客户端占用服务端的资源的时候, 第二个客户端再向服务端发起请求, 是无法进行通信的通过 sockserver 可以实现该功能
sockserver 是标准库中的一个高级模块, 通过 socketserver, 将可以使用类来编写网络应用程序, 以面向对象的方式的处理方式将更具有组织性和逻辑性
socketserver 包含了 4 个基本的类: 基于 TCP 流式套接字的 TCPServer; 基于 UDP 数据报套接字的 UDPServer, 以及基于文件的基础同步 UnixStreamServer 和 UnixDatagramServer 使用最多的 TCPServer, 后面三个很少用到
在 socketserver 服务器框架中, 基本所有的处理逻辑代码会放在一个请求处理程序中,(即一个类), 服务端每当收到一个请求, 就会实例化一个处理程序, 并且调用相应的方法来响应客户端发来的请求, BaseRequestHandler 类把所有的操作都放在 handle 方法中, 这个方法会访问属性 self.request 中的客户端套接字
服务端做如下改写:
- import socketserver,struct
- class Server2(socketserver.BaseRequestHandler):
- def handle(self):
- while True:
- try:
- data = self.request.recv(1024)
- if not data:break
- self.request.send(data.title())
- except Exception:
- break
- if __name__ == "__main":
- s = socketserver.ThreadingTCPServer(("127.0.0.1",8080),Server2)
- s.serve_forever()
在这个程序中, self.request 就等同于前面创建简单 TCP 服务端的 conn, 通过重写 handle 方法, 该方法在默认情况下为空当接收到一个客户端请求的时候, 他就会调用 handle() 方法, 然后通过 socketserver.ThreadingTCPServer 方法, 传入给定的主机 ip 地址端口号和所定义的类, 最后使用 serve_forever() 方法来启动 TCP 服务器无限循环的等待并服务于客户端请求
来源: https://www.cnblogs.com/Chen-Zhipeng/p/8488901.html