GIT transfer protocol implementation
Among the three mainstream transfer protocols of GIT, HTTP SSH GIT, the GIT protocol is the least used protocol (that is, the protocol whose URL starts with git://). This is because the git protocol has almost no permission control, either all can be read, all can be written, or all can be read and written. So for code hosting platforms, the purpose of the git protocol is only to support read-only access to public projects.
Among the various transmission protocols of git, the git protocol is undoubtedly the most efficient. HTTP is limited by the characteristics of HTTP, and the transmission process requires the construction of HTTP requests and responses. If it is HTTPS, encryption and decryption are also involved. In addition, HTTP timeout settings and packet size limits will affect user experience.
The performance issues of the SSH protocol mainly focus on encryption and decryption. Of course, these costs are acceptable compared to the user's information security.
The git protocol is actually equivalent to SSH without encryption and verification, so there is no way to talk about permission control. However, in fact, if some synchronization services within the code hosting platform are implemented using the git protocol, the performance will be greatly improved.
Transfer protocol specifications
The technical documentation of the git protocol can be found from Documentation/technical in the git source code directory, that is, Packfile transfer protocols After creating a TCP connection, the git client takes the lead in sending the request body. The request format is described based on BNF as follows:
git-proto-request = request-command SP pathname NUL [ host-parameter NUL ] request-command = "git-upload-pack" / "git-receive-pack" / "git-upload-archive" ; case sensitive pathname = *( %x01-ff ) ; exclude NUL host-parameter = "host=" hostname [ ":" port ]
An example is as follows:
0033git-upload-pack /project.git
在 C 语言中,有 popen 函数,可以创建一个进程,并将进程的标准输出或标准输入创建成一个文件指针,即 FILE*其他可以使用 C 函数的语言很多也提供了类似的实现,比如 Ruby,基于 Ruby 的 git HTTP 服务器 grack 正是使用 的 popen,相比与其他语言改造的 popen,C 语言中 popen 存在了一些缺陷,比如无法同时读写,如果要输出标准 错误,需要在命令参数中额外的将标准错误重定向到标准输出。
在 musl libc 的中,popen 的实现如下:
FILE *popen(const char *cmd, const char *mode) { int p[2], op, e; pid_t pid; FILE *f; posix_spawn_file_actions_t fa; if (*mode == 'r') { op = 0; } else if (*mode == 'w') { op = 1; } else { errno = EINVAL; return 0; } if (pipe2(p, O_CLOEXEC)) return NULL; f = fdopen(p[op], mode); if (!f) { __syscall(SYS_close, p[0]); __syscall(SYS_close, p[1]); return NULL; } FLOCK(f); /* If the child's end of the pipe happens to already be on the final * fd number to which it will be assigned (either 0 or 1), it must * be moved to a different fd. Otherwise, there is no safe way to * remove the close-on-exec flag in the child without also creating * a file descriptor leak race condition in the parent. */ if (p[1-op] == 1-op) { int tmp = fcntl(1-op, F_DUPFD_CLOEXEC, 0); if (tmp < 0) { e = errno; goto fail; } __syscall(SYS_close, p[1-op]); p[1-op] = tmp; } e = ENOMEM; if (!posix_spawn_file_actions_init(&fa)) { if (!posix_spawn_file_actions_adddup2(&fa, p[1-op], 1-op)) { if (!(e = posix_spawn(&pid, "/bin/sh", &fa, 0, (char *[]){ "sh", "-c", (char *)cmd, 0 }, __environ))) { posix_spawn_file_actions_destroy(&fa); f->pipe_pid = pid; if (!strchr(mode, 'e')) fcntl(p[op], F_SETFD, 0); __syscall(SYS_close, p[1-op]); FUNLOCK(f); return f; } } posix_spawn_file_actions_destroy(&fa); } fail: fclose(f); __syscall(SYS_close, p[1-op]); errno = e; return 0; }
在 Windows Visual C++ 中,popen 源码在 C:\Program Files (x86)\Windows Kits\10\Source\${SDKVersion}\ucrt\conio\popen.cpp , 按照 MSDN 文档说明,Windows 32 GUI 程序,即 subsystem 是 Windows 的程序,使用 popen 可能导致程序无限失去响应。
所以在笔者实现 git-daemon 及其他 git 服务器时,都不会使用 popen 这个函数。
为了支持跨平台和简化编程,笔者在实现 svn 代理服务器时就使用了 Boost Asio 库,后来也用 Asio 实现过一个 git 远程命令服务, 每一个客户端与服务器连接后,服务器启动程序,需要创建 3 条管道,分别是 子进程的标准输入 输出 错误,即 stdout stdin stderr, 然后注册读写异步事件,将子进程的输出与错误写入到 socket 发送出去,读取 socket 写入到子进程的标准输入中。
在 POSIX 系统中,boost 有一个文件描述符类 boost::asio::posix::stream_descriptor 这个类不能是常规文件,以前用 go 做 HTTP 前端 没注意就 coredump 掉。
在 Windows 系统中,boost 有文件句柄类 boost::asio::windows::stream_handle 此处的文件应当支持随机读取,比如命名管道(当然 在 Windows 系统的,匿名管道实际上也是命名管道的一种特例实现)。
以上两种类都支持 async_read async_write ,所以可以很方便的实现异步的读取。
上面的做法,唯一的缺陷是性能并不是非常高,代码逻辑也比较复杂,当然好处是,错误异常可控一些。
在 Linux 网络通信中,类似与 git 协议这样读取子进程输入输出的服务程序的传统做法是,将 子进程的 IO 重定向到 socket, 值得注意的是 boost 中 socket 是异步非阻塞的,然而,git 命令的标准输入标准错误标准输出都是同步的,所以在 fork 子进程之 前,需要将 socket 设置为同步阻塞,当 fork 失败时,要设置回来。
socket_.native_non_blocking(false);
另外,为了记录子进程是否异常退出,需要注册信号 SIGCHLD 并且使用 waitpid 函数去等待,boost 就有 boost::asio::signal_set::async_wait 当然,如果你开发这样一个服务,会发现,频繁的启动子进程,响应信号,管理连接,这些操作才是性能的短板。
一般而言,Windows 平台的 IO 并不能重定向到 socket,实际上,你如果使用 IOCP 也可以达到相应的效率。还有,Windows 的 socket API WSASocket WSADuplicateSocket 复制句柄 DuplicateHandle ,这些可以好好利用。
其他
对于非代码托管平台的从业者来说,上面的相关内容可能显得无足轻重,不过,网络编程都是殊途同归,最后核心理念都是类似的。关于 git-daemon 如果笔者有时间会实现一个跨平台的简易版并开源。