本文概述
- 龙卷风和WebSockets
- WebSockets在行动
- 在Nginx后面跑
- 下一步是什么?
在Python世界中, 存在许多流行的Web框架。 Django之类的框架几乎提供了构建Web应用程序所需的所有内容, 而缺少的任何内容都可以通过Django可用的数千个插件之一来弥补。但是, 由于Python或其大多数Web框架的工作方式, 处理长期存在的连接很快会成为噩梦。线程模型和全局解释器锁通常被认为是Python的致命弱点。
但是所有这些已经开始改变。借助Python 3的某些新功能以及适用于Python的框架(例如Tornado), 处理长期存在的连接不再是一个挑战。 Tornado用Python提供了Web服务器功能, 在处理长期连接方面特别有用。
在本文中, 我们将研究如何使用Tornado在Python中构建简单的WebSocket服务器。该演示应用程序将允许我们上传制表符分隔值(TSV)文件, 对其进行解析并将其内容提供给唯一的URL。
龙卷风和WebSockets Tornado是一个异步网络库, 专门处理事件驱动的网络。由于服务器自然可以同时容纳数以万计的打开连接, 因此服务器可以利用此优势并在单个节点中处理大量WebSocket连接。 WebSocket是一种协议, 可通过单个TCP连接提供全双工通信通道。由于它是开放式套接字, 因此该技术使Web连接成为有状态的, 并有助于与服务器之间的实时数据传输。服务器保持客户端的状态, 使基于WebSockets的实时聊天应用程序或Web游戏的实现变得容易。
WebSocket设计为在Web浏览器和服务器中实现, 并且目前在所有主要的Web浏览器中都受支持。连接一次打开, 并且消息可以在连接关闭之前来回传播多次。
安装龙卷风相当简单。它在PyPI中列出, 可以使用pip或easy_install进行安装:
pip install tornado
Tornado带有自己的WebSockets实现。就本文而言, 这几乎是我们所需要的。
WebSockets在行动 使用WebSocket的优点之一是其有状态属性。这改变了我们通常认为客户端-服务器通信的方式。这种情况的一种特殊用例是, 要求服务器执行缓慢的长时间过程并将结果逐渐流回客户端。
在我们的示例应用程序中, 用户将能够通过WebSocket上传文件。在连接的整个生命周期中, 服务器将在内存中保留已解析的文件。根据请求, 服务器可以将文件的一部分发送回前端。此外, 该文件将通过URL提供, 然后可供多个用户查看。如果将另一个文件上传到相同的URL, 则查看该文件的每个人都可以立即看到新文件。
对于前端, 我们将使用AngularJS。该框架和库将使我们能够轻松处理文件上传和分页。但是, 对于与WebSockets相关的所有内容, 我们将使用标准的JavaScript函数。
这个简单的应用程序将分为三个单独的文件:
- parser.py:实现了带有请求处理程序的Tornado服务器
- templates / index.html:前端HTML模板
- static / parser.js:对于我们的前端JavaScript
在前端, 可以通过实例化WebSocket对象来建立WebSocket连接:
new WebSocket(WEBSOCKET_URL);
这是我们在页面加载时必须要做的事情。实例化WebSocket对象后, 必须附加处理程序以处理三个重要事件:
- 打开:建立连接时触发
- 消息:从服务器收到消息时触发
- 关闭:关闭连接时触发
$scope.init = function() {
$scope.ws = new WebSocket('ws://' + location.host + '/parser/ws');
$scope.ws.binaryType = 'arraybuffer';
$scope.ws.onopen = function() {
console.log('Connected.')
};
$scope.ws.onmessage = function(evt) {
$scope.$apply(function () {
message = JSON.parse(evt.data);
$scope.currentPage = parseInt(message['page_no']);
$scope.totalRows = parseInt(message['total_number']);
$scope.rows = message['data'];
});
};
$scope.ws.onclose = function() {
console.log('Connection is closed...');
};
}$scope.init();
由于这些事件处理程序不会自动触发AngularJS的$ scope生命周期, 因此需要将处理程序函数的内容包装在$ apply中。如果你有兴趣, 可以使用AngularJS特定的软件包, 这些软件包使在AngularJS应用程序中集成WebSocket更容易。
值得一提的是, 断开的WebSocket连接不会自动重新建立, 并且需要应用程序在触发close事件处理程序时尝试重新连接。这有点超出本文的范围。
选择要上传的文件
由于我们正在使用AngularJS构建单页应用程序, 因此尝试使用陈旧的方法提交包含文件的表单将不起作用。为了简化操作, 我们将使用Danial Farid的ng-file-upload库。使用它, 我们需要做的就是允许用户上传文件, 是使用特定的AngularJS指令在我们的前端模板中添加一个按钮:
<
button class="btn btn-default" type="file" ngf-select="uploadFile($file, $invalidFiles)"
accept=".tsv" ngf-max-size="10MB">
Select File<
/button>
在许多方面, 该库使我们可以设置可接受的文件扩展名和大小。就像任何< input type =” file” > 元素一样, 单击此按钮将打开标准文件选择器。
上载档案
当你要传输二进制数据时, 可以在数组缓冲区和Blob之间进行选择。如果仅仅是图像数据之类的原始数据, 请选择blob并在服务器中正确处理。数组缓冲区用于固定长度的二进制缓冲区, 并且文本文件(例如TSV)可以字节串的格式传输。此代码段显示了如何以阵列缓冲区格式上载文件。
$scope.uploadFile = function(file, errFiles) {
ws = $scope.ws;
$scope.f = file;
$scope.errFile = errFiles &
&
errFiles[0];
if (file) {
reader = new FileReader();
rawData = http://www.srcmini.com/new ArrayBuffer();
reader.onload = function(evt) {
rawData = evt.target.result;
ws.send(rawData);
}reader.readAsArrayBuffer(file);
}
}
ng-file-upload指令提供了uploadFile函数。在这里, 你可以使用FileReader将文件转换为数组缓冲区, 然后通过WebSocket发送该文件。
请注意, 通过将大型文件读入数组缓冲区来通过WebSocket发送大文件可能不是上载的最佳方法, 因为它会很快占用大量内存, 从而导致不良的体验。
在服务器上接收文件
文章图片
龙卷风使用4位操作码确定消息类型, 并为二进制数据返回str, 为文本返??回unicode。
if opcode == 0x1:
# UTF-8 data
self._message_bytes_in += len(data)
try:
decoded = data.decode("utf-8")
except UnicodeDecodeError:
self._abort()
return self._run_callback(self.handler.on_message, decoded)
elif opcode == 0x2:
# Binary data
self._message_bytes_in += len(data)
self._run_callback(self.handler.on_message, data)
在Tornado Web服务器中, 以str类型接收数组缓冲区。
在此示例中, 我们期望的内容类型是TSV, 因此将文件解析并转换为字典。当然, 在实际应用中, 有更明智的方式来处理任意上载。
def make_message(self, page_no=1):
page_size = 100
return {
"page_no": page_no, "total_number": len(self.rows), "data": self.rows[page_size * (page_no - 1):page_size * page_no]
}def on_message(self, message):
if isinstance(message, str):
self.rows = [csv.reader([line], delimiter="\t").next()
for line in (x.strip() for x in message.splitlines()) if line]
self.write_message(self.make_message())
请求页面
由于我们的目标是在小页面的大块中显示上载的TSV数据, 因此我们需要一种请求特定页面的方法。为简单起见, 我们将简单地使用相同的WebSocket连接将页码发送到我们的服务器。
$scope.pageChanged = function() {
ws = $scope.ws;
ws.send($scope.currentPage);
}
服务器将以unicode形式收到此消息:
def on_message(self, message):
if isinstance(message, unicode):
page_no = int(message)
self.write_message(self.make_message(page_no))
尝试使用来自Tornado WebSocket服务器的命令进行响应将自动以JSON格式对其进行编码。因此, 只发送包含100行内容的字典是完全可以的。
与他人共享访问权限
为了能够与多个用户共享对同一上传的访问权限, 我们需要能够唯一标识上传。每当用户通过WebSocket连接到服务器时, 都会生成一个随机UUID并将其分配给他们的连接。
def open(self, doc_uuid=None):
if doc_uuid is None:
self.uuid = str(uuid.uuid4())
uuid.uuid4()会生成一个随机UUID, 而str()会将UUID转换为标准格式的十六进制数字字符串。
如果另一个具有UUID的用户连接到服务器, 则将文件处理程序的相应实例添加到以UUID为键的字典中, 并在关闭连接时将其删除。
@classmethod
@tornado.gen.coroutine
def add_clients(cls, doc_uuid, client):
with (yield lock.acquire()):
if doc_uuid in cls.clients:
clients_with_uuid = FileHandler.clients[doc_uuid]
clients_with_uuid.append(client)
else:
FileHandler.clients[doc_uuid] = [client]@classmethod
@tornado.gen.coroutine
def remove_clients(cls, doc_uuid, client):
with (yield lock.acquire()):
if doc_uuid in cls.clients:
clients_with_uuid = FileHandler.clients[doc_uuid]
clients_with_uuid.remove(client)if len(clients_with_uuid) == 0:
del cls.clients[doc_uuid]
同时添加或删除客户端时, 客户端字典可能会引发KeyError。由于Tornado是异步网络库, 因此它提供了用于同步的锁定机制。使用协程的简单锁适合处理客户字典的这种情况。
如果有任何用户上传文件或在页面之间移动, 则所有具有相同UUID的用户都将查看同一页面。
@classmethod
def send_messages(cls, doc_uuid):
clients_with_uuid = cls.clients[doc_uuid]
message = cls.make_message(doc_uuid)for client in clients_with_uuid:
try:
client.write_message(message)
except:
logging.error("Error sending message", exc_info=True)
在Nginx后面跑 实现WebSockets非常简单, 但是在生产环境中使用WebSockets时需要考虑一些棘手的事情。 Tornado是一台Web服务器, 因此可以直接获取用户的请求, 但是出于多种原因, 将其部署在Nginx后面可能是一个更好的选择。但是, 要通过Nginx使用WebSocket, 需要花费更多的精力:
http {
upstream parser {
server 127.0.0.1:8080;
}server {
location ^~ /parser/ws {
proxy_pass http://parser;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
【如何使用Tornado创建一个简单的Python WebSocket服务器】这两个proxy_set_header指令使Nginx将必需的标头传递给升级到WebSocket的连接所必需的后端服务器。
下一步是什么? 在本文中, 我们实现了一个简单的Python Web应用程序, 该应用程序使用WebSockets维护服务器与每个客户端之间的持久连接。使用像Tornado这样的现代异步网络框架, 在Python中同时保持数以万计的开放连接是完全可行的。
尽管此演示应用程序的某些实现方面可以用其他方法完成, 但我希望它仍有助于在https://www.srcmini.com/tornado框架中演示WebSockets的用法。演示应用程序的源代码可在GitHub上获得。
相关:Python多线程教程:并发和并行
推荐阅读
- Sass样式指南(有关如何编写更好的CSS代码的Sass教程)
- 使用Java和Spring Security的JWT实现REST安全性
- 使用Google App Engine GQL查找给定半径内的所有位置
- 如果Flask应用程序在Google App Engine中不可用,如何提供默认错误页面
- Appengine api explorer不会向localhost发送查询
- Android只打电话第二次工作
- 迁移到AndroidX后(无法实例化类:androidx.appcompat.widget.ShareActionProvider。)
- 如何使用adb与Memu / Bluestacks / Nox App Player等模拟器
- Android调试器不断断开连接