6-发起网络监听

新入门skynet系列视频b站网址 https://www.bilibili.com/video/BV19d4y1678X

lua应用层是怎么发起监听的

在具体讨论前,我们简单的讨论一下skynet的网络部分。

skynet网络线程大体上是处理两部分内容。

  • 处理系统的网络事件,比如发现新连接,最终会push消息到监听服务。又或者是发现某个连接上又网络数据达到。
  • 处理上层的请求命令。这个请求主要是我们lua层发起的。lua层把请求写入管道的一端,然后网络线程从管道的另一头把这些请求读出来,然后进行处理。比如我们主动发起请求监听的命令。最终也可能会push消息到某个服务

ok,开始讨论监听。

监听的主要目的是发现外部网络发起的请求连接。比如一个玩家发起连接请求。我们底层大致是这样发现的:

  • 在底层创建一个监听socket,进行监听的各种相关设置。然后加入epoll对象中。
  • 最终我们网络线程,会不断的查询这个监听socket上面是否有事件发生,如果有,就表示有新的连接进来了。

下面就是讨论lua层和底层怎么协调把这个监听建立起来的。

image-20220909083804861

最终水管里面的水 其实都是一个个新连接。也即是说有新连接来了,一定会先告诉监听服务。

image-20220527171324675

		local listenid = socket.listen("127.0.0.1", 8001)
	

		socket.start(listenid , function(id, addr)--id 是新连接的id, addr是新连接的发起者ip
        
			print("connect from " .. addr .. " " .. id)
	
		end)

上面是我们实际代码中监听网络的基本套路。过程主要分为两步

  • socket.listen

  • socket.start

下面是分析。注意代码有删减。

1- socket.listen

//lua-socket.c

LUAMOD_API int
luaopen_skynet_socketdriver(lua_State *L) {
	luaL_checkversion(L);
	luaL_Reg l[] = {
		{ "buffer", lnewbuffer },

		{ NULL, NULL },
	};
	luaL_newlib(L,l);
	luaL_Reg l2[] = {
	
		{ "listen", llisten },//这里是监听函数
	
		{ NULL, NULL },
	};
	lua_getfield(L, LUA_REGISTRYINDEX, "skynet_context");
	struct skynet_context *ctx = lua_touserdata(L,-1);
	if (ctx == NULL) {
		return luaL_error(L, "Init skynet context first");
	}

	luaL_setfuncs(L,l2,1);

	return 1;
}
-- socket.lua

local skynet = require "skynet"
local driver = require "skynet.socket" --这里会调用 luaopen_skynet_socketdriver ,最后返回一个库

function socket.listen(host, port, backlog)--next
	if port == nil then
		host, port = string.match(host, "([^:]+):(.+)$")
		port = tonumber(port)
	end
	return driver.listen(host, port, backlog)--开始调用c层代码 next
end

注意 行4 打开了一个c库。实际上你可以认为 行4 调用 lua-socket.c里面的 luaopen_skynet_socketdriver函数。

接下来我们从 行6 开始分析。最终调用 行11的 driver.listendriver.listen 其实主要是把 监听加入epoll,但是没有立即开启可读监听事件。具体做的事情是

  • 向skynet申请槽位,调用操作系统api listen。
  • 把监听请求写入管道
  • 网络线程从管道读取请求,并把槽位对应的socket结构体填充,并把监听加入到epoll

下面跟踪代码

static int
llisten(lua_State *L) {//next
	const char * host = luaL_checkstring(L,1);
	int port = luaL_checkinteger(L,2);
	int backlog = luaL_optinteger(L,3,BACKLOG);
	struct skynet_context * ctx = lua_touserdata(L, lua_upvalueindex(1));//ctx代表发起监听的服务
	int id = skynet_socket_listen(ctx, host,port,backlog);//next
	if (id < 0) {
		return luaL_error(L, "Listen error");
	}

	lua_pushinteger(L,id);
	return 1;
}
int 
skynet_socket_listen(struct skynet_context *ctx, const char *host, int port, int backlog) {//next
	uint32_t source = skynet_context_handle(ctx);//获取发起监听服务的地址标识
	return socket_server_listen(SOCKET_SERVER, source, host, port, backlog);//next
}

上面最终调用了 socket_server_listen,继续看下面 行31

static int
reserve_id(struct socket_server *ss) {//预定一个32位id, 相当于向ss申请预留一个槽位. 
	int i;
	for (i=0;i<MAX_SOCKET;i++) {//每次查找最大次数是 MAX_SOCKET=pow(2,16)
		int id = ATOM_FINC(&(ss->alloc_id))+1;
		if (id < 0) {//32位int的最大正整数是0x7fffffff ,之后再加1 就变负数了
			id = ATOM_FAND(&(ss->alloc_id), 0x7fffffff) & 0x7fffffff;//置零 
		}
		struct socket *s = &ss->slot[HASH_ID(id)];
		int type_invalid = ATOM_LOAD(&s->type);
		if (type_invalid == SOCKET_TYPE_INVALID) {//是空闲的
			if (ATOM_CAS(&s->type, type_invalid, SOCKET_TYPE_RESERVE)) {//设置为预留
				s->id = id;
				s->protocol = PROTOCOL_UNKNOWN;
				// socket_server_udp_connect may inc s->udpconncting directly (from other thread, before new_fd), 
				// so reset it to 0 here rather than in new_fd.
				ATOM_INIT(&s->udpconnecting, 0);
				s->fd = -1;
				return id;
			} else {
				// retry
				--i;
			}
		}
	}
	return -1;
}


int //next
socket_server_listen(struct socket_server *ss, uintptr_t opaque, const char * addr, int port, int backlog) {
	int fd = do_listen(addr, port, backlog);//调用操作系统提供的socket api,这里返回的fd是操作系统监听socket
	if (fd < 0) {
		return -1;
	}
	struct request_package request;
	int id = reserve_id(ss);//预定一个id,这个id是skynet分配的,不同于系统分配的socket fd
	if (id < 0) {
		close(fd);
		return id;
	}
	request.u.listen.opaque = opaque;//发起监听服务的地址
	request.u.listen.id = id;//
	request.u.listen.fd = fd;//操作系统监听socket
	send_request(ss, &request, 'L', sizeof(request.u.listen));//把请求写入管道 next
	return id;
}

这个函数主要做的事情是,

  • 调用操作系统网络监听api完成监听。
  • 向skynet申请一个槽位 。 这个槽位是skynet管理的。每创建一个socket都会有一个对应的槽位,这个槽位对应一个skynet自定义的一个socket结构。
  • 往管道里写入一个类型为 L 的请求 。

下面我们主要看看写管道的过程。send_request这个函数主要是在当前线程里把请求写入管道,网络线程会适时从管道中读取出来处理。看 下面 行13

struct request_package {
	uint8_t header[8];	// 6 bytes dummy
	union {
		struct request_close close;
		struct request_listen listen;
	
	} u;
	uint8_t dummy[256];
};


static void
send_request(struct socket_server *ss, struct request_package *request, char type, int len) {//next
	request->header[6] = (uint8_t)type;
	request->header[7] = (uint8_t)len;
	const char * req = (const char *)request + offsetof(struct request_package, header[6]);//header是结构体request_package的成员变量
	for (;;) {//发送的数据在内存中排序依次是 [一字节]type   [一字节]len   [sizeof(request.u.listen]字节request.u.listen
		ssize_t n = write(ss->sendctrl_fd, req, len+2);
		if (n<0) {
			if (errno != EINTR) {
				skynet_error(NULL, "socket-server : send ctrl command error %s.", strerror(errno));
			}
			continue;
		}
		assert(n == len+2);
		return;
	}
}

image-20220909100828484

上面 行16 req变量表示的地址 是request的起始地址再偏移6个字节。也就是说request的前面六个字节暂时是没有用到的,可能是将来作为扩展用。

上面 行18 最终调用系统api write把请求数据写入了管道.以上代码就是lua层调用监听时,在当前线程里面的做的事情。

我们看到最后是把请求监听请求写入管道了。而网络线程会在合适的时机在管道的接收端读取请求。

下面是网络线程处理管道请求的分析。 socket_server_poll是处理管道消息的地方。

image-20220527084514069

上图中 方框的部分就是具体处理管道消息的地方。先看 has_cmd.

static int
has_cmd(struct socket_server *ss) {
	struct timeval tv = {0,0};//设置为非阻塞立即返回检查模式
	int retval;

	FD_SET(ss->recvctrl_fd, &ss->rfds);//监听可读事件

	retval = select(ss->recvctrl_fd+1, &ss->rfds, NULL, NULL, &tv);//判断管道里面是否有数据
	if (retval == 1) {//说明有可读事件发生
		return 1;
	}
	return 0;
}

has_cmd是通过 系统api select函数 判断管道里面是否有数据.如果管道里面有数据,则调用 ctrl_cmd 处理。

// return type
static int
ctrl_cmd(struct socket_server *ss, struct socket_message *result) {//next
	int fd = ss->recvctrl_fd;
	// the length of message is one byte, so 256+8 buffer size is enough.
	uint8_t buffer[256];
	uint8_t header[2];
	block_readpipe(fd, header, sizeof(header));//读取管道数据
	int type = header[0];
	int len = header[1];//消息的长度用一个字节保存 可以表示的最大值是256
	block_readpipe(fd, buffer, len);//读取管道数据
	// ctrl command only exist in local fd, so don't worry about endian.
	switch (type) {
	
	case 'L':
		return listen_socket(ss,(struct request_listen *)buffer, result);//处理监听请求 next
	
	return -1;
}

ctrl_cmd 把管道数据读取出来,然后根据请求类型,转交给 listen_socket 处理。看下面 行35.

static struct socket *
new_fd(struct socket_server *ss, int id, int fd, int protocol, uintptr_t opaque, bool reading) {
	struct socket * s = &ss->slot[HASH_ID(id)];
	assert(ATOM_LOAD(&s->type) == SOCKET_TYPE_RESERVE);

	if (sp_add(ss->event_fd, fd, s)) {//加入epoll 注意跟s做了一个关联.
		ATOM_STORE(&s->type, SOCKET_TYPE_INVALID);
		return NULL;
	}

	s->id = id;//可以认为是skynet分配的槽位id. 实际上 HASH_ID(id) 才是真正的槽位索引
	s->fd = fd;//系统fd
	s->reading = true;//监听读事件开关
	s->writing = false;//监听写事件开关
	s->closing = false;
	ATOM_INIT(&s->sending , ID_TAG16(id) << 16 | 0);//ID_TAG16是取出高16位
	s->protocol = protocol;
	s->p.size = MIN_READ_BUFFER;//初始设置可读缓存size
	s->opaque = opaque;//关联的服务地址
	s->wb_size = 0;
	s->warn_size = 0;
	check_wb_list(&s->high);
	check_wb_list(&s->low);
	s->dw_buffer = NULL;
	s->dw_size = 0;
	memset(&s->stat, 0, sizeof(s->stat));
	if (enable_read(ss, s, reading)) {//设置可读监听开关 此时reading是false 所以并没有开启可读监听
		ATOM_STORE(&s->type , SOCKET_TYPE_INVALID);
		return NULL;
	}
	return s;
}

static int//next
listen_socket(struct socket_server *ss, struct request_listen * request, struct socket_message *result) {
	int id = request->id;//可以认为是预分配的槽位
	int listen_fd = request->fd;
	struct socket *s = new_fd(ss, id, listen_fd, PROTOCOL_TCP, request->opaque, false);//注意最后一个参数false表示不监听可读事件

	ATOM_STORE(&s->type , SOCKET_TYPE_PLISTEN);//注意这里类型是 SOCKET_TYPE_PLISTEN
	return -1;//返回-1 表示此次命令处理结束 可以开始处理下一个命令了
    
}

行38 填充了槽位关联socket结构体。并把监听socket加入到epoll。到此soket.listen监听请求在网络线程的处理就是结束了。也就是说 在lua层调用 socket.listen只返回了一个id,c层也没有其他通知lua层的消息。但注意 此时c层分配的监听socket的类型是 SOCKET_TYPE_PLISTEN 而不是 SOCKET_TYPE_LISTEN。即当前是 预监听状态,而不是监听状态。

2- socket.start

回顾最开始lua层的代码。

	local listenid = socket.listen("127.0.0.1", 8001)


	socket.start(listenid , function(id, addr)--id 是新连接的id, addr是新连接的发起者ip

        print("connect from " .. addr .. " " .. id)

    end)

上面的 行1 已经完成了。下面看 socket.start(id, func) 。其实socket.start主要做的事情就是告诉底层监听将在哪个服务安家,底层知道后,会回应lua层,这样监听才算正式开启。有人要说,调用 socket.listen 的时候,底层不是已经知道lua层的监听服务是在哪里了吗,怎么还要调用socket.start再次告诉底层监听所在的服务?这里我们调用socket.listen和socket.start是在同一个服务,实际上,socket.start才是决定了监听最终所在的位置。

function socket.start(id, func) --注意这里的id是监听id
	driver.start(id)//next
	return connect(id, func)
end

我们先关注driver.start(id) .后面再回来看lua层的connect函数

driver.start(id)

static int
lstart(lua_State *L) {
	struct skynet_context * ctx = lua_touserdata(L, lua_upvalueindex(1));
	int id = luaL_checkinteger(L, 1);
	skynet_socket_start(ctx,id);//next
	return 0;
}
void 
skynet_socket_start(struct skynet_context *ctx, int id) {
	uint32_t source = skynet_context_handle(ctx);//调用socket.start的服务地址
	socket_server_start(SOCKET_SERVER, source, id);//next
}
void
socket_server_start(struct socket_server *ss, uintptr_t opaque, int id) {
	struct request_package request;
	request.u.resumepause.id = id;//监听id
	request.u.resumepause.opaque = opaque;//调用socket.start的服务地址
	send_request(ss, &request, 'R', sizeof(request.u.resumepause));//next
}

上面又再次使用了send_request把请求写入管道。我们可以根据请求类型 ‘R’ 找到在网络线程处理的代码。如下 行7

// return type
static int
ctrl_cmd(struct socket_server *ss, struct socket_message *result) {

	switch (type) {
	case 'R':
		return resume_socket(ss,(struct request_resumepause *)buffer, result);//next
	
	return -1;
}

继续看 resume_socket

static int
resume_socket(struct socket_server *ss, struct request_resumepause *request, struct socket_message *result) {//next
	int id = request->id;
	result->id = id;//监听id
	result->opaque = request->opaque;//调用socket.start的服务地址
	result->ud = 0;
	result->data = NULL;
	struct socket *s = &ss->slot[HASH_ID(id)];//通过监听id槽位找到对应的skynet定义的socket

	if (halfclose_read(s)) {
		// The closing socket may be in transit, so raise an error. See https://github.com/cloudwu/skynet/issues/1374
		result->data = "socket closed";
		return SOCKET_ERR;
	}
	struct socket_lock l;
	socket_lock_init(s, &l);
	if (enable_read(ss, s, true)) {//开启可读事件监听 之前是没有开启的
		result->data = "enable read failed";
		return SOCKET_ERR;
	}
	uint8_t type = ATOM_LOAD(&s->type);//此时type是 SOCKET_TYPE_PLISTEN
	if (type == SOCKET_TYPE_PACCEPT || type == SOCKET_TYPE_PLISTEN) {
		ATOM_STORE(&s->type , (type == SOCKET_TYPE_PACCEPT) ? SOCKET_TYPE_CONNECTED : SOCKET_TYPE_LISTEN);
		s->opaque = request->opaque;
		result->data = "start";
		return SOCKET_OPEN;//注意这里的返回值,表示需要s->opaque 代表的服务push一个网络消息
	}
	// if s->type == SOCKET_TYPE_HALFCLOSE_WRITE , SOCKET_CLOSE message will send later
	return -1;
}

之前分析socket.listen 时我们说过,监听id并没有开启可读事件监听。此时 行17 开启了可读事件监听。而且当时socket的类型是预监听,行23 现在才把监听类型设置为 所谓的监听状态。注意 行26 这里最后有一个返回值 SOCKET_OPEN。

我们重新来看网络线程的处理流程。网络线程会一直循环调用skynet_socket_poll。里面又会调用 socket_server_poll。看如下代码

int 
skynet_socket_poll() {
	
	int type = socket_server_poll(ss, &result, &more);//上面分析了,这次返回值是 SOCKET_OPEN 
	switch (type) {

	case SOCKET_OPEN://监听正式启动 
		forward_message(SKYNET_SOCKET_TYPE_CONNECT, true, &result);
		break;

	}
}

注意 result是socket_message类型,也就是网络消息首先是保存在这个结构里的。

下面是 socket_server_poll处理管道消息的地方。

image-20220527111603275

所以最终会调用 forward_message(SKYNET_SOCKET_TYPE_CONNECT, true, &result);这个函数会往调用socket.start的服务发送消息 ,看下面 行10


static void
forward_message(int type, bool padding, struct socket_message * result) {//next
	struct skynet_socket_message *sm;
	size_t sz = sizeof(*sm);
	if (padding) {
		if (result->data) {//当前的data是 "start"
			size_t msg_sz = strlen(result->data);
			if (msg_sz > 128) {
				msg_sz = 128;
			}
			sz += msg_sz;
		}
	}
	sm = (struct skynet_socket_message *)skynet_malloc(sz);//分配内存 这个sz已经包括result->data占用的字节了
	sm->type = type;//SKYNET_SOCKET_TYPE_CONNECT
	sm->id = result->id;//监听id
	sm->ud = result->ud;//0
	if (padding) {
		sm->buffer = NULL;
		memcpy(sm+1, result->data, sz - sizeof(*sm));//把result->data的数据放置到sm尾部。
	} 

	struct skynet_message message;
	message.source = 0;
	message.session = 0;
	message.data = sm;
	message.sz = sz | ((size_t)PTYPE_SOCKET << MESSAGE_TYPE_SHIFT);//PTYPE_SOCKET放置在sz的高八位
	
	if (skynet_context_push((uint32_t)result->opaque, &message)) {//push消息
		// todo: report somewhere to close socket
		// don't call skynet_socket_close here (It will block mainloop)
		skynet_free(sm->buffer);
		skynet_free(sm);
	}
}

forward_message函数主要是把 socket_message 的内容转化为 skynet_socket_message。之后把 skynet_socket_message 的内容转化成skynet_message。看看这几个结构体

struct socket_message {
	int id;
	uintptr_t opaque;
	int ud;	// for accept, ud is new connection id ; for data, ud is size of data 
	char * data;
};

#define SKYNET_SOCKET_TYPE_DATA 1
#define SKYNET_SOCKET_TYPE_CONNECT 2
#define SKYNET_SOCKET_TYPE_CLOSE 3
#define SKYNET_SOCKET_TYPE_ACCEPT 4
struct skynet_socket_message {
	int type;//这里的type主要就是上面几个常量
	int id;
	int ud;
	char * buffer;
};

struct skynet_message {
	uint32_t source;
	int session;
	void * data;
	size_t sz;
};

同时注意此时的数据如下所示

image-20220623121954801

以上就是整个driver.start的分析。现在我们回到lua层。分析connect函数。

connect(id,func)

function socket.start(id, func)
	driver.start(id)
	return connect(id, func)--这里func是有新连接时的回调函数 --next
end

这个connect函数主要是进行一些初始化工作,然后挂起自己,等待唤醒。

这里socket.start(id, func)的时序是 driver.start向管道写请求,connect挂起当前线程,网络线程读取管道,处理请求,然后发送 SKYNET_SOCKET_TYPE_CONNECT 给当前服务,最后当前协程被唤醒,connect函数继续执行。

local function connect(id, func)--fun是新连接的accept回调函数
	local newbuffer
	if func == nil then
		newbuffer = driver.buffer()
	end
	local s = {
		id = id,--监听id
		buffer = newbuffer,--这里newbuffer是nil
		pool = newbuffer and {},
		connected = false,
		connecting = true,
		read_required = false,
		co = false,
		callback = func, --如果func不为nil 说明当前是监听socket
		protocol = "TCP",
	}
	assert(not socket_onclose[id], "socket has onclose callback")
	assert(not socket_pool[id], "socket is not closed")
	socket_pool[id] = s
	suspend(s)--next
	local err = s.connecting
	s.connecting = nil
	if s.connected then
		return id
	else
		socket_pool[id] = nil
		return nil, err
	end
end

每个socket对象都保存在 socket_pool中。监听socket也不例外。注意行20 挂起了当前协程。suspend(s) 里面主要做到事情就是 调用 skynet.wait(s.co)。看下面代码 行11

function skynet.wait(token)--next
	local session = c.genid() --分配一个session
	token = token or coroutine.running()
    
	suspend_sleep(session, token)--挂起协程 next
    
	sleep_session[token] = nil
	session_id_coroutine[session] = nil
end

local function suspend(s)--next
	assert(not s.co)
	s.co = coroutine.running()--保存当前协程

	skynet.wait(s.co) --next
	
	-- wakeup closing corouting every time suspend,
	-- because socket.close() will wait last socket buffer operation before clear the buffer.
	if s.closing then
		skynet.wakeup(s.closing)
	end
end

上面代码在 行15 挂起了协程。继续看下面的代码 suspend_sleep。发现最后调用 coroutine_yield "SUSPEND"

--sleep的原因分为两种 一种是skynet.sleep 一种是skynet.wait 
local function suspend_sleep(session, token)--next
	
	session_id_coroutine[session] = running_thread
	assert(sleep_session[token] == nil, "token duplicative")
	sleep_session[token] = session --标记协程为睡眠. 特别的是以协程作为key. 将来时机成熟时,通过 skynet.wakeup(token) 唤醒协程 其实token不一定是协程 只要是可以表示唯一的值即可 因为最终是token->session->co

	return coroutine_yield "SUSPEND" --让出当前协程
end

通过上面的分析。当前协程已经挂起了。等待被唤醒。实际上整个监听的时序如下

image-20220527171324675

继续看。当收到 网络消息 SKYNET_SOCKET_TYPE_CONNECT 后,lua层具体是怎么处理的?

image-20220908165405441

其实socket.lua加载的时候就通过 skynet.register_protocol 注册了 网络消息类型 的回调函数 dispatch。还记得吗,当lua层处理消息,会调用 skynet.dipath_message,然后获取一个协程并设置任务函数f,这个f 就是下面 行5 设置的dispatch

skynet.register_protocol {
	name = "socket",
	id = skynet.PTYPE_SOCKET,	-- PTYPE_SOCKET = 6
	unpack = driver.unpack,--把struct skynet_socket_message内的数据提取出来 即 type id ud buffer 注意type不同 后面参数的意义也是不同的
	dispatch = function (_, _, t, ...) --参数分别是: session source SKYNET_SOCKET_TYPE_CONNECT ...
		socket_message[t](...) --...表示的参数是 监听id 0 "start"
	end
}

注意上面 driver.unpack 是把 c层的 skynet_socket_message 的内容提取出来给dispatch当作参数用 ,看下面的提取代码

static int
lunpack(lua_State *L) {
	struct skynet_socket_message *message = lua_touserdata(L,1);
	int size = luaL_checkinteger(L,2);

	lua_pushinteger(L, message->type);//SKYNET_SOCKET_TYPE_CONNECT
	lua_pushinteger(L, message->id);//监听id
	lua_pushinteger(L, message->ud);//0
	if (message->buffer == NULL) {//此时buffer为NULL
		lua_pushlstring(L, (char *)(message+1),size - sizeof(*message));//这里当前是 "start"
	} 
	
	return 4;//总共四个返回值: SKYNET_SOCKET_TYPE_CONNECT 监听id 0 "start"
}

也就是说lua层此时的基本处理流程是 skynet.dispatch_message--->raw_dispatch_message--->dispatch。下面是注册的收到 SKYNET_SOCKET_TYPE_CONNECT 网络消息类型 时调用的函数



-- SKYNET_SOCKET_TYPE_CONNECT = 2
socket_message[2] = function(id, _ , addr)
	local s = socket_pool[id] --通过监听id找到对应的s对象
	if s == nil then
		return
	end
	-- log remote addr
	if not s.connected then	-- resume may also post connect message
		s.connected = true --设置为连接成功 实际上是完成了监听
		wakeup(s)--next 
	end
end

上面是 addr 参数其实是"start" 根本不是地址

local function wakeup(s)--next
	local co = s.co
	if co then
		s.co = nil
		skynet.wakeup(co)--把之前等待的协程加入唤醒队列 next
	end
end
function skynet.wakeup(token)--next
	if sleep_session[token] then --已经处于sleep状态中
		tinsert(wakeup_queue, token)--加入唤醒队列
		return true
	end
end

注意 标记睡眠状态是以协程为key ,唤醒时以协程为key查找对应协程是否睡眠

这里收到网络消息 SKYNET_SOCKET_TYPE_CONNECT 后,co_create了一个协程y,然后在协程中执行上面的dispatch代码。dispatch会把之前 因调用connect函数挂起的协程x 加入了唤醒队列。等当前协程y让出后,之前挂起的那个协程x就有机会继续执行了。也即是继续执行connect函数

注意最终监听底层对应的socket的type是SOCKET_TYPE_LISTEN.虽然最后连接成功发送给监听服务的网络消息是 SKYNET_SOCKET_TYPE_CONNECT

posted @ 2022-12-10 20:06  程序员阿钢  阅读(454)  评论(0编辑  收藏  举报