LuaSocket 學習筆記

——— LUA SocketLib 和 協程php

前言:
這是一篇譯文(The LUA SocketLib and the Coroutines),有刪改,原文見下方連接。html

簡介

目標讀者:會使用 LUA SocketLib;會用協程。編程

LUA SocketLib 不只提供了 TCP-UDP/IP 的網絡鏈接。還提供了諸如 TCP、UDP 的客戶端和服務端,以及 FTP、HTTP 協議等高級對象。服務器

本教程專一於 LUA SocketLib 提供的 socket 和 TCP/IP 服務器。一旦掌握了基本的操做,這個庫裏面的其餘組件用起來就是小菜一碟。網絡

協程是 LUA 5.1 的特性會在 TCP/IP 服務器的管理中使用到。多線程

TCP/IP 和 Socket 知識回顧

首先, TCP/IP 協議的聖經在此:RFC793
TCP/IP 是許多通訊協議的一個大集合,它基於兩個原始的協議:TCP 和 IP。 TCP 在 OSI 模型的第四層(傳輸層),IP在第三層(網絡層)。TCP 協議用於從應用向網絡的數據傳輸,可以以可靠的方式處理從源到目標地址的字節流(stream)。一個 TCP 鏈接由一個五件套表示{協議, IP本地地址,本地端口,IP遠程地址,遠程端口},它在給定的整個網絡中具備惟一性。
一個鏈接由 2 個 半套接字(half-sockets):源(客戶端)一半套接字 和 終點(服務端)一半套接字。socket

在監聽一個端口的時候, TCP 服務器建立半個套接字(服務器套接字)。接收到鏈接時,服務器對這半個套接字進行復制並與終點的半個套接字(客戶端套接字)鏈接,創建通訊套接字,實現信息數據流的傳輸。所以,服務器老是擁有一個可用的半個套接字。tcp

創建 TCP/IP 服務器

一般創建一個TCP服務器的過程能夠直接用 SocketLib 的函數實現。具體流程以下:函數

  1. 創建服務端套接字
  2. 把服務端套接字附加到端口
  3. 監聽這個端口

SocketLib 是 Hyperion 的一部分,它能夠像 LUA 提供的其餘標準庫同樣使用。下面給出創建 TCP 服務器的代碼:測試

function createTCPServer( host, port, backlog) // host 是個啥?
    local tcp_server,err = socket.tcp();
    if tcp_server == nil then
        return nil,err;
    end

    tcp_server:setoption("reuseaddr",true);
    local res, err = tcp_server:bind(host,port);
    if res==nil then
        return nil,err;
    end

    res,err = tcp_server:listen(backlog);
    if res == nil then
        return nil,err;
    end
    
    return tcp_server;
end

socket.tcp() 建立一個 TCP 服務端對象。函數和對象的詳解見本頁末尾。
在建立對象以後是一些耳熟能詳的步驟:setoption(), bind() 及 listen();

大多數 SocketLib 函數成功返回一個參數,失敗返回兩個參數。在失敗的狀況下,參數一返回 nil,參數二返回錯誤信息。

createTCPServer() 必須在 host 程序初始化的時候調用。在 Hyperion 中,咱們直接把函數及其調用都放在一個 LUA 的初始化腳本中(run_mode="INIT")。

最後一件事:注意 createTCPServer() 函數中的 host 參數。若是你用 localhost 來設置它,那麼這個 TCP 服務器只能接受本地主機(localhost)傳進來的鏈接(例如客戶端和服務器端在同一個電腦裏運行)。因此,若是你但願可以連放在另外一臺計算機運行的客戶端程序,你須要把host設置爲:host = "*";。這是一個小技巧,可是相信我,若是你不知道的話可能會浪費不少時間!

建立一個 TCP 服務器是最簡單的部分,讓咱們來處理有份量的那部分:傳入鏈接的接收與管理。

接收(incoming)與發送(connecting)鏈接管理 -協程

處理傳入鏈接的指導原則是把 TCP 服務器設置成一個死循環,在循環的開始處用 server:accept() 函數來等待鏈接。可是咱們立刻發現了問題:若是咱們進入這個死循環,那麼服務器程序就會凍結在那裏,這是咱們沒法接受的。

對於這個尖銳的問題,其解決方案是把這個死循環轉換爲一個與程序主線程分離的執行路徑。有兩個方法:線程或者協程。

線程你們都比較瞭解,再也不贅述。

協程與線程同樣是一個分離的執行隊列,可是不禁 OS 管理,而是由主程序來控制協程的切換。主程序每隔一小段時間會遍歷一遍目前存在的協程。

協程也被稱做協做的多線程,意思是協程必須相互合做來使得多線程正確工做。若是一個協程由於某種緣由中止合做,整個程序都會受到影響。若是一個應用程序卡死,那麼極可能不是因爲操做系統的問題,而是因爲應用程序中不合理的協做關係。

爲了可以正確進行協做,協程中無限循環的每一次循環都必須儘快結束。協程中最關鍵的函數是 accept() 。這個函數在默認狀況下直到新的鏈接到來纔會執行下一步。這種行爲是高度非協做性的。所以咱們須要經過 server:settimeout() 來設置一個等待的最大時間。在後面提供的 DEMO_LUA_TCP_Server_Coroutine.xml demo 中,這個時間被設置爲 10ms。

故事到此尚未結束。協程須要告訴主程序何時可以出讓空間給下一個協程。這個功能由 coroutine.yield() 函數提供。yield 是協程無限循環中的最後一個指令。經過這個函數,主程序會獲知當前協程已經執行完畢,輪到執行下一個協程。另外一個協程的運行經過 coroutine.resume() 函數來實現。

如今,讓咱們以更具體的方式來看一看。先來看看等待傳入鏈接的核心實現部分。

function runTCPServer()
    local stop_server = 0;
    local num_cnx = 0;

    while( stop_server == 0) do
        local err = "";
        local tcp_client = 0;

        g_tcp_server:settimeout(0.01);
        tcp_client,err = g_tcp_server:accept();

        if tcp_client ~= nil then

            g_client_msg, err = tcp_client:receive('*|'); 

            if( g_client_msg ~= nil) then
                if (g_client_msg == "STOP") then
                    stop_server = 1;
                else
                    tcp_client:send("Message :"..g_client_msg);
                end
            end

            if tcp_client ~= nil then
                tcp_client:close();
            end
        end

        coroutine.yield();

    end
end

這個協程的建立是在初始化部分經過 coroutine.create() 函數實現。它的參數是等待處理的協程。這個函數會返回一個新建協程的句柄。

g_tcp_co = coroutine.create(runTCPServer);

經過這個句柄,咱們能夠按固定間隔來執行對應的協程(例如每幀調用一次),調用 coroutine.resume() 函數便可。

coroutine.resume( g_tcp_co);

一旦有鏈接傳進來,TCP 服務器經過 server:receive() 函數接收到數據。參數中 *| 表示接收到的數據以 LF\n 結尾。這裏 TCP 服務器以反射模式工做:經過 server:send() 把接收到的數據原路返回給客戶端。

爲了測試 TCP 服務端,這裏有一個TCP 客戶端可供使用:

這個客戶端用起來很簡單。只要輸入服務器名稱(主機名稱或者IP地址,例如:127.0.0.1)、服務器的端口號和須要傳送的消息。這個客戶端會自動添加消息結尾。而後點擊發送就OK啦,服務器的反饋信息會在底部區域顯示。


參考連接

The LUA SocketLib and the Coroutines
Tutorial:Networking with UDP
LUA 網絡編程

相關文章
相關標籤/搜索