May 5, 2017 - golangSlice的扩容规则

一段代码

       以前一直以为go语言中的slice,也就是切片,其容量增长规则与std::vector一样,指数扩容,每次扩容都增长一倍,没有去详细了解过源代码。直到同事丢给了我以下这段代码:


s := []int{1,2}
s = append(s,4,5,6)
fmt.Printf("%d %d",len(s),cap(s))

       如果简单地按照指数扩容,那么结果应该是 5,8。从初始化时的 2,2 扩容到 4,4 ,然后增长到 5, 8。但结果并不是这样,而是输出了 5,6。深入测试这段代码,每次往s中append两个元素,往后cap(s)都是6的倍数增长。

       源代码位于runtime/slice.go中,关于slice增长的函数是growslice,其中的代码,决定了slice的扩容规则

基本cap的增长规则

     newcap := old.cap
	if newcap+newcap < cap {
		newcap = cap
	} else {
		for {
			if old.len < 1024 {
				newcap += newcap
			} else {
				newcap += newcap / 4
			}
			if newcap >= cap {
				break
			}
		}
	}

       此处的cap是旧容量加上新加入元素大小的结果,也就是此处slice扩容的理论上的最小值,old就是旧的slice。可以看到cap增长基本规则是,若新入元素大小通过倍数增长能够hold住,那就根据旧容量是否超过1024决定是倍数增长还是1.25倍逐步增长;若新入元素大小超过了原有的容量,则新容量取两者相加计算出来的最小cap值。

       于是在例子中,经过扩容规则,()2 + 3 = 5) > (2 *2 = 4 ),newcap应当取5

内存对齐

       计算出了新容量之后,还没有完,出于内存的高效利用考虑,还要进行内存对齐

capmem := roundupsize(uintptr(newcap) * uintptr(et.size))

       newcap就是前文中计算出的newcap,et.size代表slice中一个元素的大小,capmem计算出来的就是此次扩容需要申请的内存大小。roundupsize函数就是处理内存对齐的函数。

func roundupsize(size uintptr) uintptr {
	if size < _MaxSmallSize {
		if size <= 1024-8 {
			return uintptr(class_to_size[size_to_class8[(size+7)>>3]])
		} else {
			return uintptr(class_to_size[size_to_class128[(size-1024+127)>>7]])
		}
	}
	if size+_PageSize < size {
		return size
	}
	return round(size, _PageSize)
}

       _MaxSmallSize的值在64位macos上是32«10,也就是2的15次方,32k。golang事先生成了一个内存对齐表。通过查找(size+7) » 3,也就是需要多少个8字节,然后到class_to_size中寻找最小内存的大小。承接上文的size,应该是40,size_to_class8的内容是这样的:

size_to_class8:1 1 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11...

       查表得到的数字是4,而class_to_size的内容是这样的:

class_to_size:0 8 16 32 48 64 80 96 112 128 144 160 176 192 208 224 240 256...

       因此得到最小的对齐内存是48字节。完成内存对齐计算后,重新计算应有的容量,也就是48/8 = 6。扩容得到的容量就是6了。

Apr 12, 2017 - tinyproxy源码分析

重要的全局变量

static vector_t listen_fds;// listen端口fd

static struct child_s *child_ptr; // 子进程信息数组

static unsigned int *servers_waiting;  // 等待连接的子进程,也就是空闲状态

unsigned int received_sighup = FALSE;

main函数及注解

int
main (int argc, char **argv)
{
        // 设置文件权限
        umask (0177);

        log_message (LOG_INFO, "Initializing " PACKAGE " ...");

		// 解析配置文件
        if (config_compile_regex()) {
                exit (EX_SOFTWARE);
        }

		// 加载默认配置
        initialize_config_defaults (&config_defaults);

        // 解析命令行
        process_cmdline (argc, argv, &config_defaults);

		// 加载配置文件配置
        if (reload_config_file (config_defaults.config_file,
                                &config,
                                &config_defaults)) {
                exit (EX_SOFTWARE);
        }

		// 初始化profiling相关内存
        init_stats ();

        // 匿名代理处理
        if (is_anonymous_enabled ()) {
                anonymous_insert ("Content-Length");
                anonymous_insert ("Content-Type");
        }

		// 守护进程
        if (config.godaemon == TRUE)
                makedaemon ();

		// 注册SIGPIPE 防异常退出。对关闭连接的操作会产生这个信号
        if (set_signal_handler (SIGPIPE, SIG_IGN) == SIG_ERR) {
                fprintf (stderr, "%s: Could not set the \"SIGPIPE\" signal.\n",
                         argv[0]);
                exit (EX_OSERR);
        }

#ifdef FILTER_ENABLE
        if (config.filter)
                filter_init ();
#endif /* FILTER_ENABLE */

        // 获得监听端口的fd
        if (child_listening_sockets(config.listen_addrs, config.port) < 0) {
                fprintf (stderr, "%s: Could not create listening sockets.\n",
                         argv[0]);
                exit (EX_OSERR);
        }

        // 如果是root运行则切换用户
        if (geteuid () == 0)
                change_user (argv[0]);
        else
                log_message (LOG_WARNING,
                             "Not running as root, so not changing UID/GID.");

       // 创建日志
        if (setup_logging ()) {
                exit (EX_SOFTWARE);
        }

        /* Create pid file after we drop privileges */
        if (config.pidpath) {
                if (pidfile_create (config.pidpath) < 0) {
                        fprintf (stderr, "%s: Could not create PID file.\n",
                                 argv[0]);
                        exit (EX_OSERR);
                }
        }

		// 初始化子进程相关信息,并在此处逐个启动子进程,子进程数由配置中的startserver决定
        if (child_pool_create () < 0) {
                fprintf (stderr,
                         "%s: Could not create the pool of children.\n",
                         argv[0]);
                exit (EX_SOFTWARE);
        }

        /* These signals are only for the parent process. */
        log_message (LOG_INFO, "Setting the various signals.");

		// 注册SIGCHLD 在taskesig中waitpid非阻塞等待子进程退出
        if (set_signal_handler (SIGCHLD, takesig) == SIG_ERR) {
                fprintf (stderr, "%s: Could not set the \"SIGCHLD\" signal.\n",
                         argv[0]);
                exit (EX_OSERR);
        }

		//注册SIGTERM ,处理父进程退出事件
        if (set_signal_handler (SIGTERM, takesig) == SIG_ERR) {
                fprintf (stderr, "%s: Could not set the \"SIGTERM\" signal.\n",
                         argv[0]);
                exit (EX_OSERR);
        }


		// 收到SIGHUP杀死所有子进程
        if (set_signal_handler (SIGHUP, takesig) == SIG_ERR) {
                fprintf (stderr, "%s: Could not set the \"SIGHUP\" signal.\n",
                         argv[0]);
                exit (EX_OSERR);
        }

        /* Start the main loop */
        log_message (LOG_INFO, "Starting main loop. Accepting connections.");

		// 其实是父进程循环,当收到SIGTERM时退出
        child_main_loop ();

        log_message (LOG_INFO, "Shutting down.");

		// 向子进程发送退出信号
        child_kill_children (SIGTERM);

        // 关闭listenfds中的fd,并且释放相关内存
        child_close_sock ();

        /* Remove the PID file */
        if (unlink (config.pidpath) < 0) {
                log_message (LOG_WARNING,
                             "Could not remove PID file \"%s\": %s.",
                             config.pidpath, strerror (errno));
        }

#ifdef FILTER_ENABLE
        if (config.filter)
                filter_destroy ();
#endif /* FILTER_ENABLE */

        shutdown_logging ();

        return EXIT_SUCCESS;
}

可以看到,tinyproxy的main函数启动结构非常简单,是一个最基本的多进程unix服务端程序。其工作核心就在child.c/child_main函数中完成的

父进程的工作

父进程主要起到一个增加子进程,当等待的子进程小于了配置的最小空闲数后,则在最大进程数的范围内,新开子线程;另外也负责在收到关闭信号后,关闭所有子进程。而进程数收缩的工作,由子线程自身在每次完成一次转发后review当前空闲的连接是否大于了最大空闲数,检查后会结束自己的声明。

子进程的工作

子进程的实现就是一个个worker。单个worker使用select对父进程fork出来的listenfds进行监听,当有连接事件发生时,worker通过accept获取到客户端socket的资源,然后读取连接数据做代理转发。

handle_connection函数是主要的处理流程。顺序读取socket中的数据,以http协议的约定解析。如第一行数据是协议版本,之后每一行都是http header数据,直到空行。tinyproxy最多支持10000个header头,宏的形式写死在代码中。

存储headers的方式

tinyproxy解析到一个请求,会将他的请求头放到一个哈希表中。哈希函数如下:

for (hash = seed; *key != '\0'; key++) {
                hash = ((hash << 5) + hash) ^ tolower (*key);
        }

seed是一个随机数,其中的数学原理,还没有参透,改日再细看有什么玄机。(= =) 这个哈希表默认是一个大小4096的拉链法哈希表。

存储请求中的请求头后,再将配置文件中定义的用户自定义请求头写入这个哈希表。

检查请求头

此时一个请求的方法体还没有被从连接缓冲区中读出,因为还要对请求头做更多的检查。如解析http版本号,http method等;还比如支持https的重要对Connect方法的检查。在配置文件中必须指定接受Connect方法的端口。

...
 } else if (strcmp (request->method, "CONNECT") == 0) {
                if (extract_url (url, HTTP_PORT_SSL, request) < 0) {
                        indicate_http_error (connptr, 400, "Bad Request",
                                             "detail", "Could not parse URL",
                                             "url", url, NULL);
                        goto fail;
                }

                /* Verify that the port in the CONNECT method is allowed */
                if (!check_allowed_connect_ports (request->port,
                                                  config.connect_ports))
                {
                        indicate_http_error (connptr, 403, "Access violation",
                                             "detail",
                                             "The CONNECT method not allowed "
                                             "with the port you tried to use.",
                                             "url", url, NULL);
                        log_message (LOG_INFO,
                                     "Refused CONNECT method on port %d",
                                     request->port);
                        goto fail;
                }

                connptr->connect_method = TRUE;
        } else {
        ....

完成代理转发

之后就很简单了,根据请求头中指定的目标地址,建立tcp连接,将哈希表中的http头写入连接,之后“一次性”读出客户连接中的所有数据,再向目标连接replay。这里的一次性考虑到buffer大小,其实有可能是分段写的。

总结

之前写过一段时间unix后端程序,业务框架很重,对unix编程没有什么太深的感受。本次学习tinyproxy也是为了顺便熟悉一下unix编程。再次感受到了unix/C简单高效的编程之美,比干啃书有意思很多(逃)

Apr 9, 2017 - tinyproxy的使用

前言

       tinyproxy是一个开源的正向代理软件,仓库位于github,tinyproxy - a light-weight HTTP/HTTPS proxy daemon for POSIX operating systems

       提及代理技术,必谈Nginx。Nginx可能是地球上最著名的反向代理,也是最早解决C10K问题的服务器软件。但Nginx并不能满足作为一个正向代理的技术需求,因为Nginx不支持https的正向代理。

       不支持https的技术原因是Nginx没有实现http1.1 Connect方法。关于Connect和隧道技术,详见RFC2817。另外知乎上也有一个讲得很不错的答案。什么是HTTP隧道,怎么理解HTTP隧道呢?。隧道的含义大约就是帮助无法完成TLS握手的代理服务器透传可以完成TLS握手的客户端请求,而不再解析流量中的内容。

       没有实现Connect的结果就是Nginx无法将加密的https报文正确地代理到要去的地方,而Nginx作者的意思是,Nginx没有必要再去实现https的正向代理了,市面上已经有很多实现了。因此经过一些调查,选择了这款tinyproxy作为正向代理的研究对象。

使用

编译依赖(ubuntu14)
$ sudo apt-get install asciidoc autotools-dev automake
下载编译
$ git clone https://github.com/tinyproxy/tinyproxy
$ cd tinyproxy
$ ./autogen.sh
$ ./configure
$ make
$ make install
apt安装

或者可以选择直接安装

$ sudo apt-get install tinyproxy
配置文件简介

默认配置文件为/etc/tinyproxy.conf

重要配置如下:

代理端口
Port 8888

连接最大空闲时间
Timeout 600

日志文件位置和日志级别
Logfile "/var/log/tinyproxy/tinyproxy.log"
LogLevel Info

最大客户端数量
MaxClients 100

最大最小服务进程
MinSpareServers 5
MaxSpareServers 20

其实进程数
StartServers 10

子进程最大连接数
MaxRequestsPerChild 0

网段限制,客户端必须位于网段内,否则请求被拒绝
Allow 127.0.0.1
Allow 10.0.0.0/8

http header Via的值
ViaProxyName "tinyproxy"

http Connect 端口
ConnectPort 443
ConnectPort 563
golang example
func main(){
	proxy := func(_ *http.Request) (*url.URL, error) {
		return url.Parse("http://127.0.0.0:8888")
	}
	transport := &http.Transport{Proxy: proxy}
	proxy_client = &http.Client{
		Transport:transport,
	}
	proxy_client.Get(...)
}