精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Node.js 打造實時多人游戲框架

開發 后端
在 Node.js 如火如荼發展的今天,我們已經可以用它來做各種各樣的事情。前段時間UP主參加了極客松活動,在這次活動中我們意在做出一款讓“低頭族”能夠更多交流的游戲,核心功能便是 Lan Party 概念的實時多人互動。

在 Node.js 如火如荼發展的今天,我們已經可以用它來做各種各樣的事情。前段時間UP主參加了極客松活動,在這次活動中我們意在做出一款讓“低頭族”能夠更多交流的游戲,核心功能便是 Lan Party 概念的實時多人互動。極客松比賽只有短得可憐的36個小時,要求一切都敏捷迅速。在這樣的前提下初期的準備顯得有些“水到渠成”。跨平臺應用的 solution 我們選擇了 node-webkit,它足夠簡單且符合我們的要求。

按照需求,我們的開發可以按照模塊分開進行。本文具體講述了開發 Spaceroom(我們的實時多人游戲框架)的過程,包括一系列的探索與嘗試,以及對 Node.js、WebKit 平臺本身的一些限制的解決,和解決方案的提出。

Getting started

Spaceroom 一瞥

在最開始,Spaceroom 的設計肯定是需求驅動的。我們希望這個框架可以提供以下的基礎功能:

  • 能夠以 房間(或者說頻道) 為單位,區分一組用戶
  • 能夠接收收集組內用戶發來的指令
  • 在各個客戶端之間對時,能夠按照規定的 interval 精確廣播游戲數據
  • 能夠盡量消除由網絡延遲帶來的影響

當然,在 coding 的后期,我們為 Spaceroom 提供了更多的功能,包括暫停游戲、在各個客戶端之間生成一致的隨機數等(當然根據需求這些都可以在游戲邏輯框架里自己實現,并非一定需要用到 Spaceroom 這個更多在通信層面上工作的框架)。

APIs

Spaceroom 分為前后端兩個部分。服務器端所需要做的工作包括維護房間列表,提供創建房間、加入房間的功能。我們的客戶端 APIs 看起來像這樣:

  • spaceroom.connect(address, callback) – 連接服務器
  • spaceroom.createRoom(callback) – 創建一個房間
  • spaceroom.joinRoom(roomId) – 加入一個房間
  • spaceroom.on(event, callback) – 監聽事件
  • ……

客戶端連接到服務器后,會收到各種各樣的事件。例如一個在一間房間中的用戶,可能收到新玩家加入的事件,或者游戲開始的事件。我們給客戶端賦予了“生命周期”,他在任何時候都會處于以下狀態的一種:

ss_0

你可以通過 spaceroom.state 獲取客戶端的當前狀態。

使用服務器端的框架相對來說簡單很多,如果使用默認的配置文件,那么直接運行服務器端框架就可以了。我們有一個基本的需求:服務器代碼 可以直接運行在客戶端中,而不需要一個單獨的服務器。玩過 PS 或者 PSP 的玩家應該清楚我在說什么。當然,可以跑在專門的服務器里,自然也是極好的。

邏輯代碼的實現這里簡略了。初代的 Spaceroom 完成了一個 Socket 服務器的功能,它維護房間列表,包括房間的狀態,以及每一個房間對應的游戲時通信(指令收集,bucket 廣播等)。具體實現可以參看源碼。

同步算法

那么,要怎么才能使得各個客戶端之間顯示的東西都是實時一致的呢?

這個東西聽起來很有意思。仔細想想,我們需要服務器幫我們傳遞什么東西?自然就會想到是什么可能造成各個客戶端之間邏輯的不一致:用戶指令。既然處理游戲邏輯的代碼都是相同的,那么給定同樣的條件,代碼的運行結果也是相同的。唯一不同的就是在游戲過程當中,接收到的各種玩家指令。理所當然的,我們需要一種方式來同步這些指令。如果所有的客戶端都能拿到同樣的指令,那么所有的客戶端從理論上講就能有一樣的運行結果了。

網絡游戲的同步算法千奇百怪,適用的場景也各不相同。Spaceroom 采用的同步算法類似于幀鎖定的概念。我們把時間軸分成了一個一個的區間,每一個區間稱為一個 bucket。Bucket 是用來裝載指令的,由服務器端維護。在每一個 bucket 時間段的末尾,服務器把 bucket 廣播給所有客戶端,客戶端拿到 bucket 之后從中取出指令,驗證之后執行。

為了降低網絡延遲造成的影響,服務器接到的來自客戶端的指令每一個都會按照一定的算法投遞到對應的 bucket 中,具體按照以下步驟:

  1. 設 order_start 為指令攜帶的指令發生時間, t 為 order_start 所在 bucket 的起始時間
  2. 如果 t + delay_time <= 當前正在收集指令的 bucket 的起始時間,將指令投遞到 當前正在收集指令的 bucket 中,否則繼續 step 3
  3. 將指令投遞到 t + delay_time 對應的 bucket 中

其中 delay_time 為約定的服務器延遲時間,可以取為客戶端之間的平均延遲,Spaceroom 里默認取值80,以及 bucket 長度默認取值48. 在每個 bucket 時間段的末尾,服務器將此 bucket 廣播給所有客戶端,并開始接收下一個 bucket 的指令。客戶端根據收到的 bucket 間隔,在邏輯中自動進行對時,將時間誤差控制在一個可以接受的范圍內。

這個意思是,正常情況下,客戶端每隔 48ms 會收到從服務器端發來的一個 bucket,當到達需要處理這個 bucket 的時間時,客戶端會進行相應處理。假設客戶端 FPS=60,每隔 3幀 左右的時間,會收到一次 bucket,根據這個 bucket 來更新邏輯。如果因為網絡波動,超出時間后還沒有收到 bucket,客戶端暫停游戲邏輯并等待。在一個 bucket 之內的時間,邏輯的更新可以使用 lerp 的方法。

在 delay_time = 80, bucket_size = 48 的情況下,任一指令最少會被延遲 96ms 執行。更改這兩個參數,例如在 delay_time = 60, bucket_size = 32 的情況下,任一指令最少會被延遲 64ms 執行。

計時器引發的血案

整個看下來,我們的框架在運行的時候需要有一個精確的計時器。在固定的 interval 下執行 bucket 的廣播。理所當然地,我們首先想到了使用setInterval(),然而下一秒我們就意識到這個想法有多么的不靠譜:調皮的setInterval() 似乎有非常嚴重的誤差。而且要命的是,每一次的誤差都會累計起來,造成越來越嚴重的后果。

于是我們馬上又想到了使用 setTimeout(),通過動態地修正下一次到時的時間來讓我們的邏輯大致穩定在規定的 interval 左右。例如此次setTimeout()比預期少了5ms, 那么我們下一次就讓他提前5ms. 不過測試結果不盡人意,而且這怎么看都不夠優雅。

所以我們又要換一個思路。是否可以讓 setTimeout() 盡可能快地到期,然后我們檢查當前的時間是否到達目標時間。例如在我們的循環中,使用setTimeout(callback, 1) 來不停地檢查時間,這看起來像是一個不錯的主意。

令人失望的計時器

我們立即寫了一段代碼來測試我們的想法,結果令人失望。在目前最新的 node.js 穩定版(v0.10.32)以及 Windows 平臺下,運行這樣一段代碼:

  1. var sum = 0, count = 0; 
  2. function test() { 
  3.   var now = Date.now(); 
  4.   setTimeout(function () { 
  5.     var diff = Date.now() - now; 
  6.     sum += diff; 
  7.     count++; 
  8.     test(); 
  9.   }); 
  10.  
  11. test(); 

一段時間之后在控制臺里輸入 sum/count,可以看到一個結果,類似于:

  1. > sum / count 
  2. 15.624555160142348 

什么?!!我要 1ms 的間隔時間,你卻告訴我實際的平均間隔為 15.625ms!這個畫面簡直是太美。我們在 mac 上做同樣的測試,得到的結果是 1.4ms。于是我們心生疑惑:這到底是什么鬼?如果我是一個果粉,我可能就要得出 Windows 太垃圾然后放棄 Windows 的結論了,不過好在我是一名嚴謹的前端工程師,于是我開始繼續思索起這個數字來。

等等,這個數字為什么那么眼熟?15.625ms 這個數字會不會太像 Windows 下的最大計時器間隔了?立即下載了一個 ClockRes 進行測試,控制臺一跑果然得到了如下結果:

  1. Maximum timer interval: 15.625 ms 
  2. Minimum timer interval: 0.500 ms 
  3. Current timer interval: 1.001 ms 

果不其然!查閱 node.js 的手冊我們能看到這樣一段對 setTimeout 的描述:

  1. The actual delay depends on external factors like OS timer granularity and system load. 

然而測試結果顯示,這個實際延遲是最大計時器間隔(注意此時系統的當前計時器間隔只有 1.001ms),無論如何讓人無法接受,強大的好奇心驅使我們翻翻看 node.js 的源碼來一窺究竟。

Node.js 中的 BUG

相信大部分你我都對 Node.js 的 even loop 機制有一定的了解,查看 timer 實現的源碼我們可以大致了解到 timer 的實現原理,讓我們從 event loop 的主循環講起:

  1. while (r != 0 && loop->stop_flag == 0) { 
  2.     /* 更新全局時間 */ 
  3.     uv_update_time(loop); 
  4.     /* 檢查計時器是否到期,并執行對應計時器回調 */ 
  5.     uv_process_timers(loop); 
  6.  
  7.     /* Call idle callbacks if nothing to do. */ 
  8.     if (loop->pending_reqs_tail == NULL && 
  9.         loop->endgame_handles == NULL) { 
  10.       /* 防止event loop退出 */ 
  11.       uv_idle_invoke(loop); 
  12.     } 
  13.  
  14.     uv_process_reqs(loop); 
  15.     uv_process_endgames(loop); 
  16.  
  17.     uv_prepare_invoke(loop); 
  18.  
  19.     /* 收集 IO 事件 */ 
  20.     (*poll)(loop, loop->idle_handles == NULL && 
  21.                   loop->pending_reqs_tail == NULL && 
  22.                   loop->endgame_handles == NULL && 
  23.                   !loop->stop_flag && 
  24.                   (loop->active_handles > 0 || 
  25.                    !ngx_queue_empty(&loop->active_reqs)) && 
  26.                   !(mode & UV_RUN_NOWAIT)); 
  27.     /* setImmediate() 等 */ 
  28.     uv_check_invoke(loop); 
  29.     r = uv__loop_alive(loop); 
  30.     if (mode & (UV_RUN_ONCE | UV_RUN_NOWAIT)) 
  31.       break
  32.   } 

其中 uv_update_time 函數的源碼如下:(https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c)

  1. void uv_update_time(uv_loop_t* loop) { 
  2.   /* 獲取當前系統時間 */ 
  3.   DWORD ticks = GetTickCount(); 
  4.  
  5.   /* The assumption is made that LARGE_INTEGER.QuadPart has the same type */ 
  6.   /* loop->time, which happens to be. Is there any way to assert this? */ 
  7.   LARGE_INTEGER* time = (LARGE_INTEGER*) &loop->time; 
  8.  
  9.   /* If the timer has wrapped, add 1 to it's high-order dword. */ 
  10.   /* uv_poll must make sure that the timer can never overflow more than */ 
  11.   /* once between two subsequent uv_update_time calls. */ 
  12.   if (ticks < time->LowPart) { 
  13.     time->HighPart += 1; 
  14.   } 
  15.   time->LowPart = ticks; 

該函數的內部實現,使用了 Windows 的 GetTickCount() 函數來設置當前時間。簡單地來說,在調用setTimeout 函數之后,經過一系列的掙扎,內部的 timer->due 會被設置為當前 loop 的時間 + timeout。在 event loop 中,先通過 uv_update_time 更新當前 loop 的時間,然后在uv_process_timers 中檢查是否有計時器到期,如果有就進入 JavaScript 的世界。通篇讀下來,event loop大概是這樣一個流程:

  1. 更新全局時間
  2. 檢查定時器,如果有定時器過期,執行回調
  3. 檢查 reqs 隊列,執行正在等待的請求
  4. 進入 poll 函數,收集 IO 事件,如果有 IO 事件到來,將相應的處理函數添加到 reqs 隊列中,以便在下一次 event loop 中執行。在 poll 函數內部,調用了一個系統方法來收集 IO 事件。這個方法會使得進程阻塞,直到有 IO 事件到來或者到達設定好的超時時間。調用這個方法時,超時時間設定為最近的一個 timer 到期的時間。意思就是阻塞收集 IO 事件,最大阻塞時間為 下一個 timer 的到底時間。

Windows下 poll 函數之一的源碼:

  1. static void uv_poll(uv_loop_t* loop, int block) { 
  2.   DWORD bytes, timeout; 
  3.   ULONG_PTR key; 
  4.   OVERLAPPED* overlapped; 
  5.   uv_req_t* req; 
  6.  
  7.   if (block) { 
  8.     /* 取出最近的一個計時器的過期時間 */ 
  9.     timeout = uv_get_poll_timeout(loop); 
  10.   } else { 
  11.     timeout = 0; 
  12.   } 
  13.  
  14.   GetQueuedCompletionStatus(loop->iocp, 
  15.                             &bytes, 
  16.                             &key, 
  17.                             &overlapped, 
  18.                             /* 最多阻塞到下個計時器到期 */ 
  19.                             timeout); 
  20.  
  21.   if (overlapped) { 
  22.     /* Package was dequeued */ 
  23.     req = uv_overlapped_to_req(overlapped); 
  24.     /* 把 IO 事件插入隊列里 */ 
  25.     uv_insert_pending_req(loop, req); 
  26.   } else if (GetLastError() != WAIT_TIMEOUT) { 
  27.     /* Serious error */ 
  28.     uv_fatal_error(GetLastError(), "GetQueuedCompletionStatus"); 
  29.   } 

按照上述步驟,假設我們設置了一個 timeout = 1ms 的計時器,poll 函數會最多阻塞 1ms 之后恢復(如果期間沒有任何 IO 事件)。在繼續進入 event loop 循環的時候, uv_update_time 就會更新時間,然后uv_process_timers 發現我們的計時器到期,執行回調。所以初步的分析是,要么是uv_update_time 出了問題(沒有正確地更新當前時間),要么是 poll 函數等待 1ms 之后恢復,這個 1ms 的等待出了問題。

查閱 MSDN,我們驚人地發現對 GetTickCount 函數的描述:

 The resolution of the GetTickCount function is limited to the resolution of the system timer, which is typically in the range of 10 milliseconds to 16 milliseconds.

GetTickCount 的精度是如此的粗糙!假設 poll 函數正確地阻塞了 1ms 的時間,然而下一次執行uv_update_time 的時候并沒有正確地更新當前 loop 的時間!所以我們的定時器沒有被判定為過期,于是 poll 又等待了 1ms,又進入了下一次 event loop。直到終于 GetTickCount 正確地更新了(所謂15.625ms更新一次),loop 的當前時間被更新,我們的計時器才在 uv_process_timers 里被判定過期。

向 WebKit 求助

Node.js 的這段源碼看得人很無助:他使用了一個精度低下的時間函數,而且沒有做任何處理。不過我們立刻想到了既然我們使用 Node-WebKit,那么除了 Node.js 的 setTimeout,我們還有 Chromium 的 setTimeout。寫一段測試代碼,用我們的瀏覽器或者 Node-WebKit 跑一下:http://marks.lrednight.com/test.html#1 (#后面跟的數字表示需要測定的間隔),結果如下圖:

ss_1

按照 HTML5 的規范,理論結果應該是前5次結果是1ms,以后的結果是4ms。測試用例中顯示的結果是從第3次開始的,也就是說表上的數據理論上應該是前3次都是1ms,之后的結果都是4ms。結果有一定的誤差,而且根據規定,我們能拿到的最小的理論結果是4ms。雖然我們不滿足,但顯然這比 node.js 的結果讓我們滿意多了。強大的好奇心趨勢我們看看 Chromium 的源碼,看看他是如何實現的。(https://chromium.googlesource.com/chromium/src.git/+/38.0.2125.101/base/time/time_win.cc)

首先,在確定 loop 的當前時間方面,Chromium 使用了 timeGetTime() 函數。查閱 MSDN 可以發現這個函數的精度受系統當前 timer interval 影響。在我們的測試機上,理論上也就是上文中提到過的 1.001ms。然而 Windows 系統默認情況下,timer interval 是其最大值(測試機上也就是 15.625ms),除非應用程序修改了全局 timer interval。

如果你關注 IT界的新聞,你一定看過這樣的一條新聞。看起來我們的 Chromium 把計時器間隔設定得很小了嘛!看來我們不用擔心系統計時器間隔的問題了?不要開心得太早,這樣的一條修復給了我們當頭一棒。事實上,這個問題在 Chrome 38 中已經得到了修復。難道我們要使用修復以前的 Node-WebKit?這顯然不夠優雅,而且阻止了我們使用性能更高的 Chromium 版本。

進一步查看 Chromium 源碼我們可以發現,在有計時器,且計時器的 timeout < 32ms 時,Chromium 會更改系統的全局定時器間隔以實現小于 15.625ms 精度的計時器。(查看源碼) 啟動計時器時,一個叫HighResolutionTimerManager 的東西會被啟用,這個類會根據當前設備的電源類型,調用EnableHighResolutionTimer 函數。具體來說,當前設備用電池時,他會調用EnableHighResolutionTimer(false),而使用電源時會傳入 true。EnableHighResolutionTimer 函數的實現如下:

  1. void Time::EnableHighResolutionTimer(bool enable) { 
  2.   base::AutoLock lock(g_high_res_lock.Get()); 
  3.   if (g_high_res_timer_enabled == enable) 
  4.     return
  5.   g_high_res_timer_enabled = enable; 
  6.   if (!g_high_res_timer_count) 
  7.     return
  8.   // Since g_high_res_timer_count != 0, an ActivateHighResolutionTimer(true) 
  9.   // was called which called timeBeginPeriod with g_high_res_timer_enabled 
  10.   // with a value which is the opposite of |enable|. With that information we 
  11.   // call timeEndPeriod with the same value used in timeBeginPeriod and 
  12.   // therefore undo the period effect. 
  13.   if (enable) { 
  14.     timeEndPeriod(kMinTimerIntervalLowResMs); 
  15.     timeBeginPeriod(kMinTimerIntervalHighResMs); 
  16.   } else { 
  17.     timeEndPeriod(kMinTimerIntervalHighResMs); 
  18.     timeBeginPeriod(kMinTimerIntervalLowResMs); 
  19.   } 

其中,kMinTimerIntervalLowResMs = 4,kMinTimerIntervalHighResMs = 1。timeBeginPeriod 以及timeEndPeriod 是 Windows 提供的用來修改系統 timer interval 的函數。也就是說在接電源時,我們能拿到的最小的 timer interval 是1ms,而使用電池時,是4ms。由于我們的循環不斷地調用了 setTimeout,根據 W3C 規范,最小的間隔也是 4ms,所以松口氣,這個對我們的影響不大。

又一個精度問題

回到開頭,我們發現測試結果顯示,setTimeout 的間隔并不是穩定在 4ms 的,而是在不斷地波動。而http://marks.lrednight.com/test.html#48 測試結果也顯示,間隔在 48ms 和 49ms 之間跳動。原因是,在 Chromium 和 Node.js 的 event loop 中,等待 IO 事件的那個 Windows 函數調用的精度,受當前系統的計時器影響。游戲邏輯的實現需要用到 requestAnimationFrame 函數(不停更新畫布),這個函數可以幫我們將計時器間隔至少設置為 kMinTimerIntervalLowResMs(因為他需要一個16ms的計時器,觸發了高精度計時器的要求)。測試機使用電源的時候,系統的 timer interval 是 1ms,所以測試結果有 ±1ms 的誤差。如果你的電腦沒有被更改系統計時器間隔,運行上面那個#48的測試,max可能會到達48+16=64ms。

使用 Chromium 的 setTimeout 實現,我們可以將 setTimeout(fn, 1) 的誤差控制在 4ms 左右,而 setTimeout(fn, 48) 的誤差可以控制在 1ms 左右。于是,我們的心中有了一幅新的藍圖,它讓我們的代碼看起來像是這樣:

  1. /* Get the max interval deviation */ 
  2. var deviation = getMaxIntervalDeviation(bucketSize); // bucketSsize = 48, deviation = 2; 
  3. function gameLoop() { 
  4.   var now = Date.now(); 
  5.   if (previousBucket + bucketSize <= now) { 
  6.     previousBucket = now; 
  7.  
  8.     doLogic(); 
  9.   } 
  10.  
  11.   if (previousBucket + bucketSize - Date.now() > deviation) { 
  12.     // Wait 46ms. The actual delay is less than 48ms. 
  13.     setTimeout(gameLoop, bucketSize - deviation); 
  14.   } else { 
  15.     // Busy waiting. Use setImmediate instead of process.nextTick because the former does not block IO events. 
  16.     setImmediate(gameLoop); 
  17.   } 

上面的代碼讓我們等待一個誤差小于 bucket_size( bucket_size – deviation) 的時間而不是直接等于一個 bucket_size,46ms 的 delay 即便發生了最大的誤差,根據上文的理論,實際間隔也是小于48ms的。剩下的時間我們使用忙等待的方法,確保我們的 gameLoop 在足夠精確的 interval 下執行。

雖然我們利用 Chromium 在一定程度上解決了問題,但這顯然不夠優雅。

還記得我們最初的要求嗎?我們的服務器端代碼是應該可以脫離 Node-Webkit 客戶端的,直接在一臺有 Node.js 環境的電腦中運行。如果直接跑上面的代碼,deviation 的值至少是16ms,也就是說在每一個48ms中,我們要忙等待16ms的時間。CPU使用率蹭蹭蹭就上去了。

意想不到的驚喜

真是氣人啊,Node.js 里這么大的一個BUG,沒有人注意到嗎?答案真是讓我們喜出望外。這個BUG在 v.0.11.3 版本里已經得到了修復。直接查看 libuv 代碼的 master 分支也能看到修改后的結果。具體的做法是,在 poll 函數等待完成之后,把 loop 的當前時間,加上一個 timeout。這樣即便 GetTickCount 沒有反應過來,在經過poll的等待之后,我們還是加上了這段等待的時間。于是計時器就能夠順利地到期了。

也就是說,辛苦半天的問題,在 v.0.11.3 里已經得到了解決。不過,我們的努力不是白費的。因為即便消除了 GetTickCount 函數的影響,poll 函數本身也受到系統定時器的影響。解決方案之一,便是編寫 Node.js 插件,更改系統定時器的間隔。

不過我們這次的游戲,初步設定是沒有服務器的。客戶端建立房間之后,就成為了一個服務器。服務器代碼可以跑在 Node-WebKit 的環境中,所以 Windows 系統下計時器的問題的優先級并不是最高的。按照上文中我們給出的解決方案,結果已經足夠讓我們滿意。

收尾

解決了計時器的問題,我們的框架實現也就基本上再沒什么阻礙了。我們提供了 WebSocket 的支持(在純 HTML5 環境下),也自定義了通信協議實現了性能更高的 Socket 支持(Node-WebKit 環境下)。當然,Spaceroom 的功能在最初是比較簡陋的,但隨著需求的提出和時間的增加,我們也在逐漸地完善這個框架。

例如我們發現在我們的游戲里需要生成一致的隨機數的時候,我們就為 Spaceroom 加上了這樣的功能。在游戲開始的時候 Spaceroom 會分發隨機數種子,客戶端的 Spaceroom 提供了利用 md5 的隨機性,借助隨機數種子生成隨機數的方法。

So far so good. 看起來還是蠻欣慰的。在編寫這樣一個框架的過程當中,也學到了很多的東西。如果你對 Spaceroom 有點興趣,也可以參與到它當中來。相信,Spaceroom 會在更多的地方施展它的拳腳。

責任編輯:張偉 來源: 阿里巴巴用戶體驗部有一點
相關推薦

2011-12-16 10:08:36

Node.js

2020-05-29 15:33:28

Node.js框架JavaScript

2019-08-29 10:58:02

Web 開發框架

2015-12-25 16:31:54

開源攻防平臺DVNA

2013-03-28 14:54:36

2012-01-10 10:04:43

Node.js

2012-03-07 14:32:41

Node.js

2013-11-01 09:34:56

Node.js技術

2015-03-10 10:59:18

Node.js開發指南基礎介紹

2020-07-15 08:06:04

Node.js框架開發

2020-04-20 16:00:05

Node.js框架JavaScript

2014-04-01 11:02:00

Node.jsWeb Socket聊天程序

2011-11-01 10:30:36

Node.js

2011-09-08 13:46:14

node.js

2011-09-09 14:23:13

Node.js

2011-09-02 14:47:48

Node

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js

2017-06-28 08:31:11

Node.jsMVC微服務

2020-08-07 10:40:56

Node.jsexpress前端
點贊
收藏

51CTO技術棧公眾號

亚洲精品色午夜无码专区日韩| 日本精品福利视频| 一级片视频播放| 欧美日韩国产一区精品一区| 亚洲国产精品字幕| 91欧美视频在线| 性欧美猛交videos| 久久―日本道色综合久久| 国产欧美日韩丝袜精品一区| 久久综合亚洲色hezyo国产| 偷拍自拍一区| 欧美一区二区三区四区视频| 久久无码高潮喷水| 高h视频在线观看| 久久这里只有精品6| 3d蒂法精品啪啪一区二区免费| 性无码专区无码| 日韩av在线播放网址| 亚洲福利视频在线| 日韩av一卡二卡三卡| 中文字幕资源网在线观看免费| 最新日韩av在线| 欧美日韩在线播放一区二区| www.色日本| 麻豆精品国产91久久久久久| 午夜精品一区二区三区在线视频| 90岁老太婆乱淫| 国产精品xxx在线观看| 欧美欧美欧美欧美| 99re在线视频免费观看| 99久久精品免费看国产小宝寻花| 国产精品久久久久久一区二区三区 | 女教师淫辱の教室蜜臀av软件| 高清精品视频| 日韩欧美国产系列| 一本一道久久a久久综合蜜桃| 欧美黑人疯狂性受xxxxx野外| 亚洲电影中文字幕在线观看| 三上悠亚免费在线观看| 午夜国产福利在线| 国产精品天干天干在观线| 久久免费看av| 天堂中文在线资| 99久久精品免费| 国产精品麻豆免费版| 国产xxxx在线观看| 国产在线精品免费| 成人女保姆的销魂服务| 亚洲综合网av| 久久精品国产成人一区二区三区| 国产精品成人aaaaa网站| 日本一区二区免费在线观看| 激情成人亚洲| 欧美精品久久久久久久久| 好吊色视频在线观看| 亚洲影视一区二区三区| 美女扒开尿口让男人操亚洲视频网站| 亚洲欧美va天堂人熟伦| 欧美日韩一二三四| 日韩在线激情视频| 国产美女久久久久久| 婷婷综合在线| 欧美激情精品久久久久久久变态| 免费在线一区二区三区| 国产字幕视频一区二区| 欧美激情视频播放| 男女视频免费看| 久热re这里精品视频在线6| 国产精品成人一区二区三区吃奶| 久久久久久亚洲av无码专区| 美女视频一区二区三区| 亚洲影视九九影院在线观看| 丁香花免费高清完整在线播放| 国产aⅴ精品一区二区三区色成熟| 国产精品免费一区二区| 亚洲欧美另类综合| 久久色视频免费观看| 亚洲国产高清国产精品| 国产欧美黑人| 婷婷丁香久久五月婷婷| 嫩草av久久伊人妇女超级a| 成人在线视频观看| 日韩视频一区二区在线观看| 久久久久无码国产精品一区李宗瑞 | 日本韩国一区二区三区视频| 在线免费观看av的网站| 国产一区二区三区精品在线观看| 亚洲国内精品视频| 波多野结衣av在线观看| 2023国产精品久久久精品双| 国模私拍视频一区| 中文字幕第99页| 国产成人在线电影| 奇米888一区二区三区| 蜜桃av在线免费观看| 亚洲小说欧美激情另类| 99视频精品免费| 欧美久久亚洲| 亚洲女同精品视频| 丰满少妇高潮久久三区| 噜噜噜在线观看免费视频日韩| 91亚洲精品久久久久久久久久久久| 亚洲高清视频网站| 欧美激情一区在线观看| 欧美一级片免费播放| 日韩国产大片| 亚洲女人天堂视频| 国产精品theporn动漫| 免费人成在线不卡| 久久精品一二三区| 91精品国产91久久久久久青草| 精品欧美aⅴ在线网站| 亚洲最大天堂网| 久操国产精品| 韩国福利视频一区| 999国产精品视频免费| 国产拍欧美日韩视频二区| 欧美无砖专区免费| 亚洲成人精品综合在线| 亚洲午夜精品久久久久久久久久久久| 国产中文字字幕乱码无限| 精品一区二区在线免费观看| 日本一区精品| 国产在线美女| 精品免费日韩av| 国产精品视频一区二区三 | 久久久久亚洲| 国产成人精品在线播放| 视频在线不卡| 五月天亚洲精品| 精品人妻人人做人人爽夜夜爽| 日韩欧美午夜| 国产精品美女免费| 国产视频网址在线| 色综合天天综合狠狠| 在线精品一区二区三区| 亚洲五月婷婷| av成人午夜| 四虎影视成人| 日韩一级片网站| 丁香花五月激情| 国产毛片精品视频| 日韩一级片一区二区| 一区二区三区| 精品国产一区二区三区久久| 中文无码av一区二区三区| 久久这里只有精品首页| 久久精品99国产| 国产亚洲欧美日韩在线观看一区二区 | 亚洲国产成人精品激情在线| 福利电影一区二区三区| 国产一区 在线播放| 一区二区亚洲视频| 久久久久久国产免费| 黄色av一区二区三区| 性做久久久久久久免费看| 人妻体内射精一区二区三区| 亚洲精品少妇| 免费成人深夜夜行视频| 亚洲高清黄色| 日韩中文字幕欧美| www国产一区| 午夜精品久久久久久久蜜桃app| 中国黄色片视频| 先锋亚洲精品| 色综合影院在线观看| 香蕉久久一区| 欧美激情综合亚洲一二区| 少妇高潮久久久| 在线免费观看视频一区| 性生交大片免费全黄| 国产一区不卡视频| 国产婷婷一区二区三区| 精品香蕉视频| 亚洲一区二区三区香蕉| 岛国av在线网站| 亚洲天堂成人在线| 国产免费视频一区二区三区| 亚洲一区二区三区在线| 麻豆精品免费视频| 久久99久久久久| 男人添女荫道口女人有什么感觉| 免费日韩一区二区三区| 国产精品青草久久久久福利99| 菠萝菠萝蜜在线视频免费观看 | 黄网在线观看| 制服视频三区第一页精品| 国产一级片网址| 久久精品男人天堂av| 中文 日韩 欧美| 模特精品在线| 国产香蕉一区二区三区| 天堂成人娱乐在线视频免费播放网站 | 潘金莲一级淫片aaaaa免费看| 国产乱人伦丫前精品视频| 国产成人一区二区三区小说| 羞羞污视频在线观看| 亚洲一二三在线| 亚洲爱情岛论坛永久| 欧美亚洲免费在线一区| 久一区二区三区| 国产精品久久久久久久蜜臀| 理论片大全免费理伦片| 欧美aaaaa成人免费观看视频| 蜜臀av色欲a片无码精品一区| 日韩欧美高清| 欧美精品123| 亚洲超碰在线观看| 国产精品爽爽ⅴa在线观看| 岛国av在线网站| 久久91精品国产| sese一区| 亚洲女人天堂成人av在线| 蜜桃久久一区二区三区| 911国产精品| 亚洲午夜在线播放| 一本久久a久久免费精品不卡| 久草视频免费在线播放| 一区在线观看视频| 国产精品天天干| 91丨九色丨蝌蚪富婆spa| 中文字幕avav| 韩国一区二区视频| 久久久久久蜜桃一区二区| 久久久久久自在自线| 欧美日韩精品在线一区二区| 午夜国产精品视频| 樱空桃在线播放| 97久久视频| 亚洲电影网站| 不卡一区综合视频| 欧美日韩一区在线播放| 你懂的一区二区三区| 久久精品久久精品国产大片| 国产精品宾馆| 国产在线精品日韩| 国偷自产av一区二区三区| 99免费在线视频观看| 日韩一区二区三区在线看| 亚洲一区亚洲二区亚洲三区| 国产95亚洲| 亚洲va久久久噜噜噜久久天堂| av日韩一区| 91久久在线视频| 精品国产亚洲日本| 亚洲在线第一页| 亚洲精品v亚洲精品v日韩精品| 亚洲精品免费网站| 亚洲精品18| 国产精品一区二区av| 国产精品一区二区中文字幕| 国产日本一区二区三区| 少妇久久久久| 欧美日韩在线一二三| 精品国产一区二区三区噜噜噜 | 制服丝袜在线91| 国产同性人妖ts口直男| 欧美成人精品福利| 免费av网站在线播放| 亚洲激情在线观看| 男人的天堂在线视频| 在线观看日韩av| 国产午夜精品久久久久免费视| 欧美精品生活片| 日本不卡1234视频| 国产精品久久97| 精品三级久久久| 国产精品99久久久久久久| 久久精品论坛| 日韩成人av网站| 91精品啪在线观看国产81旧版| 国产精品无码免费专区午夜| 日韩一级不卡| 另类小说第一页| 国产精品综合在线视频| 亚洲av网址在线| 国产精品无圣光一区二区| 极品久久久久久| 精品久久久久久久久久久久| 中文字幕在线日本| 91精品福利在线一区二区三区| 国产综合视频在线| 亚洲欧美日韩直播| 欧美a在线看| 51精品在线观看| 亚洲天堂网站| 精品在线一区| 91久久久精品国产| 日韩中文字幕在线视频观看| 蜜桃视频一区二区| 国产女人18毛片水真多18| 国产精品日产欧美久久久久| 久久精品女人毛片国产| 欧美日韩国产不卡| 艳母动漫在线看| 久久精品人人做人人爽| 欧美裸体视频| 91系列在线播放| 自拍视频一区| 日韩成人三级视频| 日韩成人一区二区三区在线观看| 亚洲av午夜精品一区二区三区| 久久久.com| 美女视频黄免费| 欧美日韩在线一区二区| 日韩三级电影网| 欧美精品一本久久男人的天堂| 在线成人av观看| 成人在线免费观看一区| 国产精品精品国产一区二区| 欧美黄网站在线观看| 国产精品一区二区在线观看网站| 88久久精品无码一区二区毛片| 亚洲国产你懂的| 国产精品久久久久毛片| 国产亚洲欧美aaaa| 性感女国产在线| 国产一级二级三级精品| 牛夜精品久久久久久久99黑人| 亚洲一区在线不卡| wwwwww.欧美系列| 国产一级片久久| 日韩免费高清av| 国产一二三区在线观看| 国产日本欧美一区| 日韩av专区| 亚洲精品一二三四五区| av动漫一区二区| 黄色小说在线观看视频| 日韩限制级电影在线观看| 国产在线观看av| 成人夜晚看av| 久久国产综合| 一起操在线视频| 中文字幕中文字幕在线一区 | 久久久国产精品成人免费| 91精品国产高清一区二区三区| 日本亚洲精品| 国产精品一区二区久久久| 操欧美老女人| 国产免费又粗又猛又爽| 中文字幕不卡在线| 亚洲在线观看av| 日韩中文字幕网址| 国产激情综合| 老司机午夜免费福利视频| 国产精选一区二区三区| 九九久久免费视频| 亚洲第一页自拍| 麻豆mv在线观看| 欧美不卡三区| 日韩av电影天堂| 亚洲综合久久av一区二区三区| 欧美精品九九99久久| av观看在线| 国产精品乱子乱xxxx| 亚洲综合精品| 一级在线观看视频| 欧美日韩日本视频| 亚洲丝袜精品| 国产综合 伊人色| 久久国产66| 亚洲女同二女同志奶水| 日韩一区二区三区在线| 国产又色又爽又黄刺激在线视频| 激情小说网站亚洲综合网| 久久成人免费| 日韩精品一区二区三区在线视频| 欧美一区二区精品久久911| 久久av色综合| 日本在线高清视频一区| 精品综合免费视频观看| 精品无码久久久久久久久| 亚洲精品小视频在线观看| 久久xxx视频| 国产一二三区在线播放| 97超碰欧美中文字幕| 在线免费看毛片| 欧美激情一二区| 欧美三级情趣内衣| 宇都宫紫苑在线播放| 亚洲va欧美va天堂v国产综合| 黄色视屏网站在线免费观看| 91性高湖久久久久久久久_久久99| 亚洲免费成人| 日本精品久久久久中文| 精品国产成人在线影院| 日韩中文视频| 国产在线观看欧美| 国产欧美日韩中文久久| 俄罗斯嫩小性bbwbbw| 国产精品白嫩初高中害羞小美女 | 秋霞视频一区二区| 国产精品久久久久久网站| 国产精品啊啊啊| 国产午夜精品久久久久久久久| 精品国产一区二区三区av性色| 日本精品网站| 欧美 日韩 国产在线观看| 亚洲人成网站精品片在线观看|