因为上一篇写创建tcp服务端已经把很多重要接口分析过了,这一篇会写的比较简单。不过在分析代码前,我想先说下非阻塞connect,因为这是本篇博客的核心。
以下的文字摘抄自《UNIX网络编程》:
当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三路握手继续进行。我们接着使用select检测这个连接或成功或失败的已建立条件。既然使用select等待连接的建立(在libhv中未必是select,也可能是poll、epoll等其他io事件监视器),我们可以给select指定一个时间限制,使得我们能够缩短connect的超时(libhv使用定时器实现的该功能),许多实现有着从75秒钟到数分钟的connect超时时间,应用程序有时想要一个更短的超时时间。非阻塞connect虽然听似简单,却有一些我们必须处理的细节。尽管套接字是非阻塞的,如果连接的服务器在同一个主机上,那么当我们调用connect时,连接通常立刻建立(我在自己的环境测试,感觉即使是在一个主机,也无法立刻建立)。我们必须处理这种情况。源自Berkeley的实现(和POSIX)有关于select和非阻塞connect的以下两个规则:(1)当连接成功建立时,描述符变为可写(2)当连接建立遇到错误时,描述符变为即可读又可写。一个TCP套接字上发生某个错误时,这个待处理错误总是导致该套接字变为既可读又可写。
如果描述符变为可读或可写,我们就调用getsockopt取得套接字的待处理错误(使用SO_ERROR套接字选项)。如果连接成功建立,该值将为0。如果连接建立发生错误,该值就是对应连接错误的errno值。我们之前说过,套接字的各种实现以及非阻塞connect会带来移植性问题。首先,调用select之前有可能连接已经建立并有来自对端的数据到达。这种情况下即使套接字上不发生错误,套接字也是即可读又可写的,这和连接建立失败情况下套接字的读写条件一样。其次,我们不能假设套接字的可写(而不可读)条件是select返回套接字操作成功条件的唯一方法,下一个移植性问题就是怎么判断连接建立是否成功。张贴到Usenet上的解决方法各式各样。这些方法可以取代getsockopt调用。
- 调用getpeername替代getsockopt。如果getpeername以ENOTCONN错误失败返回,那么连接建立已经失败,我们必须接着以SO_ERROR调用getsockopt取得套接字上待处理的错误。
- 以值为0的长度参数调用read。如果read失败,那么connect已经失败,read返回的errno给出了连接失败的原因。如果连接建立成功,那么read应该返回0.
- 再调用connect一次。它应该失败,如果错误是EISCONN,那么套接字已经连接,也就是说第一次连接已经成功。
ok,开始创建客户端了。。。。
与创建tcp服务端类似,创建一个客户端的步骤如下:
hloop_t* loop = hloop_new(HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); //创建一个loop
hio_t* sockio = hloop_create_tcp_client(loop, host, port, on_connect);
hio_setcb_close(sockio, on_close);
hio_setcb_read(sockio, on_recv);
hio_set_readbuf(sockio, recvbuf, RECV_BUFSIZE);
hloop_run(loop);
hloop_free(&loop);
hloop_new上一篇已经分析过了,创建一个loop,然后通过hloop_create_tcp_client,创建一个与服务端通信的io结构体。
hio_t* hloop_create_tcp_client (hloop_t* loop, const char* host, int port, hconnect_cb connect_cb) {
sockaddr_u peeraddr;
memset(&peeraddr, 0, sizeof(peeraddr));
//填充服务端网络地址结构体
int ret = sockaddr_set_ipport(&peeraddr, host, port);
if (ret != 0) {
//printf("unknown host: %s\n", host);
return NULL;
}
int connfd = socket(peeraddr.sa.sa_family, SOCK_STREAM, 0);
if (connfd < 0) {
perror("socket");
return NULL;
}
//创建io结构体
hio_t* io = hio_get(loop, connfd);
assert(io != NULL);
//设置对端地址信息
hio_set_peeraddr(io, &peeraddr.sa, sockaddr_len(&peeraddr));
hconnect(loop, connfd, connect_cb);
return io;
}
除了hconnect,其他都比较简单,hio_get在上一篇博客中也分析过了
hio_t* hconnect (hloop_t* loop, int connfd, hconnect_cb connect_cb) {
hio_t* io = hio_get(loop, connfd);
assert(io != NULL);
//设置connect回调
if (connect_cb) {
io->connect_cb = connect_cb;
}
hio_connect(io);
return io;
}
int hio_connect(hio_t* io) {
//尝试连接服务端
int ret = connect(io->fd, io->peeraddr, SOCKADDR_LEN(io->peeraddr));
#ifdef OS_WIN
if (ret < 0 && socket_errno() != WSAEWOULDBLOCK) {
#else
if (ret < 0 && socket_errno() != EINPROGRESS) {
#endif
perror("connect");
hio_close(io);
return ret;
}
//如果直接连接成功,调用回调函数
if (ret == 0) {
// connect ok
__connect_cb(io);
return 0;
}
//如果没有直接成功,设置超时时间,等待连接成功
int timeout = io->connect_timeout ? io->connect_timeout : HIO_DEFAULT_CONNECT_TIMEOUT;
io->connect_timer = htimer_add(io->loop, __connect_timeout_cb, timeout, 1);
io->connect_timer->privdata = io;
io->connect = 1;
return hio_add(io, hio_handle_events, HV_WRITE);
}
根据上一篇博客的说明,描述符已经被设置为非阻塞的,根据博客开始的文字说明,调用connect很可能无法直接成功,需要等待一段时间。为了防止一直不成功,这里设置了一个connect的定时器,如果在定时时间内没有连接成功,__connect_timeout_cb回调函数会被调用。因为需要等待连接成功,所以将该事件加入到io事件监视器中,由loop等待connect成功(其实就是文字说明中的select实现的功能)。而connect的成功会使描述符成为可写的。所以这里hio_add将该事件加入io事件监视器并设置感兴趣的事件类型为可写HV_WRITE,注册了回调函数hio_handle_events,等到可写触发时,该回调函数会被调用。设置完这些后,就从hloop_create_tcp_client返回。
调用hloop_create_tcp_client获得了与服务端通信的io结构体,之后设置了一些回调函数,设置了可读缓冲区。这个缓冲区用户可以自己设置,如果不设置会使用在hloop_new中初始化的那个缓冲区,可以参考上一篇博客。
设置完后,调用hloop_run等待事件的发生。关于hloop_run的细节也参考之前的博客。根据上面的分析,其实已经有两个事件加入到loop了,一个是尝试建立连接的io事件,一个是定时器事件。那么就有两种可能,第一种是io事件触发,第二种是定时器事件触发。假设在定时器到期之前,连接建立成功,即可写事件触发,回调上面注册的回调函数hio_handle_events:
static void hio_handle_events(hio_t* io) {
if ((io->events & HV_READ) && (io->revents & HV_READ)) {
if (io->accept) {
nio_accept(io);
}
else {
nio_read(io);
}
}
if ((io->events & HV_WRITE) && (io->revents & HV_WRITE)) {
// NOTE: del HV_WRITE, if write_queue empty
if (write_queue_empty(&io->write_queue)) {
iowatcher_del_event(io->loop, io->fd, HV_WRITE);
io->events &= ~HV_WRITE;
}
if (io->connect) {
// NOTE: connect just do once
// ONESHOT
io->connect = 0;
nio_connect(io);
}
else {
nio_write(io);
}
}
io->revents = 0;
}
这个函数在上一篇也有分析,不过只分析了读事件,这次是写事件。假设该客户端连接上一篇博客的服务端,这时候服务端也会触发这个函数,不过服务端会调用这里的nio_accept,而本客户端调用的是nio_connect。有一个地方需要注意的是,在这里调用iowatcher_del_event清除了前面注册的HV_WRITE事件类型,原因是如果不清除HV_WRITE,之后会一直触发可写,造成busy-loop。
static void nio_connect(hio_t* io) {
//printd("nio_connect connfd=%d\n", io->fd);
socklen_t addrlen = sizeof(sockaddr_u);
//获取对端地址信息
int ret = getpeername(io->fd, io->peeraddr, &addrlen);
if (ret < 0) {
io->error = socket_errno();
printd("connect failed: %s: %d\n", strerror(socket_errno()), socket_errno());
goto connect_failed;
}
else {
addrlen = sizeof(sockaddr_u);
getsockname(io->fd, io->localaddr, &addrlen);
if (io->io_type == HIO_TYPE_SSL) {
hssl_ctx_t ssl_ctx = hssl_ctx_instance();
if (ssl_ctx == NULL) {
goto connect_failed;
}
hssl_t ssl = hssl_new(ssl_ctx, io->fd);
if (ssl == NULL) {
goto connect_failed;
}
io->ssl = ssl;
ssl_client_handshark(io);
}
else {
// NOTE: SSL call connect_cb after handshark finished
__connect_cb(io);
}
return;
}
connect_failed:
hio_close(io);
}
根据博客开始的文字描述,可以知道即使套接字是可写的,也有可能是因为有错误发生,这里的getpeername实际上也可以用来检测connect是否成功。如果成功,调用__connect_cb,这里先忽略ssl相关的内容。之前分析心跳的博客就涉及到了__connect_cb接口,当时提到这里是设置心跳和keepalive的两个位置之一,因为心跳部分之前博客分析过了,这里我把那部分代码删了,这里主要分析与连接相关的内容。
static void __connect_cb(hio_t* io) {
if (io->connect_timer) {
htimer_del(io->connect_timer);
io->connect_timer = NULL;
io->connect_timeout = 0;
}
if (io->connect_cb) {
io->connect_cb(io);
}
}
在__connect_cb中,会关闭之前设置的定时器,因为这个定时器就是防止connect长时间连接不成功的,这里既然connect已经成功了,当然要把这个定时器删了。删除定时器后,调用用户在调用hloop_create_tcp_client时注册的回调函数。在本次tcp客户端示例程序中,回调函数是这样的:
void on_connect(hio_t* io) {
//使能读
hio_read_start(io);
//向服务端发送一条消息
static char buf[] = "PING\r\n";
hio_write(io, buf, 6);
}
这样整个connect就成功了。
上面是io事件触发的情况,还有一种就是io事件一直没有触发,可能一直在尝试连接,那么会发生超时,这时候设置的定时器被触发,__connect_timeout_cb被调用:
static void __connect_timeout_cb(htimer_t* timer) {
hio_t* io = (hio_t*)timer->privdata;
if (io) {
char localaddrstr[SOCKADDR_STRLEN] = {0};
char peeraddrstr[SOCKADDR_STRLEN] = {0};
hlogw("connect timeout [%s] <=> [%s]",
SOCKADDR_STR(io->localaddr, localaddrstr),
SOCKADDR_STR(io->peeraddr, peeraddrstr));
io->error = ETIMEDOUT;
hio_close(io);
}
}
在定时器回调中,会关闭这次的连接。连接服务器失败。。。