用一個簡易的 web chat 說說 Python、Golang、Nodejs 的非同步

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

在 web 編程中,經常有業務需要在處理請求時做非同步作業,比如耗時太長的 IO 操作,等非同步執行完成之後再結束請求返回 response 到 client,在這個過程中 client 和 server 一直保持著串連不釋放,也就是當前請求在從 client 的角度看一直處於阻塞狀態,直到請求結束。

之所以稱之為非同步,最重要的特徵就是 server 可以繼續處理其他 request 而不被阻塞

不同語言在處理這種非同步情境的方式是截然不同的,常見的處理策略有:訊息共用(非同步任務隊列)、多線程多進程、event(linux signals,nodejs event loop)、協程 coroutine(返回 Future、Promise 代表程式執行的未來狀態),其中 coroutine 是應用最廣泛的,這也是今天此篇的主題。

什麼是 coroutine?簡單來說就是一段可以在特定時刻自由被 suspend、execute、kill 的 program。程式對 coroutine 的控制就像作業系統對 process 的控制,但是代價要低廉的多。這也是很多語言都支援用 coroutine 的方式進行非同步作業的一個重要原因,其中就包括 Golang、Python、JavaScript(ES6)、Erlang 等。

Talk is cheap, show me your code. 在此我們用一個非常簡單的 web chat demo app 來一起表一表 Golang、Python、Nodejs 中的非同步。

Chat demo app 的簡單描述

Demo 只是為了說明 coroutine 在不同語言是如何應用的,因而情境非常簡單:一個內容輸入框,任意 client 發送的訊息都能在其他 client 顯示。

項目地址

https://github.com/zhyq0826/chat-app-tutorial

Chat demo app 的工作原理

兩個主要的 API:

  1. /a/message/new 用於訊息發送,在此稱之為 message-new
  2. /a/message/updates 用於訊息接受,在此稱之為 message-update

Client 通過 message-update 從 server 端擷取最新的訊息,如果沒有新訊息,當次 request 就被掛起,等待新訊息的發送,當有新訊息來臨,擷取最新訊息之後,斷開 connection,一定間隔之後重新請求 server 繼續擷取新的訊息,並重複之前的過程。

由於 message-update 從 server 擷取訊息的時候有可能需要較長時間的等待,server 會一直持有 client 的串連不釋放,因而要求來自 message-update client 的請求不能阻塞 server 處理其他請求,並且 message-update 在沒有訊息到達時需要一直掛起。

Server 處理 message-update 的過程就是一個非同步過程

Python 的實現

Python 中用 yield 來實現 coroutine,但是想要在 web 中實現 coroutine 是需要特別處理,在此我們用了 tornado 這個支援 asynchronous network 的 web framework 來實現 message-update 的處理。

Tornado 中一個 Future 代表的是未來的一個結果,在一次非同步請求過程中,yield 會解析 Future,如果 Future 未完成,請求就會繼續等待。

12345678910
@gen.coroutine   #1def post(self):    cursor = self.get_argument("cursor", None)    # Save the future returned by wait_for_messages so we can cancel    # it in wait_for_messages    self.future = GLOBAL_MESSAGE_BUFFER.wait_for_messages(cursor=cursor)    messages = yield self.future #2    if self.request.connection.stream.closed():        return    self.write(dict(messages=messages))

#1 出通過 tornado 特有的 gen.coroutine 讓當前請求支援 coroutine,#2 是當前請求等待的未來的執行結果,每個 message-update client 都通過 GLOBAL_MESSAGE_BUFFER.wait_for_messages 的調用產生一個 future,然後加入訊息等待的列表,只要 future 未解析完成,請求會一直掛起,tornado 就是通過 yield 和 future 的配合來完成一次非同步請求的。

理解 yield 是如何等待 future 完成的過程其實就是理解 Python generator 如何解析的過程,細節我們有機會再表。

Golang 的實現

Golang 天生就在語言層面支援了 coroutine go func() 就可以開啟 coroutine 的執行,是不是很簡單,是不是很刺激,比起 tornado 必須特別處理要賞心悅目的多,而且 go 內建的 net/http 包實現的 http 請求又天生支援 coroutine,完全不需要類似 tornado 這種第三方 library 來支援了(此處為 Python 2 )。Golang 比 Python 更牛逼的地方在於支援 coroutine 之間使用 channel 進行通訊,是不是更刺激。

1234567
func MessageUpdatesHandler(w http.ResponseWriter, r *http.Request) {client := Client{id: uuid.NewV4().String(), c: make(chan []byte)} #1messageBuffer.NewWaiter(&client) #2msg := <-client.c //掛起請求等待訊息來臨 #3w.Header().Set("Content-Type", "application/json")w.Write(msg)}

#1 為每個 client 產生 一個唯一的身份和 channel,然後 client 加入訊息 #2 等待列表等候訊息的來臨,#3 就是掛起請求的關鍵點:等待 channel 的訊息。Channel 的通訊預設就是阻塞的,即當前 message-update 這個 coroutine 會被 #3 的等待而掛起不會執行,也就達到了 client 串連不能斷的要求。

Nodejs 的實現

Nodejs 天生非同步,通過 callback 來完成非同步通知的接收和執行。為了示範方便我們用了 express ,在 express 中如果一個請求不主動調用 res.endres.sendres.json 請求就不會結束。在 nodejs 中請求如何才能知道訊息到達了,需要 response?Python 我們用了 Future,Golang 用了 channel,Nodejs 實現也絕不僅僅只有一種,在此我們用了事件Promise

Promise 類似 Future,代表的是一次未來的執行,並且在執行完成之後通過 resolve 和 reject 來完成執行結果的通知,then 或 catch 中擷取執行結果。通過 Promise 能有效解決 nodejs 中回調嵌套以及非同步執行錯誤無法外拋的問題。

Promise 作為一種規範,nodejs 中有多種第三方庫都做了實現,在此我們用了 bluebird 這個 library。

事件是 nodejs 中常用的編程模型,熟悉 JavaScript 的同學應該很瞭解了,不細表。

12345678910111213
app.post('/a/message/updates', function(req, res){    var p = new Promise(function(resolve, reject){        messageBuffer.messageEmitter.on("newMessage", function(data){  #1            resolve(data); #2        });    });    var client = makeClient(uuidv4(), p);    messageBuffer.newWaiter(client);    p.then(function(data){  #3        res.set('Content-Type', 'application/json');        res.json({'messages': data});    });});

每個 message-update client 都會產生一個 Promise,並且 Promise 在訊息來臨事件 newMessage #1 觸發以後執行 Promise 的 resolve #2 來告知當前 client 訊息來臨。

小結

三種語言實現非同步策略是不盡相同的,其中 Golang 的最容易理解也最容易實現,這完全得意於 go 天生對 coroutine 的支援以及強大的 channel 通訊。文中 Python 的實現是基於 Python 2,在 Python 3 中 coroutine 的使用有了很大的改善,但是相比 Golang 還是美中不足。Nodejs 作為天生的非同步後端 JavaScript,想要完全使用 Promise 來發揮其優勢還是需要很多技巧來讓整個調用棧都完美支援,不過 ES6 中的 yield,ES7 中的 await/async 對非同步操作都有了很大改善,他們的處理方式神似 Python(據說 ES6 草案的實現就是一群 Python 程式員)。

Python 的例子改自 tornado https://github.com/tornadoweb/tornado/tree/master/demos/chat

chat-app 地址 https://github.com/zhyq0826/chat-app-tutorial

Promise Bluebird http://bluebirdjs.com/docs/getting-started.html

tornado https://tornado.readthedocs.io/en/stable/guide/intro.html

Golang channel https://tour.golang.org/concurrency/2

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.