△点击上方“Python猫”关注 ,回复“1”领取电子书
剧照:《眷思量》
作者:从零开始的程序员生活
来源:https://www.cnblogs.com/lgjbky/p/15186188.html
前言
最近工作中需要开发前端操作远程虚拟机的功能,简称 WebShell。基于当前的技术栈为 react+django,调研了一会发现大部分的后端实现都是 django+channels 来实现 websocket 服务。
大致看了下觉得这不够有趣,翻了翻 django 的官方文档发现 django 原生是不支持 websocket 的,但 django3 之后支持了 asgi 协议可以自己实现 websocket 服务。
于是选定 gunicorn+uvicorn+asgi+websocket+django3.2+paramiko 来实现 WebShell。
实现 websocket 服务
使用 django 自带的脚手架生成的项目会自动生成 asgi.py 和 wsgi.py 两个文件,普通应用大部分用的都是 wsgi.py 配合 nginx 部署线上服务。
这次主要使用 asgi.py 实现 websocket 服务的思路大致网上搜一下就能找到,主要就是实现 connect/send/receive/disconnect 这个几个动作的处理方法。
这里 How to Add Websockets to a Django App without Extra Dependencies(https://jaydenwindle.com/writing/django-websockets-zero-dependencies/) 就是一个很好的实例,但过于简单……
思路
# asgi.py
import osfrom django.core.asgi import get_asgi_application
from websocket_app.websocket import websocket_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'websocket_app.settings')django_application = get_asgi_application()async def application(scope, receive, send):if scope['type'] == 'http':await django_application(scope, receive, send)elif scope['type'] == 'websocket':await websocket_application(scope, receive, send)else:raise NotImplementedError(f"Unknown scope type {scope['type']}")# websocket.py
async def websocket_application(scope, receive, send):pass
# websocket.py
async def websocket_application(scope, receive, send):while True:event = await receive()if event['type'] == 'websocket.connect':await send({'type': 'websocket.accept'})if event['type'] == 'websocket.disconnect':breakif event['type'] == 'websocket.receive':if event['text'] == 'ping':await send({'type': 'websocket.send','text': 'pong!'})
实现
上面的代码提供了思路,比较完整的可以参考这里 websockets-in-django-3-1 (https://aliashkevich.com/websockets-in-django-3-1/) 基本可以复用了。
其中最核心的实现部分我放下面:
class WebSocket:def __init__(self, scope, receive, send):self._scope = scopeself._receive = receiveself._send = sendself._client_state = State.CONNECTINGself._app_state = State.CONNECTING@propertydef headers(self):return Headers(self._scope)@propertydef scheme(self):return self._scope["scheme"]@propertydef path(self):return self._scope["path"]@propertydef query_params(self):return QueryParams(self._scope["query_string"].decode())@propertydef query_string(self) -> str:return self._scope["query_string"]@propertydef scope(self):return self._scopeasync def accept(self, subprotocol: str = None):"""Accept connection.:param subprotocol: The subprotocol the server wishes to accept.:type subprotocol: str, optional"""if self._client_state == State.CONNECTING:await self.receive()await self.send({"type": SendEvent.ACCEPT, "subprotocol": subprotocol})async def close(self, code: int = 1000):await self.send({"type": SendEvent.CLOSE, "code": code})async def send(self, message: t.Mapping):if self._app_state == State.DISCONNECTED:raise RuntimeError("WebSocket is disconnected.")if self._app_state == State.CONNECTING:assert message["type"] in {SendEvent.ACCEPT, SendEvent.CLOSE}, ('Could not write event "%s" into socket in connecting state.'% message["type"])if message["type"] == SendEvent.CLOSE:self._app_state = State.DISCONNECTEDelse:self._app_state = State.CONNECTEDelif self._app_state == State.CONNECTED:assert message["type"] in {SendEvent.SEND, SendEvent.CLOSE}, ('Connected socket can send "%s" and "%s" events, not "%s"'% (SendEvent.SEND, SendEvent.CLOSE, message["type"]))if message["type"] == SendEvent.CLOSE:self._app_state = State.DISCONNECTEDawait self._send(message)async def receive(self):if self._client_state == State.DISCONNECTED:raise RuntimeError("WebSocket is disconnected.")message = await self._receive()if self._client_state == State.CONNECTING:assert message["type"] == ReceiveEvent.CONNECT, ('WebSocket is in connecting state but received "%s" event'% message["type"])self._client_state = State.CONNECTEDelif self._client_state == State.CONNECTED:assert message["type"] in {ReceiveEvent.RECEIVE, ReceiveEvent.DISCONNECT}, ('WebSocket is connected but received invalid event "%s".'% message["type"])if message["type"] == ReceiveEvent.DISCONNECT:self._client_state = State.DISCONNECTEDreturn message
缝合怪
做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的 WebSocket 类与 paramiko 结合起来,实现从前端接受字符传递给远程主机,并同时接受返回呢?
import asyncio
import traceback
import paramiko
from webshell.ssh import Base, RemoteSSH
from webshell.connection import WebSocketclass WebShell:"""整理 WebSocket 和 paramiko.Channel,实现两者的数据互通"""def __init__(self, ws_session: WebSocket,ssh_session: paramiko.SSHClient = None,chanel_session: paramiko.Channel = None):self.ws_session = ws_sessionself.ssh_session = ssh_sessionself.chanel_session = chanel_sessiondef init_ssh(self, host=None, port=22, user="admin", passwd="admin@123"):self.ssh_session, self.chanel_session = RemoteSSH(host, port, user, passwd).session()def set_ssh(self, ssh_session, chanel_session):self.ssh_session = ssh_sessionself.chanel_session = chanel_sessionasync def ready(self):await self.ws_session.accept()async def welcome(self):# 展示Linux欢迎相关内容for i in range(2):if self.chanel_session.send_ready():message = self.chanel_session.recv(2048).decode(&#39;utf-8&#39;)if not message:returnawait self.ws_session.send_text(message)async def web_to_ssh(self):# print(&#39;--------web_to_ssh------->&#39;)while True:# print(&#39;--------------->&#39;)if not self.chanel_session.active or not self.ws_session.status:returnawait asyncio.sleep(0.01)shell = await self.ws_session.receive_text()# print(&#39;-------shell-------->&#39;, shell)if self.chanel_session.active and self.chanel_session.send_ready():self.chanel_session.send(bytes(shell, &#39;utf-8&#39;))# print(&#39;--------------->&#39;, "end")async def ssh_to_web(self):# print(&#39;<--------ssh_to_web-----------&#39;)while True:# print(&#39;<-------------------&#39;)if not self.chanel_session.active:await self.ws_session.send_text(&#39;ssh closed&#39;)returnif not self.ws_session.status:returnawait asyncio.sleep(0.01)if self.chanel_session.recv_ready():message = self.chanel_session.recv(2048).decode(&#39;utf-8&#39;)# print(&#39;<---------message----------&#39;, message)if not len(message):continueawait self.ws_session.send_text(message)# print(&#39;<-------------------&#39;, "end")async def run(self):if not self.ssh_session:raise Exception("ssh not init!")await self.ready()await asyncio.gather(self.web_to_ssh(),self.ssh_to_web())def clear(self):try:self.ws_session.close()except Exception:traceback.print_stack()try:self.ssh_session.close()except Exception:traceback.print_stack()
前端
xterm.js 完全满足,搜索下找个看着简单的就行。
export class Term extends React.Component {private terminal!: HTMLDivElement;private fitAddon = new FitAddon();componentDidMount() {const xterm = new Terminal();xterm.loadAddon(this.fitAddon);xterm.loadAddon(new WebLinksAddon());// using wss for https// const socket = new WebSocket("ws://" + window.location.host + "/api/v1/ws");const socket = new WebSocket("ws://localhost:8000/webshell/");// socket.onclose = (event) => {// this.props.onClose();// }socket.onopen = (event) => {xterm.loadAddon(new AttachAddon(socket));this.fitAddon.fit();xterm.focus();}xterm.open(this.terminal);xterm.onResize(({ cols, rows }) => {socket.send("" + cols + "," + rows)});window.addEventListener(&#39;resize&#39;, this.onResize);}componentWillUnmount() {window.removeEventListener(&#39;resize&#39;, this.onResize);}onResize = () => {this.fitAddon.fit();}render() {return this.terminal = ref as HTMLDivElement}>
;}
}
好了,废话不多少了,代码我放这里了webshell (https://github.com/aleimu/webshell) 欢迎 star/fork!
Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~
还不过瘾?试试它们
▲Python 协程与 Javascript 协程的对比
▲把 Redis 当作队列用,真的合适吗?
▲我在 GitHub 上读清华
▲如何快速从 JSON 中找到特定的 Key?
▲Python进阶:用websocket构建实时日志跟踪器
▲吐槽:Python正在从简明转向臃肿,从实用转向媚俗
如果你觉得本文有帮助
请慷慨分享和点赞,感谢啦!