创建一个简单的tcp客户端

   日期:2020-12-21     浏览:87    评论:0    
核心提示:因为上一篇写创建tcp服务端已经把很多重要接口分析过了,这一篇会写的比较简单。与创建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

因为上一篇写创建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调用。

  1. 调用getpeername替代getsockopt。如果getpeername以ENOTCONN错误失败返回,那么连接建立已经失败,我们必须接着以SO_ERROR调用getsockopt取得套接字上待处理的错误。
  2. 以值为0的长度参数调用read。如果read失败,那么connect已经失败,read返回的errno给出了连接失败的原因。如果连接建立成功,那么read应该返回0.
  3. 再调用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);
    }
}

 在定时器回调中,会关闭这次的连接。连接服务器失败。。。

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服