remote_send_cb这个回调函数的工作是将从客户端收取来的数据转发给ss-server。在之前阅读server_recv_cb代码时可以看到,在STAGE_STREAM阶段有几种可能都会开启remote->fd的写事件的监听,从而当有写事件触发时调用remote_send_cb。从代码结构看,外层的分支是remote->send_ctx->connected是否为0,内部的分支是是否fast_open或直连。而根据实际代码执行流向,可看成是否有fast_open或直接。而最普通的情况就是没开启fast_open或者remote为直连目标服务器的情况,先讨论这种情况下的connected分支:

1. remote->send_ctx->connected为0时,即第一次进入STAGE_STREAM,尚未连接remote server时。

在server_recv_cb中直接调用非阻塞connect后,开启事件监听:

ev_io_start(EV_A_ & remote->send_ctx->io);

ev_timer_start(EV_A_ & remote->send_ctx->watcher);

因为非阻塞fd调用connect后,当connect成功后,fd可写。所以这儿监听了send_ctx->io即写事件(回忆一下:```ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE);```)

现在看一下remote_send_cb函数中,当connect成功回调cb时,由于此时 remote->send_ctx->connected==0,所以会进入:

if (!remote_send_ctx->connected) {
        struct sockaddr_storage addr;
        socklen_t len = sizeof addr;
        int r         = getpeername(remote->fd, (struct sockaddr *)&addr, &len);
        if (r == 0) {
            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);

            // no need to send any data
            if (remote->buf->len == 0) {
                ev_io_stop(EV_A_ & remote_send_ctx->io);
                ev_io_start(EV_A_ & server->recv_ctx->io);
                return;
            }
        } else {
            // not connected
            ERROR("getpeername");
            close_and_free_remote(EV_A_ remote);
            close_and_free_server(EV_A_ server);
            return;
        }
    }

这儿通过getpeername获取对端地址,即服务器地址,如果返回值为0表示获取成功,因此判断为连接成功(其实我不明白为什么要判断,但是这么判断肯定没错)。连接成功后设置remote_send_ctx->connected=1,并且stop remote send timer。说明这种情况下先前设置的remote send timer只是用来做connect超时用。然后start remote recv的timer,且start remote fd的读事件监听(回忆一下:ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ);),即准备从remote接受返回的数据

这之后判断remote->buf是否为空,如果为空则停止发送并开始监听读取客户端的数据。当前这种情况,buf不可能为空,因为至少还有socks5相关的头,即之前的abuf。如果getpeername返回非0则表示连接失败,关闭remote和server。

继续看remote_send_cb的后半段:

if (remote->buf->len == 0) {
        // close and free
        close_and_free_remote(EV_A_ remote);
        close_and_free_server(EV_A_ server);
        return;
    } else {
        // has data to send
        ssize_t s = send(remote->fd, remote->buf->data + remote->buf->idx,
                         remote->buf->len, 0);
        if (s == -1) {
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                ERROR("remote_send_cb_send");
                // close and free
                close_and_free_remote(EV_A_ remote);
                close_and_free_server(EV_A_ server);
            }
            return;
        } else if (s < (ssize_t)(remote->buf->len)) {
            // partly sent, move memory, wait for the next time to send
            remote->buf->len -= s;
            remote->buf->idx += s;
            return;
        } else {
            // all sent out, wait for reading
            remote->buf->len = 0;
            remote->buf->idx = 0;
            ev_io_stop(EV_A_ & remote_send_ctx->io);
            ev_io_start(EV_A_ & server->recv_ctx->io);
        }
    }

如果buf为空则关闭连接,目前应该不会为空,所以走到else里面。使用send发送数据,发送的数据的指针是remote->buf->data+remote->buf->idx,回忆一下server_recv_cb里面,如果未connected的情况下,这个idx是设置为0的,所以这儿发送数据就是整个buf的数据。因为remote->fd是非阻塞的,send调用后就立即返回了,如果返回值s是-1,要检查一下errno是否为EAGAIN或EWOULDBLOCK,如果不是他俩就真出错了,断开连接;如果是他俩说明要等一下再发送,直接return出去等下次remote_send_cb被回调。下次再回调进来时,remote_send_ctx->connected已经是1了,所以直接进下半段代码继续send。

如果s小于buf->len,说明发送了部分数据,需要调整idx的位置并从len减去s。等下次写事件触发可以继续发送时,就从idx的位置继续发送。

如果s等于buf->len说明全部发送完毕,len和idx清0,stop remote fd的写事件监听并sart server fd的读事件监听,即继续从客户端读取数据。

小结一下上面的讨论:是不使用fast_open或是直接目标服务器的情况下,之前没有connect时进行connet调用,connect成功后调用了remote_send_cb,随即发送remote->buf中的全部数据,可能一次send只发送了一部分,那么回调就会多次触发; 如果发送完成了则需要再次从客户端读取数据,此时客户端还是处于STAGE_STREAM状态。好了,我们现在回过头继续看server_recv_cb,此时已经是connect过了。

2. remote->send_ctx->connected==1的情况。请进入server_recv_cb继续分析:

在进入if (!remote->send_ctx->connected)对应的else代码块之前。先看一下此时读取的数据,if (!remote->direct) 里面之前是第一次进入,会将abuf插入到前面,此时已经没有abuf了,所以读取出来的数据直接加密后发送。从这儿可以看到ss只是TCP一次连接要发送的所有数据的开头加上一个头数据,之后就都是原始数据了,当然所有这些数据都是加密的。好了,继续看server_recv_cb里面的发送代码:

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;
                }
            }

此处,即server_recv_cb的STAGE_STREAM阶段,已经连接上ss-server后,读取到新的客户端数据后随即发送到ss-server。即调用了send,注意这儿的send是从buf->data的开头发送的,结合之前的代码有个结论,server_recv_cb读取到数据后即试图发送buf,这些数据是新读取到的,所以从buf开头发送,如果发送了部分数据,则设置idx,然后在remote_send_cb里面从idx处继续发送,全部发送完毕后再次开启server_recv_cb对应的监听,继续读取客户端数据然后转发到ss-server。

因此这儿send之后的代码很好理解,出错处理就不说了都一样;部分发送就是设置idx和len并停启相应事件;全部发送就是请空idx,len就行并不需要再设置开启send监听了,因为已经发送完了,就等着从客户端再次读取数据过来。在server_recv_cb前面,如果读取到EOF,即返回值为0,则说明客户端已经没有数据要发送了,且断开了连接,此次转发发送成功,回忆一下之前的代码:

if (r == 0) {
            // connection closed
            close_and_free_remote(EV_A_ remote);
            close_and_free_server(EV_A_ server);
            return;
        } 

好了,那么remote_send_cb其实处理的就是server_recv_cb没有发送出去的数据,他要继续发送,因为已经connect过,所以直接进入上面贴出的remote_send_cb的后半段代码,和上面connect成功进入一样,调用send发送数据,处理部分发送和全部发送的情况。

至此,不使用fast open或者直连目标服务器的情况已经分析完了。再总结一下整个流程:

- remote_send_ctx->connected==0时,先在server_recv_cb里面执行connect,connect成功remote_send_cb被回调

- remote_send_cb第一次被调用是connect成功时,通过getpeername判断是否成功,如果成功会开启remote fd的读事件监听和timer,即准备从ss-server接受数据。然后就是向ss-server发送数据了。connect时附带的数据可能分几次才发送完。总之发送完后就再启用server_recv_cb读取数据。

- server_recv_cb再次被调用后就继续读取客户端的数据然后发送,如果是部分发送就再次调用remote_send_cb。最终没有数据可读取则断开连接。

以上,是不开启fast open或直连的情况。下面分析开启fast open的情况。

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

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

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

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

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

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

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

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

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

  5. Shiro源码解析-Session篇

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

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

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

  7. Spring源码解析 | 第二篇:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  8. myBatis源码解析-数据源篇(3)

    前言:我们使用mybatis时,关于数据源的配置多使用如c3p0,druid等第三方的数据源.其实mybatis内置了数据源的实现,提供了连接数据库,池的功能.在分析了缓存和日志包的源码后,接下来分析 ...

  9. myBatis源码解析-反射篇(4)

    前沿 前文分析了mybatis的日志包,缓存包,数据源包.源码实在有点难顶,在分析反射包时,花费了较多时间.废话不多说,开始源码之路. 反射包feflection在mybatis路径如下: 源码解析 ...

  10. libev 源码解析

    一  libev简介 libev是一个轻量级的事件通知库,具备支持多种事件通知能力,通过对libev的源码的阅读,可以清楚了解事件通知实现内部机制. 二 核心数据结构 在libev中关键的数据结构是, ...

随机推荐

  1. ArchLinux基本系统到XFCE4桌面搭建

      Keep It Simple, Stupid 这是ArchLinux的哲学,更是一种人生哲学 好久没用linux了,这段时间因为一点点"破坏性"需求重新拾起linux用了一把 ...

  2. vCenter Server 6.7 集成 vRealize Orchestrator 7.5

    第一步,安装独立Orchestrator 7.5,并初始化   Orchestrator ova导入和初始化步骤省略...请参考官方文档... Orchestrator 初始化中的认证源需要和vCen ...

  3. ACM输入函数测试 - scanf cin 优化的输入

    2017-08-27 10:26:19 writer:pprp 进行测试如下四种输入方式: 1.scanf 2.cin 3.用了ios::sync_with_stdio(false);的cin 4.自 ...

  4. [小问题笔记(四)] Enum枚举类型转换为DataTable( C# )

    枚举: public enum ProductType { 小产品=, 大产品, 超大产品 } 转换方法: /// <summary> /// 枚举类型转化为DataTable /// & ...

  5. 机器学习笔记—混合高斯和 EM 算法

    本文介绍密度估计的 EM(Expectation-Maximization,期望最大). 假设有 {x(1),...,x(m)},因为是无监督学习算法,所以没有 y(i). 我们通过指定联合分布 p( ...

  6. 【三小时学会Kubernetes!(五) 】完成整个架构

    完成整个架构 现在我们学习了完成架构的所有必须的资源,因此这一节会非常快.图 22 中灰色的部分是需要做的事情.让我们从底部开始:部署 sa-logic 的部署. 图 22:当前应用程序状态 部署 S ...

  7. getline

    istream& istream::getline(char*, streamsize,char= '\n'); 函数getline与get的区别在于,函数get当遇到分隔符后,停止获取,并将 ...

  8. IntelliJ IDEA 左侧显示/展开类中的方法

    困扰我很久的问题: project直接右键: 打开.关闭对应效果: 之前查到的都是 : 虽然也有类似的功能,但是展开的是右侧窗口中,打开的那个类的: 即使不是我想要的,但也是不错的功能!

  9. git 提交作业流程

    git 提交作业流程,主要分为4个步骤 # 拉取远程git最新版本到本地,每次都可以先执行这条命令,因为会有其他同学更新仓库 git pull # add需要上传的文件,那个文件修改或者新增的,就ad ...

  10. ACM比赛辅导--授课内容

    Lesson1(3月19日) 1.讲解Dev-C++.VC++6.0的单步调试方法 2.学习比赛的基本输入输出,练习C语言网的1085—1092 Lesson2(3月21日) 1.学习挑战程序设计,第 ...