{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Kivy指南-5-远程桌面app\n", "\n", "> 做一个远程桌面app,用“真正的”应用层协议进行通信\n", "\n", "- toc: true \n", "- badges: true\n", "- comments: true\n", "- categories: [jupyter,Kivy,Android,iOS]\n", "- image: kbpic/5.3remotedesk.png" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "本章做一个远程桌面app,依然和网络相关。我们用“真正的”应用层协议进行通信,解决一个复杂的问题。\n", "\n", "简单介绍一下:首先我们的目的是实现一个经典的桌面应用——远程连接,允许用户通过网络操纵其他电脑。这类应用在技术支持和远程协助中使用广泛。\n", "\n", "其次,介绍两个术语:主机(*host* machine)是远程控制者(运行远程控制服务器),客户机(*client*)是主机控制的系统。远程系统管理基本上就是这样的用户交互过程,主机把客户机当作代理来使用。\n", "\n", "因此,关键的两个步骤就是:\n", "\n", "- 收集客户机用户的输入(如鼠标和键盘动作),并应用到主机\n", "- 从主机向客户机发送任何输出(最常见的是截屏,还有声频、视频等)\n", "\n", "远程桌面就是这两个步骤的不断重复。很多商业软件都会实现这些功能,有一些甚至允许运行视频游戏——通过图形和游戏控制器加速。我们准备实现的一些功能如下:\n", "\n", "- 用户的输入只支持鼠标点击或Tab切换\n", "- 输出只有屏幕截图,因为通过网络抓取声音比较复杂\n", "- 主机只支持Windows平台,任何桌面版本都行。客户端没有限制,因为Kivy app哪儿都可以运行\n", "\n", "最后一条实在遗憾,因为不同的系统截屏和模拟点击行为的API不同,我们只能选择最流行的系统来实现。其他平台的支持以后会实现,原理都一样。\n", "\n", ">中国用户可以忽略这条:如果没Windows,自己找虚拟机安装一个。Mac上也可以用Parallers安装,就是要换点钱。\n", "\n", "本章教学大纲如下:\n", "\n", "- 用Python的Flask微框架写一个HTTP服务器\n", "- 用PIL(Pillow)实现截屏\n", "- 用WinAPI功能模拟Windows点击\n", "- 做一个简单的JavaScript客户端原型,用它来测试\n", "- 做一个基于Kivy的HTTP客户端app连接到远程桌面服务器" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "## 服务器" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "为了方便测试和使用,我们希望用容易实现的应用层协议做服务器。我们选择HTTP,要容易实现和简单测试,需要具体两个特征:\n", "\n", "- 支持很多特性,包括服务器端和客户端。HTTP是最流行的协议,完全符合。\n", "- 与其他协议不同,使用HTTP协议,我们可以用JavaScript轻易写出一个可以在浏览器上运行的客户端。这和本书的主题关系不大,但是依然是一个流行的做法\n", "\n", "建服务器的模块,我们用Flask,Django更流行,不过太大材小用了。要安装Flask,直接用pip安装即可:\n", "\n", "**pip install Flask**\n", "\n", "简单高效,完全开源,[文档](https://github.com/mitsuhiko/flask)详细,推荐学习一下。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Flask服务器" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "服务器通常就是由一系列绑定到不同URL的handler组成的。这些绑定通常叫做路由(*routing*)。Flask可以非常容易的实现这些功能。我们建立一个网页的服务器`server.py`:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from flask import Flask\n", "\n", "app = Flask(__name__)\n", "\n", "\n", "@app.route(\"/\")\n", "def index():\n", " return \"Hello, Flask\"\n", "\n", "\n", "if __name__ == \"__main__\":\n", " app.run(host=\"0.0.0.0\", port=7080, debug=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在Flask里面,路由是以修饰器方式实现的,如` @app.route('/')`,在URL比较少的时候这么做很方便。\n", "\n", "'/'路由是服务器域名,对应一个IP地址。运行`server.py`后,在浏览器打开`http://127.0.0.1:7080 `,看到`Hello, Flask`说明服务器ok了。\n", "\n", "![flaskserver](kbpic/5.1flaskserver.png)\n", "\n", "这里,`app.run()`里面的参数`0.0.0.0`不是一个有效的IP地址,不能通过正常访问。服务器绑定这个IP表示我们的app会监听所有IPV4接口——也就是说,从任何一个可用的IP发出的请求都会得到相应。\n", "\n", "这和默认设置(localhost,127.0.0.1)不同。localhost的IP只允许监听同一个机器传来的请求。因此,这个IP适合调试和测试用。但是,当我们发布产品后,`0.0.0.0`就是面向世界,开张圣听。不过,注意这不会自动绕过路由器;它可以在你的局域网工作,但是在公网工作可能需要其他配置。\n", "\n", "还有,记得设置防火墙策略,因为它需要满足应用层设置的优先级。\n", "\n", ">**端口选择**\n", "\n", ">用哪个端口并不重要,重要的是服务器和客户端要用同一个端口,无论是一个浏览器还是一个Kivy app。\n", "\n", ">还要注意,几乎所有的系统中1024以下的端口一般只能由授权用户使用(root或admin)。而且很多端口已经分配了固定用途,所以建议选择1024以上的端口。\n", "\n", ">默认的HTTP端口是80,默认端口通常不需要指定,`http://www.w3.org`和`http://www.w3.org:80/`是一样的。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "你可能发现Python开发实在太容易了——几行代码就可以运行一个服务器。不过,不是样样都很简单,有些事情还不能立竿见影。\n", "\n", "这是Python的优势:如果你要实现一些不太复杂的功能,试试Python吧,通常都会取得好效果。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 服务器新功能——截屏" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "协议和服务器实现之后,我们来做截屏和模拟点击功能。这里仅实现Windows的实现,Mac和Linux支持可以做练习。\n", "\n", "PIL可以实现,用`PIL.ImageGrab.grab()`就行,我们把截图保存为RGB格式。之后就是把图片接到Flask上,这样就可以通过HTTP传送了。\n", "\n", "PIL已经不再维护了,现在有Pillow实现PIL,直接用pip安装即可,具体参阅[文档](http://pillow.readthedocs.org/)。\n", "\n", "**pip install Pillow**\n", "\n", "实现的代码如下:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from flask import send_file\n", "from PIL import ImageGrab\n", "from StringIO import StringIO\n", "\n", "# # more compatible\n", "# try:\n", "# from StringIO import StringIO # python2\n", "# except ImportError:\n", "# from io import BytesIO as # python3\n", "\n", "\n", "@app.route(\"/desktop.jpeg\")\n", "def desktop():\n", " screen = ImageGrab.grab()\n", " buf = StringIO()\n", " screen.save(buf, \"JPEG\", quality=75)\n", " buf.seek(0)\n", " return send_file(buf, mimetype=\"image/jpeg\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "这里`StringIO`是把内容存在内存,不是磁盘上。使用API处理临时数据的时候这种“虚拟文件”很有用。本例中,我们不想要保存截屏内容,就是一个临时文件。如果连续下载文件到磁盘上效率很低,不如直接放到内存里,用完就释放。\n", "\n", "代码很简单,就是用`PIL.ImageGrab.grab()`截屏叫`screen`,然后用`screen.save()`保存为JPEG格式,这样省流量一些,最后用MIME type形式`'image/jpeg'`发送到Flask,这样就可以直接现在在浏览器上了。\n", "\n", ">为了实现更好的速度,用比分辨率的图片更合适,因为每一个帧都要截图是很费流量的。\n", "\n", ">这里又一次充分证明了一点:验证新概念或市场研究时,快速原型是多么高效。\n", "\n", "`buf.seek(0)`是为了倒回(*rewind*)`StringIO`实例;否则,程序就到了数据的最后,不会给`send_file()`发送任何数据了。\n", "\n", "现在就可以测试效果了,打开`http://127.0.0.1:7080/desktop.jpeg`就会看到当前屏幕的截图了。\n", "\n", "![screenshot](kbpic/5.2screenshot.png)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "这里的路由很有意思**\"desktop.jpeg\"**,URL的最后是文件曾经在服务器工具里面是一种习惯,像**Personal Home\n", "Page (PHP)**,一种适合做简单的网站的简单编程语言。其实这里并没有路径的概念,你实际上只要把文件的名称输入地址栏然后从服务器获得它。\n", "\n", "这么做是很不安全的行为,远程连接者可以看到服务器的配置,比如`'/../../etc/passwd'`输入地址栏就可以看到密码了,之后的各种木马病毒像Trojans木马(后门)就来了。\n", "\n", "Python的网页服务器都经历过这些教训。你也可以这么写,但是强烈不推荐,这样太危险了。另外,Python的模块通常默认也不会使用这些配置。\n", "\n", "今天,从文件系统直接获取文件的事情并不是没有,但主要是用于静态文件。另外,有时我们也会把动态网页(如`/index.html`,`/desktop.jpeg`等)也写成文件名的形式是为了让用户更容易明白这些URL的作用。" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "### 模拟点击行为" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "截屏部分完成之后,我们需要实现的功能就是鼠标点击,用WinAPI可以实现,不过很麻烦,我们用Python的ctypes模块来做。\n", "\n", "首先我们需要从URL或者点击的坐标。我们用`GET`方式来实现:`/click?x=100&y=200`,这种方法容易在浏览器测试,不像`POST`和其他HTTP方式需要其他工具来测试。\n", "\n", "Flask支持这种URL带参数的方式:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from flask import request\n", "\n", "\n", "@app.route(\"/click\")\n", "def click():\n", " try:\n", " x = int(request.args.get(\"x\"))\n", " y = int(request.args.get(\"y\"))\n", " except TypeError:\n", " return \"error: expecting 2 ints, x and y\"" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "建立原型的时候在可能出错的地方加异常处理代码是必要的,因为经常会忘记或发送了不合理的参数,所以我们要做异常处理,所有我们需要检查`GET`请求的参数是否可用。调试的时候如果出现错误就会显示信息,这样就知道问题出在哪里了。\n", "\n", "有了点击的坐标之后,我们就要用WinAPI来调用它们。这里需要两个函数都在`user32.dll`:`SetCursorPos()`是设置鼠标光标的位置,`mouse_event()`模拟鼠标的点击事件,比如按下或松开按键。\n", "\n", ">`user32.dll`这个32和你系统的32位或64位没关系。Win32 API首次出现在 Windows NT,比之后的AMD64 (x86_64)架构早7年多,之所以这Win32是为了和之前的Win16区分。\n", "\n", "`mouse_event()`的第一个参数是事件类型,一个C语言枚举类型(一组整型常量)。我们可以在Python里面定义这些常量,用常量2表示鼠标按下,4表示鼠标松开并不是很直观。可以这样:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import ctypes\n", "\n", "# this is the user32.dll reference\n", "user32 = ctypes.windll.user32\n", "\n", "MOUSEEVENTF_LEFTDOWN = 2\n", "MOUSEEVENTF_LEFTUP = 4" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ ">Win API文档可以到Microsoft Developer Network (MSDN)去查:[`SetCursorPos()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms648394.aspx),[`mouse_event()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms646260.aspx)。\n", "\n", ">限于篇幅不做详细介绍,而且也不会有到很多功能;WinAPI可谓包罗万象,内容十分丰富,感兴趣自行研究。\n", "\n", "模拟点击的代码如下:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "@app.route(\"/click\")\n", "def click():\n", " try:\n", " x = int(request.args.get(\"x\"))\n", " y = int(request.args.get(\"y\"))\n", " except:\n", " return \"error\"\n", "\n", " user32.SetCursorPos(x, y)\n", " user32.mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)\n", " user32.mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)\n", " return \"done\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "代码很直白,就是设置鼠标位置,然后单击一下(按下,松开)。\n", "\n", "现在你可以启动Flask的服务器然后试试点击操作,可以在地址栏输入`http://127.0.0.1:7080/click?x=10&y=10`,如果左上角(10,10)这个位置有图标,图标会被选中。\n", "\n", "当然也可以实现一个双击行为,如果页面刷新的足够快的话(因为打开文件时屏幕变化很大,需要截取很多图片),这可能需要在另外一个设备上运行浏览器,记得修改对应的服务器IP地址。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## JavaScript客户端" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在这一章,我们将尝试一下JavaScript远程桌面客户端,因为我们用的是HTTP协议,JavaScript非常合适。这个简单的客户端可以在浏览器运行,作为Kivy客户端桌面应用的原型。\n", "\n", "如果你不熟悉JavaScript,不用担心,其语法很简单,与Python很相似。我们还要用到jQuery来处理DOM表和AJAX。\n", "\n", ">很多人可能不赞成在产品设计阶段使用jQuery,尤其是对那些性能要求高的应用。但是,要实现网页app的快速原型,jQuery还是很不错的,因为它用法简单,实现高效。\n", "\n", "做网页app,我们得把原来的**Hello, Flask**替换成。\n", "\n", "```html\n", "\n", "\n", "
\n", " \n", "