skynet 的C API采用异步读写,你可以使用C调用,监听一个端口,或发起一个TCP连接,单具体的操作结果要等待skynet的时间回调,skynet会把结果以PTYPE_SOCKET类型的消息发给发起请求的服务.参考skynet_socket.h
在处理实际业务中,这样的API很难使用,所以又提供了一组阻塞模式的lua API用于TCP socket的读写,他是对C API的封装.
所谓阻塞模式,实际上是利用了lua的coroutine机制,当你调用socket api 时,服务有可能被挂起(时间片被让给其他业务处理),待结果通过socket消息返回,coroutine将延续执行,是延续.
local socket = require "socket"这样就可以在你的服务中引入这组api.
.socket.open(address.port) 建立一个TCP连接,返回一个数字id
.socket.close(id) 关闭一个连接,这个API有可能阻塞住执行流,因为如果有其他coroutine正在阻塞读这个id对应的连接,会先驱使读操作结果,close操作才返回.
socket.close_fd(id) 在极其罕见的情况下,需要粗暴的直接关闭某个连接,而避免socket.close的阻塞等待流程,可以使用它.
socket.shutdown(id)强行关闭一个连接,和close不同的是,他不会等待可能存在的其他coroutine的读操作,一般不建议使用这个API,但如果你需要在_gc元方法中关闭连接的话,shutdown是一个比close更好的选择,(因为在gc过程中无法切换coroutine)
socket.read(id,sz) 从一个socket上读sz指定的字节数,如果读到了指定长度的字符串,他把这个字符串返回.如果连接断开导致字节数不够,将返回一个false加上读到的字符串.如果sz为nil,则返回尽可能多的字节数,但至少读一个字节(若无新数据,会阻塞)
socket.readall(id) 从一个socket上读所有的数据,知道socket主动断开,或在其他coroutine用socket.close关闭它
socket.readline(id,sep) 从一个socket上读一行数据,sep指行分割符,默认的sep为"/n".读到的字符串是不包含这个分割符的.
socket.block(id)等待一个socket可读
socket api 中有两个不同的写操作.对应skynet为每个socket设定的两个写队列,通常我们只需要用:
socket.write(id,str)把一个字符串置入正常的写队列,skynet框架会在socket可写时发送它.
但同时skynet还提供一个低优先级的写操作(如果你不需要这个设定,可以不使用它):
socket.lwrite(id,str)把字符串写入低优先级队列,如果正常的写队列还有写操作未完成时,低优先级队列上的数据永远不会被发出.只有在正常些队列为空时,才会处理低优先级队列.但是,每次写的字符串都可以看成原子操作,不会只发送一半,然后转去发送正常写队列的数据.
对于服务器,通常我们需要监听一个端口,并转发某个接入连接的处理权,那么可以用如下API:
socket.listen(address,port)监听一个端口,返回一个id,供start使用
socket.start(id,accept) accept是一个函数,每当一个监听的id对应的socket上有连接接入的时候,都会调用accept函数,这个函数会得到接入连接的id以及ip地址,你可以做后续操作.
每当accept函数获得一个新的socket id后,并不会立即收到这个socket 上的数据,这是因为,我们有时会希望把这个socket的操作权转让给别的服务区处理.
socket 的 id 对于整个 skynet 节点都是公开的,也就是说,你可以把id 这个数字通过消息发送给其它服务,其他服务也可以去操作它,任何一个服务只有在调用socket.start(id) 之后,才可以收到这个socket上的数据,skynet框架是根据调用start 这个 api 的位置来决定把对应的socket 上的数据转发到哪里去的.
向一个socket id 写数据也需要先调用start,但写数据不限制在调用start的同一个服务中,也就是说,你可以在一个服务中调用start,然后在另一个服务中向其写入数据,skynet可以保证一次write调用的原子性,即,如果你有多个服务同事向一个socket id 写数据,每个写操作的传不会被分割开.
socket.abandom(id) 清楚socket id 在本服务内的数据结构,但并不关闭这个socket,这可以用于你把id发给其他服务,以转交socket的控制权.
socket.warning(id,callback) 当id 对应的socket上待发的数据超过1M字节后,系统将回调callback以示警告,function callback(id,size)回调函数接受两个参数id和size,size的单位是k.如果你不设回调,那么将每增加64k后利用skynet.error写一行错误信息.
UDP
skynet为udp协议做了有限的支持,和tcp协议不同,udp协议不需要阻塞读取,这是因为dup是不可靠协议,无法预期下一个读到的数据包是什么(协议允许乱序和丢包),所以skynet的dup协议封装采用的是callback方式.
你可以用socket.udp 这个API创建一个udo handle,并给它绑定一个callback函数.当这个handle收到udp消息时,callback 函数将被触发.
socket.udp(function(str,from),address,port):id
第一个参数是一个callback函数,它会收到两个参数,str是一个字符串即收到的包内容,from是一个表示消息来源的字符串用于返回这条消息,(见socket.sendto)
第二个参数是一个字符串表示绑定的ip地址.如果你不写,默认为ipv4的0.0.0.0
第三个参数是一个数字,表示绑定的端口,如果不写或转0,这表示仅创建一个udp handle(用于发送),但不绑定固定端口.
这个函数会返回一个handle id.
socket.udp_connect(id,address,port,callback)
可以给一个udp handle设置一个默认的发送目的地址,当你用socket.udp创建出一个非监听状态的handle时,设置目的的地址非常有用,因为你很难有别的方法获得一个有效的供socket.sendto使用的地址串,这里callback是可选项,通常你应该在socket.udp创建出handle时就设置好callback函数.但有时,handle并不是当前service创建二十由别处创建出来的,这种情况,你可以用socket.start 重设handle 的所有权,并用这个函数设置callback函数
设置完默认的目的地址后,之后你就可以用wocket.write来发送数据包.
注:handle 只能属于一个service,当一个handle归属一个service 时,skynet框架将对应的网络消息转发给它,向一个handle发送网络数据包则不需要当前service拥有这个handle.
socket.sendto(id,from,data)
向一个网络地址发送一个数据包.第二个参数from即是一个网络地址,这是一个string,通常由callback函数生成,你无法自己构建一个地址串,但你可以把callback函数中得到的地址保存起来以后使用,发送的内容是一个字符串data.
这个字符串可以用socket.udp_address(from):address port 转换为可读的ip 地址和端口,用于记录.
通过阅读实现代码lualib/socket.lua,你可以更清楚的了解socket api是如何工作的,你也可能会发现一些并未在上面列出的api,它们可能在未来的版本中废弃.
如果你需要一个网管榜你接入大量连接并转发它们到不同的地方处理,.service/gate.lua 可以直接使用,同事也是用于了解skynet的socket模块如何工作的不错的参考,它还有一个功能近似的,但是全部用C编写的版本service-src/service_gate.c
如果仅仅是了解socket api 的基本用法,以及发件一个简单的服务器,请参考test/testsocket.lua, udp部分见test/testudp/lua
域名查询
在skynet的底层,当使用域名而不是ip时,由于调用了系统api getaddrinfo,有可能阻塞住整个socket线程(不仅仅是阻塞当前服务,而是阻塞整个skynet节点的网络消息处理),虽然大多数情况下,我们并不需要向外主动建立连接,但如果你使用了雷士httpc这样的模块以域名形式向外请求时,一定要关注这个问题.
skynet暂时不打算在底层实现非阻塞的域名查询,但提供了一个上层模块来辅助你解决dns查询时造成的线程阻塞问题
local dns = require "dns"
这样可以加载这个模块,它有两个方法.
在使用前,必须设置snd服务器.
dns.server(ip,port) : port的默认值为53,如果不填写ip的话,将从/etc/resolv.conf 中找到合适的ip.
dns.resolve(name,ipv6): 查询name对应的ip,如果ipv6为true,则下旬ipv6地址,默认为false.如果查询失败将抛出异常,成功则返回ip,以及一张包含有所有的ip的table.
dns.flush(): 默认情况下,模块会根据TTL值cache查询结果,在查询超时的情况下,也可能返回之前的结果.dns.flush()可以用来清空cache.注意:cache保存在调用者的服务中,并非针对整个skynet进程,所以,推荐写一个独立的dns查询服务统一处理dns查询.
你可以阅读test/testdns.lua看看范例,同时,这个模块也可以作为在skynet中使用udp的范例.