ss-libev 源码解析local篇(5):ss-local之remote_send_cb
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的更多相关文章
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
- jQuery2.x源码解析(构建篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...
- jQuery2.x源码解析(设计篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...
- jQuery2.x源码解析(回调篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...
- Shiro源码解析-Session篇
上一篇Shiro源码解析-登录篇中提到了在登录验证成功后有对session的处理,但未详细分析,本文对此部分源码详细分析下. 1. 分析切入点:DefaultSecurityManger的login方 ...
- myBatis源码解析-类型转换篇(5)
前言 开始分析Type包前,说明下使用场景.数据构建语句使用PreparedStatement,需要输入的是jdbc类型,但我们一般写的是java类型.同理,数据库结果集返回的是jdbc类型,而我们需 ...
- Spring源码解析 | 第二篇:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析
一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...
- myBatis源码解析-数据源篇(3)
前言:我们使用mybatis时,关于数据源的配置多使用如c3p0,druid等第三方的数据源.其实mybatis内置了数据源的实现,提供了连接数据库,池的功能.在分析了缓存和日志包的源码后,接下来分析 ...
- myBatis源码解析-反射篇(4)
前沿 前文分析了mybatis的日志包,缓存包,数据源包.源码实在有点难顶,在分析反射包时,花费了较多时间.废话不多说,开始源码之路. 反射包feflection在mybatis路径如下: 源码解析 ...
- libev 源码解析
一 libev简介 libev是一个轻量级的事件通知库,具备支持多种事件通知能力,通过对libev的源码的阅读,可以清楚了解事件通知实现内部机制. 二 核心数据结构 在libev中关键的数据结构是, ...
随机推荐
- 20144303 《Java程序设计》第二次实验实验报告
20144303 <Java程序设计>第二次实验实验报告 北京电子科技学院(besti)实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握U ...
- 20145211《网络渗透》msf辅助模块的应用
20145211<网络渗透>msf辅助模块的应用 一.实验准备 启用VB的kali,需要用到桥接,VMware桥接总是罢工…… 二.实验步骤 最好开桥接模式,要不然你就多开几个虚拟机(只要 ...
- intellij 文件太大,无法code assistant
添加 idea.max.intellisense.filesize=2500 在IDE_HOME\bin\idea.properties https://intellij-support.jetbra ...
- sphinx 安装使用
一.linux(centos)下安装源码包 1.下载 wget http://sphinxsearch.com/files/sphinx-2.3.1-beta.tar.gz 2.安装 切换目录到 ...
- 【ML数学知识】极大似然估计
它是建立在极大似然原理的基础上的一个统计方法,极大似然原理的直观想法是,一个随机试验如有若干个可能的结果A,B,C,... ,若在一次试验中,结果A出现了,那么可以认为实验条件对A的出现有利,也即出现 ...
- Dancing Links DLX
Dancing Links DLX Dancing Links 用来解精准覆盖问题. 精准覆盖问题有两种版本. 精准覆盖 : 给一个01矩阵,如何选出若干行,使得每列都有且仅有一个1. 可以求最少行数 ...
- jquery validate检验
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...
- SSH密钥登陆免密码方法
原帖地址:http://ask.apelearn.com/question/798 用Putty实现A机器远程登陆B机器,具体实现请看链接:http://www.cnblogs.com/ImJerry ...
- 自已开发完美的触摸屏网页版仿app弹窗型滚动列表选择器/日期选择器
手机端网页版app在使用下拉列表时,传统的下拉列表使用起来体验非常不好,一般做的稍好一点的交互功能界面都不会直接使用下拉列表,所以app的原生下拉列表都是弹窗列表选择,网页型app从使用体验上来当然也 ...
- poj1679次小生成树入门题
次小生成树求法:例如求最小生成树用到了 1.2.4这三条边,总共5条边,那循环3次的时候,每次分别不用1.2.4求得最小生成树的MST,最小的MST即为次小生成树 如下代码maxx即求最小生成树时求得 ...