继续探索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。由此可见,ss传输到ss-server的数据就是加密后的原始数据,不过一开始要带有一个abuf,这个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的事件。注意,对于非阻塞connect,当连接建立时,描述字可写,因此这儿需要start写事件的监听。(描述字可读取决于对面是否发了数据过来)并且这里stop了server_recv_ctx上的读取事件,因为ss的处理方式是将已经收到的数据发送完毕再从客户端读取新数据。回忆一下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。
网友评论