在 GIT 的三種主流傳輸協議 HTTP SSH GIT 中,GIT 協議是最少被使用的協議(也就是 URL 以 git://
開始的協議)。 這是因爲 git 協議的權限控制幾乎沒有,要麼所有可讀,要麼所有可寫,要麼所有可讀寫。因此對於代碼託管平臺來講, git 協議的目的僅僅是爲了支持 公開項目的只讀訪問。前端
在 git 的各類傳輸協議中,git 協議無疑是最高效的,HTTP 受限於 HTTP 的特性,傳輸過程須要構造 HTTP 請求和響應。 若是是 HTTPS 還涉及到加密解密。另外 HTTP 的超時設置,以及包體大小限制都會影響用戶體驗。c++
而 SSH 協議的性能問題主要集中在加密解密上。固然相對於用戶的信息安全來講,這些代價都是能夠接受。git
git 協議實際上至關於 SSH 無加密無驗證,也就無從談起權限控制,但實際上代碼託管平臺內部的一些同步服務,若是使用 git 協議實現,將會獲得很大的性能提高。github
git 協議的技術文檔能夠從 git 源碼目錄的 Documentation/technical
找到,即 Packfile transfer protocols 建立 TCP 鏈接後,git 客戶端率先發送請求體,請求格式基於 BNF 的描述以下:編程
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 ]
一個例子以下:windows
0033git-upload-pack /project.git\0host=myserver.com\0
安全
在 git 的協議中,pkt-line 是很是有意思的設計,行前 4 個字節表示整個行長,長度包括其前 4 字節, 可是有個特例,0000
其表明行長爲 0,但其自身長度是 4。服務器
下面是一個關於請求的結構體:網絡
struct GitRequest{ std::string command; std::string path; std::string host; };
git 有自帶的 git-daemon 實現,這個服務程序監聽 9418 端口,在接收到客戶端的請求後,先要判斷 command 是 否是被容許的,git 協議中有 fetch 和 push 以及 archive 之類的操做,分別對應的服務器上的命令是 git-upload-pack git-receive-pack git-upload-archive。HTTP 只會支持前兩種,SSH 會支持三種,而 代碼託管平臺的 git 一般支持的 是 git-upload-pack git-upload-archive。app
當不容許的命令被接入時須要發送錯誤信息給客戶端,這個信息在不一樣的 git-daemon 實現中也不同,大致 以下所示。
001bERR service not enabled
git-daemon 將對請求路徑進行轉換,以期獲得在服務器上的絕對路徑,同時能夠判斷路徑是否存在,不存在時 能夠給客戶端發送 Repository Not Found。而 host 可能時域名也可能時 ip 地址,固然也能夠包括端口。 服務器能夠在這裏作進一步的限制,出於安全考慮應當考慮到請求是能夠被僞造的。
客戶端發送請求過去後,服務器將啓動相應的命令,將命令標準錯誤和標準輸出的內容發送給客戶端,將客戶端 傳輸過來的數據寫入到命令的標準輸入中來。
在請求體中,命令爲 git-upload-pack /project.git 在服務器上運行時,就會相似
git-upload-pack ${RepositoriesRoot}/project.git
出於限制鏈接的目的,通常還會添加 --timeout=60
這樣的參數。timeout 並非整個操做過程的超時。
與 HTTP 不一樣的是,git 協議的命令中沒有參數 --stateless-rpc
和 --advertise-refs
,在 HTTP 中,兩個參數都存在時, 只輸出存儲庫的引用列表與 capabilities,與之對於的是 GET /repository.git/info/refs?service=git-upload(receive)-pack
, 當只有 --stateless-rpc 時,等待客戶端的數據,而後解析發送數據給客戶端,,與之對應的是 POST /repository.git/git-upload(receive)-pack
。
在 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 若是筆者有時間會實現一個跨平臺的簡易版並開源。