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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章