继续探索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。

ss-libev 源码解析local篇(4): server_recv_cb之STAGE_STREAM的更多相关文章

  1. ss-libev 源码解析local篇(3): server_recv_cb之SNI和STAGE_PARSE

    上一篇看到STAGE_HANDSHAKE中的处理,到发出fake reply.这之后会从socks5 request中解析出remote addr and port,即客户端实际想要访问的服务器地址和 ...

  2. ss-libev 源码解析local篇(5):ss-local之remote_send_cb

    remote_send_cb这个回调函数的工作是将从客户端收取来的数据转发给ss-server.在之前阅读server_recv_cb代码时可以看到,在STAGE_STREAM阶段有几种可能都会开启r ...

  3. ss-libev 源码解析local篇(1): ss_local的启动,客户端连入

    学习研究ss-libev的一点记录(基于版本3.0.6) ss_local主要代码在local.c中,如果作为一个库编译,可通过start_ss_local_server启动local server. ...

  4. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  5. jQuery2.x源码解析(构建篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...

  6. jQuery2.x源码解析(设计篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...

  7. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

  8. Shiro源码解析-Session篇

    上一篇Shiro源码解析-登录篇中提到了在登录验证成功后有对session的处理,但未详细分析,本文对此部分源码详细分析下. 1. 分析切入点:DefaultSecurityManger的login方 ...

  9. myBatis源码解析-类型转换篇(5)

    前言 开始分析Type包前,说明下使用场景.数据构建语句使用PreparedStatement,需要输入的是jdbc类型,但我们一般写的是java类型.同理,数据库结果集返回的是jdbc类型,而我们需 ...

随机推荐

  1. centos 7 删除 virbr0 虚拟网卡virsh net-list

    这几天研究dubbo,在电脑上装了几台Center os 7虚拟机,最后把提供者部署到虚拟机中时,发现一个有趣的事:在dubbo-admin管理平台上看到两台不同虚拟机中的服务提供者ip都是这个玩意. ...

  2. JDBC连接池&DBUtils

    JDBC连接池 DBCP:Apache推出的Database Connection Pool 使用步骤: > 添加jar包  commons-dbcp-1.4.jar  commons-pool ...

  3. 多线程资源隔离之ThreadLocal

    上篇讲到多线程线程安全问题的解决思路,这篇将详细讲解资源隔离ThreadLocal的实践. ThreadLocal也叫线程局部变量,类似Map结构,以当前线程为key.既然是以资源隔离的思想保证线程安 ...

  4. Spring boot 解决 hibernate no session异常

    启动类中加入 @Beanpublic OpenEntityManagerInViewFilter openEntityManagerInViewFilter(){ return new OpenEnt ...

  5. [译]JavaScript需要类吗?

    [译]JavaScript需要类吗?   原文:http://www.nczonline.net/blog/2012/10/16/does-javascript-need-classes/ 译者注:在 ...

  6. PAT1070. Mooncake (25)

    #include #include #include <stdio.h> #include <math.h> using namespace std; struct SS{ d ...

  7. Codeforces Round #360 (Div. 2) D. Remainders Game

    D. Remainders Game time limit per test 1 second memory limit per test 256 megabytes input standard i ...

  8. jQuery实际案例⑤——仿京东侧边栏(楼层)

    楼层:①页面滑动到哪块儿“楼层”就显示到哪个:②点击某“楼层”页面滚动到对应的位置:③点击“返回”回到页面顶部 实现:①使用$(window).scroll(function(){ });  //监视 ...

  9. 安装 inotify-tools

    摘要 inotify-tools, 是一款google出的用于监控文件系统的软件. 一.软件下载地址官方站点地址:http://inotify-tools.sourceforge.net/仓库地址:h ...

  10. 微信小程序------联动选择器

    picker 从底部弹起的滚动选择器,现支持五种选择器,通过mode来区分,分别是普通选择器,多列选择器,时间选择器,日期选择器,省市区选择器,默认是普通选择器. 先来看看效果图: 1:普通选择器 m ...