重要的全局变量

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简单高效的编程之美,比干啃书有意思很多(逃)