预备知识:
TCP/IP:https://www.cnblogs.com/h2zZhou/p/10488670.html
socket
简单理解——socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
https://zhuanlan.zhihu.com/p/142650150
socket服务端
第一步:监听一个端口
local ID = socket.listen(“127.0.0.1”,8001) --监听一个端口,返回一个 id ,供 start 使用
assert(ID) --没有客户端接入时,保持阻塞
socket.start(ID, accept) --accept 是一个函数。每当一个监听的 id 对应的 socket 上有连接接入的时候,都会调用 accept 函数。这个函数会得到接入连接的 id 以及 ip 地址。你可以做后续操作。
第二步:收一个消息
socket.start(ID)
--任何一个服务只有在调用 socket.start(id) 之后,才可以读到这个 socket 上的数据。向一个 socket id 写数据也需要先调用 start 。
local str = socket.read(ID,sz)
--从一个socket上读sz(sz可省略)指定的字节数。如果读到了指定长度的字符串,它把这个字符串返回。
第三步:发送一个消息
socket.write(ID, str) --把一个字符串置入正常的写队列,skynet 框架会在 socket 可写时发送它。
--服务端读写操作之前,需要socket.start(ID)
第四步:关闭一个socket连接
socket.close(ID)
示例代码:
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("recv2 " ..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.error(cID .. " cIDaccepted")
skynet.fork(echo, cID, addr) --来一个连接,就开一个新的协程来处理客户端数据
end
--服务入口
skynet.start(function()
local addr = "0.0.0.0:8001"
skynet.error("listen " .. addr)
local lID = socket.listen("127.0.0.1",8001)
assert(lID)
socket.start(lID, accept)
end)
socket客户端
(基于skynet里面的socket服务)
第一步:连接一个端口
local ID = socket.open(addr) --建立一个 TCP 连接。返回一个数字 id
assert(ID) --连接建立前,保持阻塞
第二步:发送一个消息
socket.write(ID, str) --把一个字符串置入正常的写队列,skynet 框架会在 socket 可写时发送它。
--客户端读写操作之前,不需要socket.start(ID)
第三步:收一个消息
local str = socket.read(ID,sz)
--从一个socket上读sz(sz可省略)指定的字节数。如果读到了指定长度的字符串,它把这个字符串返回。
第四步:关闭一个socket连接
socket.close(ID)
skynet.exit()
示例代码:
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)
socket.channel模式(针对客户端)
在与外部服务交互式时,请求回应模式是最常用模式之一。通常的协议设计方式有两种。
①每个请求包对应一个回应包,由 TCP 协议保证时序。
②发起每个请求时带一个唯一 session标识,在发送回应时,带上这个标识。这样设计可以不要求每个请求都一定要有回应,且不必遵循先提出的请求先回应的时序。
但是问题是:
①对于第一种模式,用 skynet 的 Socket API 很容易实现,但如果在一个 coroutine 中读写一个 socket的话,由于读的过程是阻塞的,这会导致吞吐量下降(前一个回应没有收到时,无法发送下一个请求)。
②对于第二种模式,需要用 skynet.fork 开启一个新线程来收取回响应包,并自行和请求对应起来,实现比较繁琐。
这两个模式的常规socket实现:https://blog.csdn.net/qq769651718/article/details/79434989
为了解决以上两个问题,云风哥为skynet 提供了一个更高层的封装:socket channel
云风的解释:https://blog.codingnow.com/2014/03/skynet_socket_channel.html
用 socket.channel 解决问题①
第一步:创建 channel 对象
socket.channel { host = hostname, port = port_number } 创建出一个对象
local sc = require "skynet.socketchannel"
local channel = sc.channel {
host = "127.0.0.1",
port = 8001,
}
--这个对象用来和外部服务器通讯
第二步:发送请求、返回消息
使用 channel:request(request, response) 这个 api 获取回应信息
request是一个字符串,即需要发送给服务器的请求内容。而
response 是一个函数,要求它可以返回两个值:第一个是一个 boolean ,true表示回应内容正确,这时第 2 个返回值就是返回的对象。
--接收响应的数据必须这么定义,sock就是与远端的TCP服务相连的套接字,通过这个套接字可以把数据读出来
--返回值必须要有两个,第一个如果是true表示响应数据是有效的,
function response(sock)
return true, sock:read()
end
resp = channel:request("msgdata", response) --返回的值就是response函数的第二个返回值
实例
local skynet = require "skynet"
require "skynet.manager"
local sc = require "skynet.socketchannel"
local channel = sc.channel {
host = "127.0.0.1",
port = 8001,
}
--接收响应的数据必须这么定义,sock就是与远端的TCP服务相连的套接字,通过这个套接字可以把数据读出来
function response(sock)
--返回值必须要有两个,第一个如果是true表示响应数据是有效的,
return true, sock:read()
end
local function task()
local resp
local i = 0
while(i < 3) do
--第一参数是需要发送的请求,第二个参数是一个函数,用来接收响应的数据。
--调用channel:request会自动连接指定的TCP服务,并且发送请求消息。
--该函数阻塞,返回读到的内容。
resp = channel:request("data"..i.."\n", response)
skynet.error("recv", resp)
i = i + 1
end
--channel:close() --channel可以不用关闭,当前服务退出的时候会自动关闭掉
skynet.exit()
end
skynet.start(function()
skynet.fork(task)
end)
用 socket.channel 解决问题②
第一步:创建 channel 对象
socket.channel { host = hostname, port = port_number , response = dispatcher }
这里的 dispatcher 就是这样的一个函数,它需要返回 3 个值:
第一个是 session ,表示这个包对应的是哪一次请求。
第二、三个返回值的含义和前面所描述的 response 函数相同:一个 boolean 加一个返回对象。
local sc = require "skynet.socketchannel"
function dispatch(sock)
local r = sock:readline()
local session = tonumber(string.sub(r,5))
return session, true, r --返回值必须要有三个,第一个session
end
local channel = sc.channel {
host = "127.0.0.1",
port = 8001,
response = dispatch --处理消息的函数
}
第二步:发送请求、返回消息
在发起请求的时候,使用 channel:request(request, session) 即可
参数 request 是一个字符串,即需要发送给服务器的请求内容
参数 session 是这次请求的 session 。
返回值是dispatch的最后一个返回值。
resp = channel:request("msgdata", session)
实例
local skynet = require "skynet"
require "skynet.manager"
local sc = require "skynet.socketchannel"
function dispatch(sock)
local r = sock:readline()
local session = tonumber(string.sub(r,5))
return session, true, r --返回值必须要有三个,第一个session
end
--创建一个 channel 对象出来,其中 host 可以是 ip 地址或者域名,port 是端口号。
local channel = sc.channel {
host = "127.0.0.1",
port = 8001,
response = dispatch --处理消息的函数
}
local function task()
local resp
local i = 0
while(i < 3) do
skynet.fork(function(session)
resp = channel:request("data"..session.."\n", session)
skynet.error("recv", resp, session)
end, i)
i = i + 1
end
end
skynet.start(function()
skynet.fork(task)
end)
本篇博文主要参考:https://blog.csdn.net/qq769651718/category_7480207.html