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. 20144303 《Java程序设计》第二次实验实验报告

    20144303 <Java程序设计>第二次实验实验报告 北京电子科技学院(besti)实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握U ...

  2. 20145211《网络渗透》msf辅助模块的应用

    20145211<网络渗透>msf辅助模块的应用 一.实验准备 启用VB的kali,需要用到桥接,VMware桥接总是罢工…… 二.实验步骤 最好开桥接模式,要不然你就多开几个虚拟机(只要 ...

  3. intellij 文件太大,无法code assistant

    添加 idea.max.intellisense.filesize=2500 在IDE_HOME\bin\idea.properties https://intellij-support.jetbra ...

  4. sphinx 安装使用

    一.linux(centos)下安装源码包 1.下载  wget http://sphinxsearch.com/files/sphinx-2.3.1-beta.tar.gz 2.安装   切换目录到 ...

  5. 【ML数学知识】极大似然估计

    它是建立在极大似然原理的基础上的一个统计方法,极大似然原理的直观想法是,一个随机试验如有若干个可能的结果A,B,C,... ,若在一次试验中,结果A出现了,那么可以认为实验条件对A的出现有利,也即出现 ...

  6. Dancing Links DLX

    Dancing Links DLX Dancing Links 用来解精准覆盖问题. 精准覆盖问题有两种版本. 精准覆盖 : 给一个01矩阵,如何选出若干行,使得每列都有且仅有一个1. 可以求最少行数 ...

  7. jquery validate检验

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  8. SSH密钥登陆免密码方法

    原帖地址:http://ask.apelearn.com/question/798 用Putty实现A机器远程登陆B机器,具体实现请看链接:http://www.cnblogs.com/ImJerry ...

  9. 自已开发完美的触摸屏网页版仿app弹窗型滚动列表选择器/日期选择器

    手机端网页版app在使用下拉列表时,传统的下拉列表使用起来体验非常不好,一般做的稍好一点的交互功能界面都不会直接使用下拉列表,所以app的原生下拉列表都是弹窗列表选择,网页型app从使用体验上来当然也 ...

  10. poj1679次小生成树入门题

    次小生成树求法:例如求最小生成树用到了 1.2.4这三条边,总共5条边,那循环3次的时候,每次分别不用1.2.4求得最小生成树的MST,最小的MST即为次小生成树 如下代码maxx即求最小生成树时求得 ...