skynet 网络底层

最近看了云风的skynet框架,感觉获益良多。再次为云风大神的无私奉献致谢。下面是我看skynet框架时,看底层网络层时的一些心得体会,如果有写错的地方欢迎指正,本文是我的原创,如果转载请注明。

skynet的网络底层 采用了 epoll模型 (linux平台下),支持 linux,apple,freebsd,openbsd,netbsd等平台。

        socket_epoll.h --linux平台下相关接口的实现
        socket_kqueue.h --另外几个平台的接口实现
        socket_poll.h --poll接口声明

相关接口:
bool sp_invalid(poll_fd fd) --判断句柄fd是否是有效句柄,为-1是无效句柄
poll_fd sp_create() -- 创建一个epoll,返回对应的句柄
int sp_add(poll_fd fd, int sock, void *ud) -- 将sock句柄,添加到epoll中 epoll对应的句柄为fd,用户数据为ud,默认是关注in事件,返回0代表成功,1为失败
void sp_del(poll_fd fd, int sock); -从epoll fd中移除sock;
void sp_write(poll_fd, int sock, void *ud, bool enable);将epoll fd中已有的sock设置它是否关注out事件
int sp_wait(poll_fd, struct event *e, int max);--等待epoll,e为一个数组,最大长度为max,它返回wait到的相关事件。返回的是真实的事件个数。
void sp_nonblocking(int sock);--设置sock为非阻塞

skynet网络底层的具体逻辑在socket_server.h,socket_server.c这两个文件中编写。下面简要说下网络底层的简要流程。
  • 控制流程;对网络层的相关操作都是通过命令通知,网络模块创建时会创建一个管道,外部通过管道写入要请求的命令,网络模块的线程每次轮训都会判断管道是否有命令,通过读取请求命令,完成相对应的一些网络操作。
  • 读取流程;只针对tcp,每次socket有可读事件,就会读取 一定长度 的数据,然后通过有数据可读的网络事件给lua层逻辑,lua层逻辑接受到这个事件后会从buffer_pool内存池中获得一个空闲的 buf 而且长度最为相近的buf 存储这个数据,并且将这个数据拼接到对应的socket 的 socket_buffer 列表后面。而实际上socket.lua 里面定义的读取函数都是通过读取socket_buffer 列表中的数据获得的。
  • 写入流程;只针对tcp,lua 逻辑层 通过 控制流程的请求命令 request_send 发送数据给网络底层,网络底层会根据id将要发送的数据加入到 对应socket 的 两个缓存链表 (高或低)中。优先级高的数据会先存于 '高'的缓存链表。当对应的socket有可写事件时,会优先将 高的缓存列表 发送出去,然后才发 低的缓存列表 的数据。

下面详细分析下各流程是怎么实现的
数据结构的定义
struct write_buffer {  //这结构是用保存要发送的数据 一个缓存节点
	struct write_buffer * next; //由于是链表结构,这个用于指向下个节点
	void *buffer;//要发送的内存数据块    
	char *ptr;//已发送数据的偏移位置,也就是下次发送从这开始
	int sz;//剩余多少数据没有发送
	bool userobject;//是否是用户数据
	uint8_t udp_address[UDP_ADDRESS_SIZE];	//udp专用,这个数据发送的目标地址
};
//tcp 协议udp_address 项数据是无用的,所以分配内存时 就不会分配这块的内存
#define SIZEOF_TCPBUFFER (offsetof(struct write_buffer, udp_address[0]))
#define SIZEOF_UDPBUFFER (sizeof(struct write_buffer))
//发送数据的缓存列表
struct wb_list {
	struct write_buffer * head;
	struct write_buffer * tail;
};

struct socket {
	uintptr_t opaque;//保存使用这个socket对应的服务的地址,因为有些网络事件需要返回到使用这个socket的服务
	struct wb_list high;//发送缓存链表,要发送的数据都会优先存这两个列表,high是优先发送的
	struct wb_list low;
	int64_t wb_size;//两个缓存列表待发送的数据长度总和
	int fd;	//socket句柄
	int id;	//对应 socket_server 中一个socket数组的下标 可以理解为连接id,供上层逻辑使用。
	uint16_t protocol;//指定是udp还是 tcp 协议
	uint16_t type;//socket的相关状态
	union {
	int size;//下次从 fd中读取数据的长度,是变动的,初始为MIN_READ_BUFFER = 64,根据实际读取到的长度 扩张和收缩
	uint8_t udp_address[UDP_ADDRESS_SIZE];//udp绑定的地址
	} p;
};

struct socket_server {
	int recvctrl_fd;//通过管道piple创建出来的一对句柄,用于向网络底层传输请求消息
	int sendctrl_fd;
	int checkctrl;//标识当前是否需要检测recvctrl_fd内是否有请求消息可读
	poll_fd event_fd;//创建epoll返回的对应句柄
	int alloc_id;//socket 的id 是通过这个id一定算法分配出来的 需要了解算法的话去看reserve_id函数
	int event_n;//保存每次从epoll中wait出来的真实事件个数
	int event_index;//当前处理到第几个epoll事件
	struct socket_object_interface soi;
	struct event ev[MAX_EVENT];//保存epoll 中wait出来的事件
	struct socket slot[MAX_SOCKET];//socket数组
	char buffer[MAX_INFO];//保存一些控制命令操作的相关临时数据,返回结果有相关指针指向该处
	uint8_t udpbuffer[MAX_UDP_PACKAGE];
	fd_set rfds;//主要给recvctrl_fd用,让其通过select函数判断是否有数据可读
};
下面可以看 网络请求消息的定义
 
/*发送给网络层的请求消息,不同的请求使用联合体内对应的消息体,从第六个字节开始发送,header中的7,8 字节分别代表类型和长度*/
struct request_package {
	uint8_t header[8];	// 6 bytes dummy
	union {
		char buffer[256];
		struct request_open open;
		struct request_send send;
		struct request_send_udp send_udp;
		struct request_close close;
		struct request_listen listen;
		struct request_bind bind;
		struct request_start start;
		struct request_setopt setopt;
		struct request_udp udp;
		struct request_setudp set_udp;
	} u;
	uint8_t dummy[256];
};
    我们可以看lua-socket.c文件,里面里面定义了服务端 网络层相关的相关的一些接口,这些接口都是抛出给lua逻辑层使用的。对应的lua网络相关的逻辑封装在 socket.lua文件中定义。现在先分析抛出的相关接口 可以看luaopen_socketdriver函数,可以知道抛出了那些接口:
下面部分大部分都是操作读缓存的相关接口
  1. socketdriver.buffer --新建一个socket_buffer并且返回该buf
  2. socketdriver.push(socketbuf, buffer_pool, data, size) --将 在buffer_pool中找一个空闲的buf,存储data和size,并且附于socketbuf末尾
  3. socketdriver.pop(socketbuf, buffer_pool, sz) -- 从socketbuf中的内存列表,拼凑出 长度为sz的内存块并返回,socketbuf链表中内存块已经弹出的会回收到buffer_pool的第一个元素列表中
  4. socketdriver.drop(data, size) --释放data内存块
  5. socketdriver.readall(socketbuf, buffer_pool) -- 将socketbuf中的内存列表拼凑成一整块并返回,类似pop,但是这里是所有数据。
  6. socketdriver.readline(socketbuf, buffer_pool, sep) --从socketbuf中 读取以sep符号分割的内存,
  7. socketdriver.str2p(str) --将字符串转成指针并弹出
  8. socketdriver.unpack --网络底层的解包函数,一般在dispatch函数中就赋值了
这个下面一组基本都是一些网路操作,真正实现都是通过管道发送网络请求给底层,然后底层根据对应的请求才做相应的操作。
  1. socketdriver.connect(addr,port) --发送request_open请求给网络底层要求连接
  2. socketdriver.close(id) -- 发送request_close给网络底层,网络底层根据回传的id关闭对应的socket,有数据没有发送完 会先设置为 SOCKET_TYPE_HALFCLOSE 半闭合状态,等待数据都发送完后真正关闭
  3. socketdriver.shutdown(id) -- 和close类似,但是它无需等待数据发完,他会强制关闭
  4. socketdriver.listen(address, port) -- 新建一个socket句柄绑定指定地址和ip,并且开始监听,同时发送request_listen 注册到网络底层的socket池,但是不加入到epoll内,返回所在socket池下标id 
  5. socketdriver.start(id) -- 根据id找到对应的socket根据对应的状态(SOCKET_TYPE_PACCEPT 和 SOCKET_TYPE_PLISTEN 会加入epoll 关注in事件) 判断是否需要加入epoll中 这个函数一般和listen连用。为什么分开,主要是可以用户决定什么时候开始启用
  6. socketdriver.send(id,pBuf) --将数据 pBuf 添加到 id 对应的 socket对应的wb_list high队列中,等待发送,如果这个队列为空,直接发送
  7. socketdriver.lsend(id,pBuf) --类似send,但是是放入 low队列,数据发送会优先发送high队列中的数据
  8. socketdriver.bind(fd) --将一个外部的句柄 绑定到底层的 epoll中,这里看到调用的只有用绑定系统的一些句柄,例如输入输出流,用于关注流的输入输出事件
  9. socketdriver.nodelay(id) -- 将id对应的socket句柄 选项 TCP_NODELAY 设置为1
连接建立流程
服务器通过调用socket.listern接口监听对应的端口 返回 监听socket 对应的id

通过调用socket.start(id,cb)  使listern socket 的读事件开启,开始接受客户端连接。并且注册新连接进来时的处理函数cb,参数是连接的地址和 句柄。我们可以看到start函数会调用connect  下面可以看connect的实现
local function connect(id, func)
	local newbuffer
	if func == nil then
		newbuffer = driver.buffer()
	end
	local s = {
		id = id,
		buffer = newbuffer,
		connected = false,
		connecting = true,
		read_required = false,
		co = false,
		callback = func,
		protocol = "TCP",
	}
	assert(not socket_pool[id], "socket is not closed")
	socket_pool[id] = s
	suspend(s)
	local err = s.connecting
	s.connecting = nil
	if s.connected then
		return id
	else
		socket_pool[id] = nil
		return nil, err
	end
end
可以看到会根据 回调函数func来判断是否需要创建socket_buffer,就是说监听socket其实不需要 buffer,因为它只接受连接,而对应的连接socket其实需要buffer,因为需要读取来自客户端的数据,作为一个读取缓存。
另外我们可以看到有suspend(s)会挂起当前协程,其实它是需要等待request_start 网络消息的返回,如果成功会返回SKYNET_SOCKET_TYPE_CONNECT 失败会返回 SKYNET_SOCKET_TYPE_ERROR消息 我们可以看到这两个消息的处理函数。
-- SKYNET_SOCKET_TYPE_CONNECT = 2
socket_message[2] = function(id, _ , addr)
	local s = socket_pool[id]
	if s == nil then
		return
	end
	-- log remote addr
	s.connected = true
	wakeup(s)
end
成功会将connected 设置为 true, 并且重新唤醒 socket s对应的协程。

-- SKYNET_SOCKET_TYPE_ERROR = 5
socket_message[5] = function(id, _, err)
	local s = socket_pool[id]
	if s == nil then
		skynet.error("socket: error on unknown", id, err)
		return
	end
	if s.connected then
		skynet.error("socket: error on", id, err)
	elseif s.connecting then
		s.connecting = err
	end
	s.connected = false
	driver.shutdown(id)

	wakeup(s)
end
失败的话 connecting将是一些错误信息,同时 会shutdown掉对应的socket.

其实是的网络底层 触发当有新连接进来时会传一个消息给逻辑层,我们可以看到socket.lua中对应的处理
-- SKYNET_SOCKET_TYPE_ACCEPT = 4
socket_message[4] = function(id, newid, addr)
	local s = socket_pool[id]
	if s == nil then
		driver.close(newid)
		return
	end
	s.callback(newid, addr)
end
可以知道他会调用我们刚刚注册的cb函数。


当连接建立时 还需 针对 连接进来的 连接id 调用一次socket.start(id) 不用传cb,只有针对监听socket才需要传回调函数,普通的客户端socket不用传,内部会根据cb是否为空,而确定id是监听socket的id还是普通客户端的socket,如果是普通socket的id 会为这个socket新建一个socket_buffer,用于当做读取缓存。对
 

 

你可能感兴趣的:(skynet 网络底层)