skynet框架應用 (九) socket網絡服務

9 socket網絡服務

9.1 skynet.socket 常用api

--這樣就可以在你的服務中引入這組 api 。
local socket = require "skynet.socket"

--建立一個 TCP 連接。返回一個數字 id 。
socket.open(address, port)      

--關閉一個連接,這個 API 有可能阻塞住執行流。因爲如果有其它 coroutine 
--正在阻塞讀這個 id 對應的連接,會先驅使讀操作結束,close 操作才返回。
socket.close(id)

--在極其罕見的情況下,需要粗暴的直接關閉某個連接,而避免 socket.close 的阻塞等待流程,可以使用它。
socket.close_fd(id)

--強行關閉一個連接。和 close 不同的是,它不會等待可能存在的其它 coroutine 的讀操作。
--一般不建議使用這個 API ,但如果你需要在 __gc 元方法中關閉連接的話,
--shutdown 是一個比 close 更好的選擇(因爲在 gc 過程中無法切換 coroutine)。與close_fd類似
socket.shutdown(id)

--[[
    從一個 socket 上讀 sz 指定的字節數。
    如果讀到了指定長度的字符串,它把這個字符串返回。
    如果連接斷開導致字節數不夠,將返回一個 false 加上讀到的字符串。
    如果 sz 爲 nil ,則返回儘可能多的字節數,但至少讀一個字節(若無新數據,會阻塞)。
--]]
socket.read(id, sz)

--從一個 socket 上讀所有的數據,直到 socket 主動斷開,或在其它 coroutine 用 socket.close 關閉它。
socket.readall(id)

--從一個 socket 上讀一行數據。sep 指行分割符。默認的 sep 爲 "\n"。讀到的字符串是不包含這個分割符的。
--如果另外一端就關閉了,那麼這個時候會返回一個nil,如果buffer中有未讀數據則作爲第二個返回值返回。
socket.readline(id, sep) 

--等待一個 socket 可讀。
socket.block(id) 

 
--把一個字符串置入正常的寫隊列,skynet 框架會在 socket 可寫時發送它。
socket.write(id, str) 

--把字符串寫入低優先級隊列。如果正常的寫隊列還有寫操作未完成時,低優先級隊列上的數據永遠不會被髮出。
--只有在正常寫隊列爲空時,纔會處理低優先級隊列。但是,每次寫的字符串都可以看成原子操作。
--不會只發送一半,然後轉去發送正常寫隊列的數據。
socket.lwrite(id, str) 

--監聽一個端口,返回一個 id ,供 start 使用。
socket.listen(address, port) 

--[[
    accept 是一個函數。每當一個監聽的 id 對應的 socket 上有連接接入的時候,都會調用 accept 函數。
這個函數會得到接入連接的 id 以及 ip 地址。你可以做後續操作。
    每當 accept 函數獲得一個新的 socket id 後,並不會立即收到這個 socket 上的數據。
這是因爲,我們有時會希望把這個 socket 的操作權轉讓給別的服務去處理。accept(id, addr)
]]--
socket.start(id , accept) 

--[[
    任何一個服務只有在調用 socket.start(id) 之後,纔可以讀到這個 socket 上的數據。
向一個 socket id 寫數據也需要先調用 start 。
    socket 的 id 對於整個 skynet 節點都是公開的。也就是說,你可以把 id 這個數字
通過消息發送給其它服務,其他服務也可以去操作它。skynet 框架是根據調用 start 這個 
api 的位置來決定把對應 socket 上的數據轉發到哪裏去的。
--]]
socket.start(id)

--清除 socket id 在本服務內的數據結構,但並不關閉這個 socket 。
--這可以用於你把 id 發送給其它服務,以轉交 socket 的控制權。
socket.abandon(id) 

--[[
    當 id 對應的 socket 上待發的數據超過 1M 字節後,系統將回調 callback 以示警告。
function callback(id, size) 回調函數接收兩個參數 id 和 size ,size 的單位是 K 。
    如果你不設回調,那麼將每增加 64K 利用 skynet.error 寫一行錯誤信息。
--]]
socket.warning(id, callback) 

9.2 寫一個skynet TCP監聽端

示例代碼:socketservice.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"

--簡單echo服務
function echo(cID, addr)
    socket.start(cID)
    while true do
        local str = socket.read(cID)
        if str then
            skynet.error("recv " ..str)
            socket.write(cID, string.upper(str))
        else
            socket.close(cID)
            skynet.error(addr .. " disconnect")
            return
        end
    end
end

function accept(cID, addr)
    skynet.error(addr .. " accepted")
    skynet.fork(echo, cID, addr) --來一個連接,就開一個新的協程來處理客戶端數據
end

--服務入口
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID, accept)
end)

運行:

$ ./skynet examples/config
socketserver     #終端中手動輸入該服務,例如,輸入“socketserver”回車
[:01000010] LAUNCH snlua socketserver
[:01000010] listen 0.0.0.0:8001

來一個c寫的客戶端:

示例代碼:socketclient.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#define MAXLINE 128
#define SERV_PORT 8001

void* readthread(void* arg)
{
    pthread_detach(pthread_self());
    int sockfd = (int)arg;
    int n = 0;
    char buf[MAXLINE];
    while (1) 
    {
        n = read(sockfd, buf, MAXLINE);
        if (n == 0)
        {
            printf("the other side has been closed.\n");
            close(sockfd);
            exit(0);
        }
        else
            write(STDOUT_FILENO, buf, n);
    }   
    return (void*)0;
}

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    int sockfd;
    char buf[MAXLINE];
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    pthread_t thid;
    pthread_create(&thid, NULL, readthread, (void*)sockfd);

    while (fgets(buf, MAXLINE, stdin) != NULL) 
        write(sockfd, buf, strlen(buf));
    close(sockfd);
    return 0;
}

編譯與運行:

$ gcc socketclient.c -lpthread -o socketclient
$ ./socketclient 
helloworld    #發送請求
HELLOWORLD    #收到響應
^C            # ctrl+c終止掉客戶端
$ 

skynet服務器端顯示:

socketserver    #終端輸入
[:01000010] LAUNCH snlua socketserver
[:01000010] listen 0.0.0.0:8001
[:01000010] 127.0.0.1:41644 accepted   #產生請求
[:01000010] recv helloworld            #接受請求,並響應

[:01000010] 127.0.0.1:41644 disconnect  #另一端已經斷開連接

9.3 socket.readline使用

local skynet    = require "skynet"
local socket    = require "skynet.socket"

--簡單echo服務
function echo(cID, addr)
    socket.start(cID)
    while true do
        local str, endstr = socket.readline(cID)
        --local str, endstr = socket.readline(cID, "\n")
        if str then
            skynet.error("recv " ..str)
            socket.write(cID, string.upper(str))
        else
            socket.close(cID)
            if endstr then
                skynet.error("last recv " ..endstr)
            end
            skynet.error(addr .. " disconnect")
            return
        end
    end
end

function accept(cID, addr)
    skynet.error(addr .. " accepted")
    skynet.fork(echo, cID, addr)
end

--服務入口
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID, accept)
end)

9.4 socket.readall的使用

local skynet    = require "skynet"
local socket    = require "skynet.socket"

--簡單echo服務
function echo(cID, addr)
    socket.start(cID)
    local str = socket.readall(cID)
    if str then
       skynet.error("recv " ..str)
    end 
    skynet.error(addr .. " close")
    socket.close(cID)
    return
end

function accept(cID, addr)
    skynet.error(addr .. " accepted")
    skynet.fork(echo, cID, addr)
end

--服務入口
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID, accept)
end)

9.5 低優先級的發送函數

local skynet    = require "skynet"
local socket    = require "skynet.socket"

--簡單echo服務
function echo(cID, addr)
    socket.start(cID)
    while true do
        local str = socket.read(cID)
        if str then
            skynet.error("recv " ..str)
            --由於cpu處理非常快,無法看到效果,只有當cpu複覈過高的時候,纔會出現低優先級後發送的現象
            socket.lwrite(cID, "l:" .. string.upper(str))  
            socket.write(cID, "h:" .. string.upper(str))  
        else
            socket.close(cID)
            skynet.error(addr .. " disconnect")
            return
        end
    end
end

function accept(cID, addr)
    skynet.error(addr .. " accepted")
    --如果不開協程,那麼同一時刻肯定只能處理一個客戶端的連接請求
    skynet.fork(echo, cID, addr)   
end

--服務入口
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID, accept)
end)

9.6 skynet的socket代理服務

​ 我們可以把監聽的服務拆分爲兩個,一個是專門負責監聽的服務,一旦有新的連接產生,那麼監聽的服務會啓動一個agent服務,專門用來處理數據請求與應答。這樣可以讓每個服務分工明確,各司其職,服務拆分成更小的服務也便於書寫業務邏輯。

示例代碼:socketlisten.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID , function(cID, addr)
        skynet.error(addr .. " accepted")
        skynet.newservice("socketagent", cID, addr)
    end)
end)

示例代碼:socketagent.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"
function echo(cID, addr)
    socket.start(cID)
    while true do
        local str = socket.read(cID)
        if str then
            skynet.error("recv " ..str)
            socket.write(cID, string.upper(str))  
        else
            socket.close(cID)
            skynet.error(addr .. " disconnect")
            return
        end
    end
end

local cID, addr = ...
cID = tonumber(cID)

skynet.start(function()
    skynet.fork(function()
        echo(cID, addr)
        skynet.exit()
    end)
end)

運行如下:

  • 啓動socketlisten

$ ./skynet examples/config
socketlisten #終端輸入
[:01000010] LAUNCH snlua socketlisten
[:01000010] listen 0.0.0.0:8001
[:01000010] 127.0.0.1:41936 accepted                    #產生新連接
[:01000012] LAUNCH snlua socketagent 9 127.0.0.1:41936  #啓動代理服務
[:01000012] recv aaaaaaaaaaa                            #接受請求,並響應
  • 啓動c語言寫的socketclient

$ ./socketclient 
aaaaaaaaaaa      #stdin輸入aaaaaaaaaaa
AAAAAAAAAAA      #收到服務器的應答

9.7 轉交socket控制權

示例代碼:socketabandon.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID , function(cID, addr)
        skynet.error(addr .. " accepted")
        --當前服務開始使用套接字
        socket.start(cID)
        local str = socket.read(cID)
        if(str) then
            socket.write(cID, string.upper(str))
        end
        --不想使用了,這個時候遺棄控制權
        socket.abandon(cID) 

        skynet.newservice("socketagent", cID, addr) --代理服務不變
    end)
end)
  • 運行:先運行socketabandon,再運行c寫的socketclient

skynet服務端輸出:

$ ./skynet examples/config
socketabandon         #終端輸入
[:01000010] LAUNCH snlua socketabandon   #啓動socketabandon服務
[:01000010] listen 0.0.0.0:8001 
#另外一個終端啓動socketclient,這邊接受請求,並且使用新的socket,read write,然後遺棄控制權
[:01000010] 127.0.0.1:41950 accepted     
[:01000012] LAUNCH snlua socketagent 9 127.0.0.1:41950 #啓動代理服務,把socket控制權交給它
[:01000012] recv bbbbbbbbbbbbbb

[:01000012] 127.0.0.1:41950 disconnect
[:01000012] KILL self

socketclient客戶端輸出:

$ ./socketclient 
aaaaaaaaaaaaaa
AAAAAAAAAAAAAA
bbbbbbbbbbbbbb
BBBBBBBBBBBBBB
^C

9.8 skynet的TCP主動連接端

​ 1、有些時候服務要跟其他的外部服務進行交互,那麼這個時候skynet的服務會是主動去連接的一端。

​ 2、不僅如此,其實skynet中的兩個服務也可以通過socket進行通信。

示例代碼:socketclient.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"

function client(id)
  local i = 0
  while(i < 3) do
        skynet.error("send data"..i)
        socket.write(id, "data"..i.."\n")
        local str = socket.readline(id)
        if str then
            skynet.error("recv " .. str)
        else
            skynet.error("disconnect")
        end
        i = i + 1
   end
   socket.close(id)   --不主動關閉也行,服務退出的時候,會自動將套接字關閉
   skynet.exit()
end

skynet.start(function()
    local addr = "127.0.0.1:8001"
    skynet.error("connect ".. addr)
    local id  = socket.open(addr)
    assert(id)
    --啓動讀協程
    skynet.fork(client, id)
end)

監聽端代碼如下:serverreadline.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"

--簡單echo服務
function echo(cID, addr)
    socket.start(cID)
    while true do
        local str = socket.readline(cID)
        if str then
            skynet.fork(function()
                skynet.error("recv " ..str)
                skynet.sleep(math.random(1, 5) * 100)
                socket.write(cID, string.upper(str) .. "\n")
            end)
        else
            socket.close(cID)
            skynet.error(addr .. " disconnect")
            return
        end
    end
end

function accept(cID, addr)
    skynet.error(addr .. " accepted")
    skynet.fork(echo, cID, addr)
end

--服務入口
skynet.start(function()
    local addr = "0.0.0.0:8001"
    skynet.error("listen " .. addr)
    local lID = socket.listen(addr)
    assert(lID)
    socket.start(lID, accept)
end)

運行結果:

 $ ./skynet examples/config
serverreadline #終端輸入
[:01000010] LAUNCH snlua serverreadline #啓動監聽服務
[:01000010] listen 0.0.0.0:8001
socketclient #終端輸入
[:01000012] LAUNCH snlua socketclient   #啓動另一個充當客戶端的服務
[:01000012] connect 127.0.0.1:8001      #客戶端服務連接監聽服務
[:01000010] 127.0.0.1:44034 accepted    #監聽服務接受連接
[:01000012] send data0                  #依次處理
[:01000010] recv data0  
[:01000012] recv DATA0
[:01000012] send data1
[:01000010] recv data1
[:01000012] recv DATA1
[:01000012] send data2
[:01000010] recv data2
[:01000012] recv DATA2
[:01000012] KILL self                   #客戶端服務退出,主動斷開連接
[:01000010] 127.0.0.1:44034 disconnect  #監聽服務也關閉連接
  • 需要注意的是:

    雖然這樣也可以讓兩個服務之間進行通信,但是如果在同一個節點的服務,通信一般就用lua消息來通信就好,畢竟維護套接字的成本遠比本地消息調度要高的多。

9.9 發請求與收應答分離

​ 9.8中的例子我們只能嚴格按照發送請求,然後等待響應的時序來完成與其他服務的主動交互。現在我們在發送請求的時候啓動一個協程,而讀取響應的時候也啓動一個協程來處理。

​ 代碼如下:socketforkclient.lua

local skynet    = require "skynet"
local socket    = require "skynet.socket"

local function recv(id)
   local i = 0
   while(i < 3) do
    local str = socket.readline(id)
    if str then
            skynet.error("recv " .. str)
    else
            skynet.error("disconnect")
    end
    i = i + 1
   end
   socket.close(id)       --未接收完不要關閉
   skynet.exit()
end

local function send(id)    --不用管有沒接受到數據直接發送三次
    local i = 0
    while(i < 3) do
    skynet.error("send data"..i)
    socket.write(id, "data"..i.."\n")
        i = i + 1
    end
end

skynet.start(function()
    local addr = "127.0.0.1:8001"
    skynet.error("connect ".. addr)
    local id  = socket.open(addr)
    assert(id)
    --啓動讀協程
    skynet.fork(recv, id)
    --啓動寫協程
    skynet.fork(send, id)
end)

運行結果(serverreadline.lua還是使用9.8中的例子):

$ ./skynet examples/config
serverreadline
[:01000010] LAUNCH snlua serverreadline
[:01000010] listen 0.0.0.0:8001
socketforkclient
[:01000012] LAUNCH snlua socketforkclient
[:01000012] connect 127.0.0.1:8001
[:01000010] 127.0.0.1:44072 accepted
[:01000012] send data0       #一次性發完三次命令不用等待
[:01000012] send data1
[:01000012] send data2
[:01000010] recv data0
[:01000010] recv data1
[:01000010] recv data2
[:01000019] recv DATA2      #但是接收回來的數據並不是有序的了
[:01000019] recv DATA0
[:01000019] recv DATA1
[:01000012] KILL self
[:01000010] 127.0.0.1:44072 disconnect

​ 可以看到發送請求的一方可以不受響應速度的影響直接發送,但是由於每個請求的處理時間是不一樣的,所以接受到的響應信息並不是有序的了。這也是這種模型帶來的問題,解決辦法就是每個請求發送都攜帶一個session,接受到的響應信息也攜帶一個session,那麼這樣我們就能把請求與響應一一對應,在這一節我們不做擴展了。

發佈了101 篇原創文章 · 獲贊 117 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章