LuaSocket 學習筆記


——— LUA SocketLib 和 協程

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

簡介

目標讀者:會使用 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):源(客戶端)一半套接字 和 終點(服務端)一半套接字。

在監聽一個端口的時候, 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 網絡編程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM