热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Django3使用WebSocket实现WebShell

△点击上方“Python猫”关注,回复“1”领取电子书剧照:《眷思量》作者:从零开始的程序员生活来源:https:www.c

△点击上方“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正在从简明转向臃肿,从实用转向媚俗

如果你觉得本文有帮助

请慷慨分享点赞,感谢啦



推荐阅读
  • 本文探讨了利用Java实现WebSocket实时消息推送技术的方法。与传统的轮询、长连接或短连接等方案相比,WebSocket提供了一种更为高效和低延迟的双向通信机制。通过建立持久连接,服务器能够主动向客户端推送数据,从而实现真正的实时消息传递。此外,本文还介绍了WebSocket在实际应用中的优势和应用场景,并提供了详细的实现步骤和技术细节。 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 在使用 Nginx 作为服务器时,发现 Chrome 能正确从缓存中读取 CSS 和 JS 文件,而 Firefox 却无法有效利用缓存,导致加载速度显著变慢。 ... [详细]
  • spring boot使用jetty无法启动 ... [详细]
  • 从CodeIgniter中提取图像处理组件
    本指南旨在帮助开发者在未使用CodeIgniter框架的情况下,如何独立使用其强大的图像处理功能,包括图像尺寸调整、创建缩略图、裁剪、旋转及添加水印等。 ... [详细]
  • 本文探讨了在Windows系统中运行Apache服务器时频繁出现崩溃的问题,并提供了多种可能的解决方案和建议。错误日志显示多个子进程因达到最大请求限制而退出。 ... [详细]
  • 本文深入解析了Django框架中的MVT(Model-View-Template)设计模式,详细阐述了其工作原理和应用流程。通过分析URL模式、视图、模型和模板等关键组件,读者将全面理解Django应用程序的架构体系,掌握如何高效地构建和管理Web应用。 ... [详细]
  • 本文详细解析了JSONP(JSON with Padding)的跨域机制及其工作原理。JSONP是一种通过动态创建``标签来实现跨域请求的技术,其核心在于利用了浏览器对``标签的宽松同源策略。文章不仅介绍了JSONP的产生背景,还深入探讨了其具体实现过程,包括如何构造请求、服务器端如何响应以及客户端如何处理返回的数据。此外,还分析了JSONP的优势和局限性,帮助读者全面理解这一技术在现代Web开发中的应用。 ... [详细]
  • 基于Node.js的高性能实时消息推送系统通过集成Socket.IO和Express框架,实现了高效的高并发消息转发功能。该系统能够支持大量用户同时在线,并确保消息的实时性和可靠性,适用于需要即时通信的应用场景。 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • 在尝试加载支持推送通知的iOS应用程序的Ad Hoc构建时,遇到了‘no valid aps-environment entitlement found for application’的错误提示。本文将探讨此错误的原因及多种可能的解决方案。 ... [详细]
  • 探索百度WebFE团队打造的强大HTML5上传插件Web Uploader
    本文将详细介绍由百度WebFE团队开发的Web Uploader,这是一款集成了HTML5与Flash技术的上传组件,以其卓越的用户体验和强大的功能著称。 ... [详细]
  • 在现代前端开发中,组件化已成为不可或缺的技术,尤其在 React 和 Vue 生态中。然而,组件的管理和测试一直是开发者面临的挑战。本文将介绍如何使用 Storybook 来简化这一过程,提高开发效率。 ... [详细]
  • 深入解析Django CBV模型的源码运行机制
    本文详细探讨了Django CBV(Class-Based Views)模型的源码运行流程,通过具体的示例代码和详细的解释,帮助读者更好地理解和应用这一强大的功能。 ... [详细]
  • HTML5 WebSocket技术详解与应用前景
    WebSocket 是 HTML5 引入的一种在单一 TCP 连接上实现全双工通信的技术。作为一种高效且低延迟的协议,WebSocket 允许服务器和客户端之间进行实时双向数据交换。该技术不仅简化了开发过程,还显著提升了 Web 应用的性能和用户体验。未来,随着更多应用场景的探索,WebSocket 在实时通信领域的应用前景将更加广阔。 ... [详细]
author-avatar
Amandadahl
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有