ss-libev 源码解析local篇(4): server_recv_cb之STAGE_STREAM
2024-08-31 11:08:32原文
继续探索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的更多相关文章
- ss-libev 源码解析local篇(3): server_recv_cb之SNI和STAGE_PARSE
上一篇看到STAGE_HANDSHAKE中的处理,到发出fake reply.这之后会从socks5 request中解析出remote addr and port,即客户端实际想要访问的服务器地址和 ...
- ss-libev 源码解析local篇(5):ss-local之remote_send_cb
remote_send_cb这个回调函数的工作是将从客户端收取来的数据转发给ss-server.在之前阅读server_recv_cb代码时可以看到,在STAGE_STREAM阶段有几种可能都会开启r ...
- ss-libev 源码解析local篇(1): ss_local的启动,客户端连入
学习研究ss-libev的一点记录(基于版本3.0.6) ss_local主要代码在local.c中,如果作为一个库编译,可通过start_ss_local_server启动local server. ...
- 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类型,而我们需 ...
随机推荐
- centos 7 删除 virbr0 虚拟网卡virsh net-list
这几天研究dubbo,在电脑上装了几台Center os 7虚拟机,最后把提供者部署到虚拟机中时,发现一个有趣的事:在dubbo-admin管理平台上看到两台不同虚拟机中的服务提供者ip都是这个玩意. ...
- JDBC连接池&DBUtils
JDBC连接池 DBCP:Apache推出的Database Connection Pool 使用步骤: > 添加jar包 commons-dbcp-1.4.jar commons-pool ...
- 多线程资源隔离之ThreadLocal
上篇讲到多线程线程安全问题的解决思路,这篇将详细讲解资源隔离ThreadLocal的实践. ThreadLocal也叫线程局部变量,类似Map结构,以当前线程为key.既然是以资源隔离的思想保证线程安 ...
- Spring boot 解决 hibernate no session异常
启动类中加入 @Beanpublic OpenEntityManagerInViewFilter openEntityManagerInViewFilter(){ return new OpenEnt ...
- [译]JavaScript需要类吗?
[译]JavaScript需要类吗? 原文:http://www.nczonline.net/blog/2012/10/16/does-javascript-need-classes/ 译者注:在 ...
- PAT1070. Mooncake (25)
#include #include #include <stdio.h> #include <math.h> using namespace std; struct SS{ d ...
- 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 ...
- jQuery实际案例⑤——仿京东侧边栏(楼层)
楼层:①页面滑动到哪块儿“楼层”就显示到哪个:②点击某“楼层”页面滚动到对应的位置:③点击“返回”回到页面顶部 实现:①使用$(window).scroll(function(){ }); //监视 ...
- 安装 inotify-tools
摘要 inotify-tools, 是一款google出的用于监控文件系统的软件. 一.软件下载地址官方站点地址:http://inotify-tools.sourceforge.net/仓库地址:h ...
- 微信小程序------联动选择器
picker 从底部弹起的滚动选择器,现支持五种选择器,通过mode来区分,分别是普通选择器,多列选择器,时间选择器,日期选择器,省市区选择器,默认是普通选择器. 先来看看效果图: 1:普通选择器 m ...