ss-libev 源码解析local篇(1): ss_local的启动,客户端连入
学习研究ss-libev的一点记录(基于版本3.0.6)
ss_local主要代码在local.c中,如果作为一个库编译,可通过start_ss_local_server启动local server。所以先从这个函数入手,看local的启动过程。
STEP0: int start_ss_local_server(profile_t profile)
- profile_t profile 是ss-libev的配置
typedef struct {
/* Required */
char *remote_host; // hostname or ip of remote server
char *local_addr; // local ip to bind
char *method; // encryption method
char *password; // password of remote server
int remote_port; // port number of remote server
int local_port; // port number of local server
int timeout; // connection timeout
/* Optional, set NULL if not valid */
char *acl; // file path to acl
char *log; // file path to log
int fast_open; // enable tcp fast open
int mode; // enable udp relay
int mtu; // MTU of interface
int mptcp; // enable multipath TCP
int verbose; // verbose mode
} profile_t;
local启动时核心是创建一个listen_ctx_t对象,这个结构定义如下:
typedef struct listen_ctx {
ev_io io;
char *iface;
int remote_num;
int timeout;
int fd;
int mptcp;
struct sockaddr **remote_addr;
} listen_ctx_t;
其中,最重要的是io这个成员了,这是一个libev中的ev_io对象,用来监听某个套接字上的事件,后面会具体说。
fd是local服务器的监听套接字描述符:
struct sockaddr *remote_addr_tmp[MAX_REMOTE_NUM];
listen_ctx_t listen_ctx;
listen_ctx.remote_num = 1;
listen_ctx.remote_addr = remote_addr_tmp;
listen_ctx.remote_addr[0] = (struct sockaddr *)(&storage);
listen_ctx.timeout = timeout;
listen_ctx.iface = NULL;
listen_ctx.mptcp = mptcp;
if (mode != UDP_ONLY) {
// Setup socket
int listenfd;
listenfd = create_and_bind(local_addr, local_port_str);
if (listenfd == -1) {
ERROR("bind()");
return -1;
}
if (listen(listenfd, SOMAXCONN) == -1) {
ERROR("listen()");
return -1;
}
setnonblocking(listenfd);
listen_ctx.fd = listenfd;
ev_io_init(&listen_ctx.io, accept_cb, listenfd, EV_READ);
ev_io_start(loop, &listen_ctx.io);
}
local服务器的socket创建并绑定端口(create_and_bind)后监听(listen),设置为非阻塞,然后设置一个libev的ev_io对象并启动EV_READ事件的监听,当该套建字上有可以读入的数据时执行回调 accept_cb,也就是说有客户端连接上来了需要去accept。在看accept_cb之前,继续看一下剩下的代码。
// Setup UDP
if (mode != TCP_ONLY) {
LOGI("udprelay enabled");
struct sockaddr *addr = (struct sockaddr *)(&storage);
udp_fd = init_udprelay(local_addr, local_port_str, addr,
get_sockaddr_len(addr), mtu, crypto, timeout, NULL);
}
这是初始化udp转发,暂时不讨论了,注意这儿返回的udp_fd是local udp server的端口对应的描述符,ss-local作为独立应用使用的时候,一般tcp和udp使用同一个端口号,所以tcp的listenfd和udp_fd转换为端口号是相同的(通过getsockname获取的地址里面的端口号),但是如果ss-local作为一个库使用,往往需要将local port设置为0,让系统选择空闲端口,这些这两个描述符对应的端口就可能不一样了。3.0.6之前就没区分,我发了个issue作者才给改的。继续:
// Init connections
cork_dllist_init(&connections);
// Enter the loop
ev_run(loop, 0);
if (verbose) {
LOGI("closed gracefully");
}
// Clean up
if (mode != UDP_ONLY) {
ev_io_stop(loop, &listen_ctx.io);
free_connections(loop);
close(listen_ctx.fd);
}
if (mode != TCP_ONLY) {
free_udprelay();
}
return 0;
cork_dllist_init(&connections);初始化了一个链表,这个方法来自于libcork库。后面会说到server_t对象会存放到connections链表中,然后在free_connections的时候从链表中取出server_t对象关闭和释放。这儿有意思的是这种c语言的链表中并不是直接存放一个server_t类型的指针,而是存放了一个cork_dllist_item类型的变量,且server_t结构中会包含这个cork_dllist_item类型的变量。核心是通过cork_container_of方法可以根据cork_dllist_item的指针获取server_t对象的指针。这就相当于实现了一个侵入式的万能链表,不经常使用c语言的同学比如我还是觉得挺有趣的,c语言没有泛型是不是也挺好的。
/* Return a pointer to a @c struct, given a pointer to one of its
* fields. */
#define cork_container_of(field, struct_type, field_name) \
((struct_type *) (- offsetof(struct_type, field_name) + \
(void *) (field)))
回到启动函数里面,ev_run(loop, 0);这句启动了一个libev的循环,且线程就阻塞在这儿了。直到服务器退出后才继续执行后面的clean up代码。所以说listen_ctx虽然是在栈上定义的,但会在ss local运行期间一直存在。下面会看到这个对象会通过指针转换在accept_cb中取得并把他存放于server_t对象中。
STEP1: void accept_cb(EV_P_ ev_io *w, int revents)
这个函数短就直接贴了:
void
accept_cb(EV_P_ ev_io *w, int revents)
{
listen_ctx_t *listener = (listen_ctx_t *)w;
int serverfd = accept(listener->fd, NULL, NULL);
if (serverfd == -1) {
ERROR("accept");
return;
}
setnonblocking(serverfd);
int opt = 1;
setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt));
#ifdef SO_NOSIGPIPE
setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt));
#endif
server_t *server = new_server(serverfd);
server->listener = listener;
ev_io_start(EV_A_ & server->recv_ctx->io);
}
首先回顾一下上面start_ss_local_server里的两句代码:
ev_io_init(&listen_ctx.io, accept_cb, listenfd, EV_READ);
ev_io_start(loop, &listen_ctx.io);
这是libev的基本用法,简单说就是创建并初始化一个ev_io对象,即listen_ctx.io,他将监听listenfd所描述的套接字上的读事件,当有数据可读入时,调用回到函数accept_cb。ev_io_init只是初始化,只有调用了ev_io_start后才真正开始监听,这点很重要,因为代码里面经常会先init,然后在合适的时候再start,所以为了不看错代码流程一定要分清start才是真正开始监听。
回到accept_cb中,参数ev_io *w会传入监听的ev_io对象,由于io正好是listen_ctx_t结构体的第一个成员,因此可以通过指针转换得到listener的指针,进而获取到fd。由于fd是一个监听套接字,也就是说socket-bind-listen之后的套接字,这是tcp服务器的通常流程,当这个套接字上有可读数据时,说明是有客户端连入了,因此要执行accept函数,这个和在线程里面阻塞住等待accept的做法很不一样,通过libev可以异步的知道有数据可读,然后主动调用accept,这样避免开很多线程解决高并发的问题。在整个ss-local运行期间,客户端每次连入都会回调到accept_cb,进而调用accept产生一个serverfd,这个serverfd用来描述来自客户端的一条连接,ss-libev通过一个server_t结构体来管理来自客户端的一条连接。
typedef struct server {
int fd;
int stage;
cipher_ctx_t *e_ctx;
cipher_ctx_t *d_ctx;
struct server_ctx *recv_ctx;
struct server_ctx *send_ctx;
struct listen_ctx *listener;
struct remote *remote;
buffer_t *buf;
buffer_t *abuf;
ev_timer delayed_connect_watcher;
struct cork_dllist_item entries;
} server_t;
最重要的是两个server_ctx成员,recv_ctx和send_ctx,这是用来收发数据的上下文对象。server_t相当于在socks5客户端和ss-local 的socks5服务端之间交互的对象,而recv_ctx用来处理来自于socks5客户端的数据,send_ctx用来处理向socks5客户端转发来自远程服务器的数据。remote就是处理远程服务器交互的对象。其他内容先忽略,看一下server_ctx:
typedef struct server_ctx {
ev_io io;
int connected;
struct server *server;
} server_ctx_t;
其中的ev_io对象io就是用来监听客户端和ss-local socks5 sever之间的数据读写的。
回到accept_cb中,server_t *server = new_server(serverfd);创建了server对象并进行设置:
static server_t *
new_server(int fd)
{
server_t *server;
server = ss_malloc(sizeof(server_t));
memset(server, 0, sizeof(server_t));
server->recv_ctx = ss_malloc(sizeof(server_ctx_t));
server->send_ctx = ss_malloc(sizeof(server_ctx_t));
server->buf = ss_malloc(sizeof(buffer_t));
server->abuf = ss_malloc(sizeof(buffer_t));
balloc(server->buf, BUF_SIZE);
balloc(server->abuf, BUF_SIZE);
memset(server->recv_ctx, 0, sizeof(server_ctx_t));
memset(server->send_ctx, 0, sizeof(server_ctx_t));
server->stage = STAGE_INIT;
server->recv_ctx->connected = 0;
server->send_ctx->connected = 0;
server->fd = fd;
server->recv_ctx->server = server;
server->send_ctx->server = server;
server->e_ctx = ss_align(sizeof(cipher_ctx_t));
server->d_ctx = ss_align(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);
ev_timer_init(&server->delayed_connect_watcher,
delayed_connect_cb, 0.05, 0);
cork_dllist_add(&connections, &server->entries);
return server;
}
主要工作就是分配内存给recv_ctx,send_ctx以及buf和abuf这两个buffer。设置stage为STAGE_INIT,设置上fd,设置加密解密的上下文对象e_ctx和d_ctx。然后设置了两个ev_io监听,分别是对于recv_ctx->io设置READ监听,对于send_ctx->io设置WRITE监听,并且还启动了一个timer,这个timer 0.05秒之后会调用delayed_connect_cb回调。最后会通过cork_dllist_add方法,把server加入到connections链表中(上面说过)。
返回accept_cb中,执行ev_io_start(EV_A_ & server->recv_ctx->io);启动了recv_ctx->io上的事件监听,即serverfd上的读事件。下次会具体分析这个读事件的回调server_recv_cb,local的主要逻辑在这里面。
小结
- listen_ctx_t对象用于处理ss-local的监听,保存了用于处理READ事件的io和监听套接字fd,并且保存了远端ss-server服务器的地址。这个对象在ss-local运行期间一直存在。
- 当有客户端连接上ss-local后,accept_cb会被调用,其中在listen_ctx_t保存的监听fd上执行accept操作,获取serverfd,并保存在一个server_t结构中。server_t用于处理客户端和ss-lcoal之间的一条连接,客户端每次连接都会调用一次accept_cb并创建一个server_t。
- server_t对象创建后会设置recv_ctx和send_ctx,其中都包含io对象用于处理读或写事件。先启动读事件,等待客户端发送SOCKS5握手请求。
- 下文会分析server_recv_cb,从SOCKS5握手开始。
ss-libev 源码解析local篇(1): ss_local的启动,客户端连入的更多相关文章
- ss-libev 源码解析local篇(4): server_recv_cb之STAGE_STREAM
继续探索server_recv_cb,我们已经来到了STAGE_STREAM状态.如果在0.05秒的timer来之前客户端就有数据过来,server_recv_cb被调用,此时已经在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 ...
- 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类型,而我们需 ...
随机推荐
- day9 文件的读取
文件操作 一.打开文件 f = open('歌词.txt','w',encoding='utf-8') # f:文件操作符 文件句柄 文件操作对象 open打开文件是依赖了操作系统提供的途径 操作系统 ...
- 【转】解决Gradle报错找不到org.gradle.api.internal.project.ProjectInternal.getPluginManager()方法问题
源地址:http://www.mamicode.com/info-detail-1178200.html 一.概述 因为本地的AndroidStudio很久没用了,所以想要研究下github上的某个代 ...
- python中数据类型转换
python中list和str互转 1.list转str 假设有一个名为test_list的list,转换后的str名为test_str 则转换方法: test_str = "" ...
- C#基元类型取值范围对照表
byte 无符号 8 为整数,值为0-255; sbyte 有符号8位整数,-128~127; short 有符号16位整数,范围在-32768~32767 ushort 无符号16位 ...
- CPU与GPU区别 通俗易懂
转:https://blog.csdn.net/xiaolang85/article/details/51500340 有网友在网上提问:“为什么现在更多需要用的是 GPU 而不是 CPU,比如挖矿甚 ...
- 一年java程序员的感悟
前沿 在小公司干了差不多一年,刚进来与一个中级程序员做交接,过了大概一个月,那个中级程序员走了,从此,走上了"泥泞"的道路(独立开发),熟悉了公司的项目和业务用了一个月左右,公司当 ...
- 【Flask】Flask上下文
# 上下文: ### Local对象:在`Flask`中,类似于`request`的对象,其实是绑定到了一个`werkzeug.local.Local`对象上.这样,即使是同一个对象,那么在多个线程中 ...
- ASP.MVC 项目中使用 UEditor 文本编辑器
1.下载UEditor 源文件,并导入项目中 2.添加项目中需要使用的CSS和JS //Ueditor 文本编辑器必备的StyleBundle和ScriptBundle StyleBundle ued ...
- jQuery可自动播放动画的焦点图
在线演示 本地下载
- 详解Linux系统中的文件名和文件种类以及文件权限
Linux文件种类与副文件名 一直强调一个概念,那就是:任何装置在Linux底下都是文件, 不仅如此,连资料沟通的介面也有专属的文件在负责-所以,你会瞭解到,Linux的文件种类真的很多- 除了前面提 ...