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
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,那麼這樣我們就能把請求與響應一一對應,在這一節我們不做擴展了。