Python並發學習筆記:從協程到GEVENT(二)
想像一下,一個Web聊天室,上千人登錄網頁後開始在上面進行交流。消息提交是很簡單的,就像表格那樣實現就可以。但是頁面刷新是一個需要著重思考的地方。該怎麼實現頁面的實時刷新呢?
由用戶反覆F5刷新肯定是個不好玩的方式,而C/S模型的特點就是所有的請求都由客戶端發起,由服務端響應,由伺服器主動推送也無法實現。這種情況下似乎有兩種解決方法比較靠譜,一種就是長連接,一種就是長輪詢。
長連接是在TCP層實現一個長時間保活的連接,這樣伺服器可以隨時把消息推送給客戶端。一般的HTTP交互過程中,客戶端發起連接,伺服器接收連接,兩邊一來一往,一個request結束的時候連接就斷開了。這種設計是很有必要的,本身大部分HTTP服務沒有對實時的需要,瀏覽器打開一個網頁後還老保持和伺服器的連接幹嘛呢?客戶端按需請求,伺服器按需回復,連接沒有長時間保持的價值。何況連接是有代價的,尤其是伺服器端,能維護的連接是有限的,保持了一大堆連接,大部分情況卻沒有消息交互,這樣浪費的事情誰也不想做。這就是長連接的最大弊端,資源佔用高。
和長連接相比,長輪詢的資源代價會小一點。客戶端發起請求,伺服器沒有準備好數據的時候會先保持這個連接不斷開,等到數據準備好的時候發送給客戶端,客戶端斷開連接,處理數據。數據處理結束後再次發送請求。周而復始。這種情況下,當客戶端拿到當前數據後連接就會中斷,這樣假如連接數已經飽和了,那其他的客戶端就有機會拿到數據。
本文中實現的聊天室就是一個使用長輪詢(Long Polling)實現實時消息交互的Web服務。
多人在線的場景需要考慮到並發的實現。長輪詢中會盡量減少查詢的次數,盡量做到客戶端不需要頻發發送查詢請求。這樣就意味著連接需要阻塞在數據獲取的部分。那麼一個單進程單線程的Web伺服器就不夠用了。想像一下,一個用戶發送了請求後其他用戶發現伺服器不響應了,這得多讓人崩潰。這裡可以使用gevent提供的pwsgi伺服器。
另一方面,客戶端需要反覆發起查詢請求,同時還希望能夠在不刷新網頁的情況下實現頁面數據的更新,那一個可行的方案就是使用JavaScript來保證這點。由腳本不斷的發起請求,得到數據就刷新頁面,然後繼續發起請求。
思路還是很簡單的,下面結合代碼捋一遍。
# coding = utf-8nnfrom gevent.pywsgi import WSGIServernfrom gevent.queue import Queue, Emptynfrom flask import Flask, render_template, redirect, url_for, requestnfrom forms import LoginFormnfrom flask_bootstrap import Bootstrapnimport simplejsonnnapp = Flask(__name__)nbootstrap = Bootstrap(app)napp.config[SECRET_KEY] = HARDnmessages = []nuser_dict = {}nnn@app.route(/put_post/<user>, methods=["POST"])ndef put_post(user):n post = request.form[message]n for u in user_dict:n user_dict[u].put_nowait(post)n return ""nnn@app.route(/get_post/<user>, methods=["GET", "POST"])ndef get_post(user):n q = user_dict[user]n try:n p = q.get(timeout=10)n except Empty:n p = n return simplejson.dumps(p)nnn@app.route(/chatroom/<user>/)ndef chatroom(user):n user_con = user_dict.setdefault(user, Queue())n return render_template("chatroom.html", user=user)nnn@app.route("/login", methods=["GET", "POST"])ndef login():n form = LoginForm()n if form.validate_on_submit():n return redirect(url_for("chatroom", user=form.name.data))n return render_template("login.html", form=form)nnnif __name__ == __main__:n http_server = WSGIServer((127.0.0.1, 5000), app)n http_server.serve_forever()n
後端程序還是很簡明的。我按照網頁執行的順序依次說明一下。後端啟動一個由gevent提供的一個並發http伺服器。用戶首先訪問的是login,在這裡會看到一個表格,用來給自己取一個名字並傳給後端伺服器。伺服器取得用戶名後把頁面重定向到chatroom方法,在這會把用戶名和一個隊列關聯在一起作為全局變數user_dict這個字典的一個鍵值對,然後返回一個聊天室界面HTML。put_post和get_post分別是聊天信息寫入和讀入的處理方法。用戶提交表單後,腳本重寫了表單的提交方法,頁面不會刷新,消息傳入put_post中處理,put_post把消息寫入每一個用戶的隊列中然後返回。腳本發送的查詢請求由get_post處理,每一個用戶都會不斷的發送這個請求,請求處理執行到p = q.get(timeout=10)的時候被阻塞,因為消息隊列中不一定有數據。當數據到達或者超時之後,方法返回,連接中斷。
網頁代碼如下:
<!--login.html-->nn{% extends "bootstrap/base.html" %}n{% import "bootstrap/wtf.html" as wtf %}nn{% block content %}n {{ wtf.quick_form(form) }}n{% endblock %}n
<!--chatroom.html-->n<!DOCTYPE html>n<html lang="en">n<head>n <meta charset="UTF-8">n <title>Chatroom</title>n <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script>n <script type="text/javascript">n $(document).ready(function(){n $(#form).submit(function(e) {n $("#test").hide()n var message = $("#post").val();n $.ajax({n type : POST,n url : /put_post/{{user}},n data : { message: message },n dataType : json,n });n e.preventDefault();n });n var longPoll = function(){n return $.ajax({n type: get,n url: /get_post/{{ user }},n async: true,n timeout: 20000,n success: function(data){n if(data.length > 0){n $("#message").append($("<li>" + data + "</li>"))n }n return longPoll()n },n dataType : jsonn });n };n longPoll();n });n </script>n</head>n<body>n <p id="test">there</p><br/>n <form id="form">n <input type="text" id="post" />n <input type="submit" value="Submit" />n </form>n <ul id="message">n {% for message in messages %}n <li>{{ message }}</li>n {% endfor %}n </ul>n</body>n</html>n
這是個比較簡單的應用,但是可以說明一下long polling的實現思路和大體結構。總的來說,有gevent的參與後並發實現思路會很清晰,這個模塊的功能強大,下面有時間的話要好好讀讀源碼。
附上代碼地址
https://github.com/Weilor/flask-gevent-longpolling
推薦閱讀:
※gunicorn和uwsgi是怎麼在使用gevent的,gunicorn/uwsgi和gevent?
※gevent、eventlet、Twisted、Tornado各有什麼區別和優劣?