ss-libev 源码解析local篇(4): server_recv_cb之STAGE_STREAM

来源:互联网 发布:海外淘宝首页 编辑:程序博客网 时间:2024/06/01 07:40

继续探索server_recv_cb,我们已经来到了STAGE_STREAM状态。如果在0.05秒的timer来之前客户端就有数据过来,server_recv_cb被调用,此时已经在stream状态就会读入数据到remote的buf中;如果timer先到了就是直接调用的server_recv_cb,并先进入wait状态所以不读取数据。另外在之前的parse状态,remote的buf里面也有一些数据,当然这些数据都是socks5 request之后的上层协议要传输的数据,比如http的头开始的数据。

先整体看一下stream状态的一些分支点。首先要区分是否已连接,即remote->send_ctx->connected是否为1,下面会看到ss的处理方式是读取一些数据然后转发,转发成功后再读取一些数据,一开始是没有connected,需要做一些不同的处理。然后要区分是否是直连,也就是是否直接发送到客户端要连接的服务器还是使用ss-server转发。另外要区分是否使用Tcp Fast Open。

从停止timer开始看:

ev_timer_stop(EV_A_ & server->delayed_connect_watcher);

这个是显然的,这个timer的作用上一篇已经说过,现在既然server_recv_cb已经被调用自然需要停掉。
之后,如果remote不是直连,而是我们现在讨论的ss代理的情况:

// insert shadowsocks header            if (!remote->direct) {                int err = crypto->encrypt(remote->buf, server->e_ctx, BUF_SIZE);                if (err) {                    LOGE("invalid password or cipher");                    close_and_free_remote(EV_A_ remote);                    close_and_free_server(EV_A_ server);                    return;                }                if (server->abuf) {                    bprepend(remote->buf, server->abuf, BUF_SIZE);                    bfree(server->abuf);                    ss_free(server->abuf);                    server->abuf = NULL;                }            }

这儿先将remote的buf进行加密,然后会看一下有没有abuf,如果是第一次传输数据,肯定是有abuf的,abuf就是socks5 request产生的ss tcp request header加密后的数据,如果有abuf就把他插入到remote的buf前面。注意abuf是server的abuf。

之后有一个if (!remote->send_ctx->connected) {的判断,remote的send_ctx是用来管理从ss-local向ss-server发送数据的,一开始connected为0表示还没有连接到ss-server。所以此时要将buf的idx设置为0 remote->buf->idx = 0;,这个idx是发送数据的开始索引。然后就要连接到ss-server,但实际情况还要复杂一些,因为可能要使用TCP Fast Open。
* 先看不使用fast open的情况,判断条件是if (!fast_open || remote->direct)即如果是直连强制不使用fast open。具体连接的代码:

                    int r = connect(remote->fd, (struct sockaddr *)&(remote->addr), remote->addr_len);                    if (r == -1 && errno != CONNECT_IN_PROGRESS) {                        ERROR("connect");                        close_and_free_remote(EV_A_ remote);                        close_and_free_server(EV_A_ server);                        return;                    }                    // wait on remote connected event                    ev_io_stop(EV_A_ & server_recv_ctx->io);                    ev_io_start(EV_A_ & remote->send_ctx->io);                    ev_timer_start(EV_A_ & remote->send_ctx->watcher);

因为remote的fd在create_remote中被设置为非阻塞,所以connect调用应该是立即返回,如果返回值为-1,且errno不是CONNECT_IN_PROGRESS才算连接失败,否则就等待libev的事件。这里先stop了server_recv_ctx上的读取事件,因为ss的处理方式是将已经收到的数据发送完毕再从客户端读取新数据。然后start remote->send_ctx的写事件,见new_remote中的事件设置:

    ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ);    ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE);    ev_timer_init(&remote->send_ctx->watcher, remote_timeout_cb,                  min(MAX_CONNECT_TIMEOUT, timeout), 0);    ev_timer_init(&remote->recv_ctx->watcher, remote_timeout_cb,                  timeout, timeout);

最后start remote->send_ctx上的timeout timer。timeout时间为传入的timeout参数和#define MAX_CONNECT_TIMEOUT 10之间的最小值,也就是说timeout最长为10秒。总之如果10秒之内没有发送完成就会关闭server和remote,即客户端到ss-local,ss-local到ss-server的两个tcp连接全部关掉,这样此次代理就失败了。
OK,回到STAGE_STREAM的代码中。
* 如果是tcp fast open的情况,就会走到if (!fast_open || remote->direct)对应的else里面。具体分为两种,一个是apple的平台,比如mac和iOS,会调用connectx和send:

((struct sockaddr_in *)&(remote->addr))->sin_len = sizeof(struct sockaddr_in);                    sa_endpoints_t endpoints;                    memset((char *)&endpoints, 0, sizeof(endpoints));                    endpoints.sae_dstaddr    = (struct sockaddr *)&(remote->addr);                    endpoints.sae_dstaddrlen = remote->addr_len;                    int s = connectx(remote->fd, &endpoints, SAE_ASSOCID_ANY,                                     CONNECT_RESUME_ON_READ_WRITE | CONNECT_DATA_IDEMPOTENT,                                     NULL, 0, NULL, NULL);                    if (s == 0) {                        s = send(remote->fd, remote->buf->data, remote->buf->len, 0);                    }

而其他平台则调用sendto:

int s = sendto(remote->fd, remote->buf->data, remote->buf->len, MSG_FASTOPEN,                                   (struct sockaddr *)&(remote->addr), remote->addr_len);

这是因为apple的API和标准不一样,当然效果是一样的,TFO大概来说就是第一次连接时服务器会返回一个cookie给客户端,第二次连接开始就可以不用connect进行正常握手,而是直接使用cookie连接并携带数据。TFO如果能用当然特别好,毕竟优化了很多握手,但具体使用的时候我们发现TFO还是有一些限制,主要是一些运营商的路由器会丢弃大于标准长度的第一个SYN包,导致连接失败。这儿如果想办法检查一下如果失败让其禁用就好了。下面代码会看到一个类似的fallback代码,但是并不能处理这种情况,因为那个只是检查客户端是否真的支持TFO。先看一下上面send/sendto返回值s==-1的处理:

if (s == -1) {                        if (errno == CONNECT_IN_PROGRESS) { //因为是非阻塞io                            // in progress, wait until connected                            remote->buf->idx = 0;                            ev_io_stop(EV_A_ & server_recv_ctx->io);                            ev_io_start(EV_A_ & remote->send_ctx->io);                            return;                        } else {                            ERROR("sendto");                            if (errno == ENOTCONN) {                                LOGE("fast open is not supported on this platform");                                // just turn it off                                fast_open = 0; //这儿检查到是客户端平台不支持TFO,所以要禁用                            }                            close_and_free_remote(EV_A_ remote);                            close_and_free_server(EV_A_ server);                            return;                        }                    } 

因为fd设置为了异步,如果errno是CONNECT_IN_PROGRESS说明还没连上,stop sever_recv_ctx->io的读事件处理,也即不再回调我们正在讨论的server_recv_cb函数;start remote->send->io的写事件处理,即如果有数据要发送到remote,回调remote_send_cb。errno是其他情况都会关闭连接,但如果是ENOTCONN则是认为平台不支持TFO,所以要设置fast_open为0,这样下一个连接就不会使用TFO了。
再下面的代码是发送成功部分数据:

else if (s < (int)(remote->buf->len)) {                        remote->buf->len -= s; //buf长度减去已发送长度                        remote->buf->idx  = s; //当前索引移动到s                        ev_io_stop(EV_A_ & server_recv_ctx->io);                        ev_io_start(EV_A_ & remote->send_ctx->io);                        ev_timer_start(EV_A_ & remote->send_ctx->watcher);                        return;                    } 

这种情况是已发送数据大小s小于buf长度,说明只发送了一部分数据,因此需要知道下次从哪儿发送,就是这儿的remote->buf->idx=s;还需要发送多少,即减去s后剩余的buf长度。然后server_recv_ctx->io的读事件就被stop了,并且start了remote->send_ctx的写事件。这意外着剩余的数据就不在server_recv_cb里面发送了,而是到remote_send_cb里面发送。同样,这儿开启了发送timeout timer。这儿对事件的设置和上面直连或无TFO的时候一样,结果都是启动remote_send_cb去发送数据,只是直连的情况是connect成功后buf中所有数据都去remote_send_cb中发送,而这儿TFO的情况由于没有连接过程且已发送了部分数据,所以remote_send_cb中只发送剩余的数据,而数据的开始就是从buf->idx开始。
再下面的else代码如下:

else {                        // Just connected                        remote->buf->idx = 0;                        remote->buf->len = 0;#ifdef __APPLE__                        ev_io_stop(EV_A_ & server_recv_ctx->io);                        ev_io_start(EV_A_ & remote->send_ctx->io);                        ev_timer_start(EV_A_ & remote->send_ctx->watcher);#else                        remote->send_ctx->connected = 1;                        ev_timer_stop(EV_A_ & remote->send_ctx->watcher);                        ev_timer_start(EV_A_ & remote->recv_ctx->watcher);                        ev_io_start(EV_A_ & remote->recv_ctx->io);                        return;#endif                    }

走到这儿说明数据已经全部发送完成了,所以将idx和len都清0。下面的事件设置,apple平台和linux不一样了。对于apple,还是要将控制流转入remote_send_cb;而对于linux,是stop了remote上send和recv的timeout timer并且启动了remote->recv_ctx的读事件,即等待ss-server发送数据回来。同时对于Linux, send_ctx->connected设置为1表示已连接。这个connected标志在remote_send_cb里面会产生不同的代码分支,至于为啥apple平台没有算connect还需要去remote_send_cb里面看到这儿还不能明白,希望看remote_send_cb的时候能弄清楚。

下面是最后一个else,对应if (!remote->send_ctx->connected) {。即已经连接上之后,又从客户端收取到数据时的处理:

else {                int s = send(remote->fd, remote->buf->data, remote->buf->len, 0);                if (s == -1) {                    if (errno == EAGAIN || errno == EWOULDBLOCK) {                        // no data, wait for send                        remote->buf->idx = 0;                        ev_io_stop(EV_A_ & server_recv_ctx->io);                        ev_io_start(EV_A_ & remote->send_ctx->io);                        return;                    } else {                        ERROR("server_recv_cb_send");                        close_and_free_remote(EV_A_ remote);                        close_and_free_server(EV_A_ server);                        return;                    }                } else if (s < (int)(remote->buf->len)) {                    remote->buf->len -= s;                    remote->buf->idx  = s;                    ev_io_stop(EV_A_ & server_recv_ctx->io);                    ev_io_start(EV_A_ & remote->send_ctx->io);                    return;                } else {                    remote->buf->idx = 0;                    remote->buf->len = 0;                }            }

这段代码简单很多了,没有TFO的区分了。一开始是一个send,注意send是完整的buf,没有管idx(后面会看到idx是在remote send时使用),这说明在STREAM状态中,server_recv_cb从客户端读取数据之后立即转发,而之前读取的数据已经转发完成才会再次读取。send之后的处理其实和上面TFO sendto之后的处理很像,如果s==-1,判断error是否是还没发送,如果是就把控制流转到remote_send_cb中,否则就是真出错了关闭连接。注意这儿没有启动timer,因为timer已经启动了,这个timer是这次转发的一个整体的timer。然后s < (int)(remote->buf->len)也和上面一样,也只是没启动timer。最后的else里面是全部发送完毕,这儿只是把idx和len清除没有启动其他回调,那么执行到这儿server_recv_cb就退出了。

这一部分代码比较多,而且有多种分支情况:connect,TFO,apple,所以比较难理解,等后面remote_send_cb,remote_recv_cb, server_send_cb都看完再回过来整体总结一下应该就会清楚很多了。
下一篇分析remote_send_cb。

原创粉丝点击