GIT 傳輸協(xié)議實現(xiàn)
在 GIT 的三種主流傳輸協(xié)議 HTTP SSH GIT 中,GIT 協(xié)議是最少被使用的協(xié)議(也就是 URL 以?git://?開始的協(xié)議)。 這是由于 git 協(xié)議的權限控制幾乎沒有,要么全部可讀,要么全部可寫,要么全部可讀寫。所以對于代碼托管平臺來說, git 協(xié)議的目的僅僅是為了支持 公開項目的只讀訪問。
在 git 的各種傳輸協(xié)議中,git 協(xié)議無疑是最高效的,HTTP 受限于 HTTP 的特性,傳輸過程需要構造 HTTP 請求和響應。 如果是 HTTPS 還涉及到加密解密。另外 HTTP 的超時設置,以及包體大小限制都會影響用戶體驗。
而 SSH 協(xié)議的性能問題主要集中在加密解密上。當然相對于用戶的信息安全來說,這些代價都是可以接受。
git 協(xié)議實際上相當于 SSH 無加密無驗證,也就無從談起權限控制,但實際上代碼托管平臺內(nèi)部的一些同步服務,如果使用 git 協(xié)議實現(xiàn),將會得到很大的性能提升。
傳輸協(xié)議規(guī)范
git 協(xié)議的技術文檔可以從 git 源碼目錄的?Documentation/technical?找到,即?Packfile transfer protocols?創(chuàng)建 TCP 連接后,git 客戶端率先發(fā)送請求體,請求格式基于 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 ]
一個例子如下:
0033git-upload-pack /project.git\0host=myserver.com\0
在 git 的協(xié)議中,pkt-line 是非常有意思的設計,行前 4 個字節(jié)表示整個行長,長度包括其前 4 字節(jié), 但是有個特例,0000 其代表行長為 0,但其自身長度是 4。
下面是一個關于請求的結構體:
struct GitRequest{ std::string command; std::string path; std::string host; };
git 有自帶的 git-daemon 實現(xiàn),這個服務程序監(jiān)聽 9418 端口,在接收到客戶端的請求后,先要判斷 command 是 否是被允許的,git 協(xié)議中有 fetch 和 push 以及 archive 之類的操作,分別對應的服務器上的命令是 git-upload-pack git-receive-pack git-upload-archive。HTTP 只會支持前兩種,SSH 會支持三種,而 代碼托管平臺的 git 通常支持的 是 git-upload-pack git-upload-archive。
當不允許的命令被接入時需要發(fā)送錯誤信息給客戶端,這個信息在不同的 git-daemon 實現(xiàn)中也不一樣,大體 如下所示。
001bERR service not enabled
git-daemon 將對請求路徑進行轉換,以期得到在服務器上的絕對路徑,同時可以判斷路徑是否存在,不存在時 可以給客戶端發(fā)送 Repository Not Found。而 host 可能時域名也可能時 ip 地址,當然也可以包括端口。 服務器可以在這里做進一步的限制,出于安全考慮應當考慮到請求是可以被偽造的。
客戶端發(fā)送請求過去后,服務器將啟動相應的命令,將命令標準錯誤和標準輸出的內(nèi)容發(fā)送給客戶端,將客戶端 傳輸過來的數(shù)據(jù)寫入到命令的標準輸入中來。
在請求體中,命令為 git-upload-pack /project.git 在服務器上運行時,就會類似
git-upload-pack ${RepositoriesRoot}/project.git
出于限制連接的目的,一般還會添加 --timeout=60 這樣的參數(shù)。timeout 并不是整個操作過程的超時。
與 HTTP 不同的是,git 協(xié)議的命令中沒有參數(shù) --stateless-rpc 和 --advertise-refs ,在 HTTP 中,兩個參數(shù)都存在時, 只輸出存儲庫的引用列表與 capabilities,與之對于的是 GET /repository.git/info/refs?service=git-upload(receive)-pack , 當只有 --stateless-rpc 時,等待客戶端的數(shù)據(jù),然后解析發(fā)送數(shù)據(jù)給客戶端,,與之對應的是 POST /repository.git/git-upload(receive)-pack。
進程輸入輸出的讀寫
在 C 語言中,有 popen 函數(shù),可以創(chuàng)建一個進程,并將進程的標準輸出或標準輸入創(chuàng)建成一個文件指針,即 FILE*其他可以使用 C 函數(shù)的語言很多也提供了類似的實現(xiàn),比如 Ruby,基于 Ruby 的 git HTTP 服務器 grack 正是使用 的 popen,相比與其他語言改造的 popen,C 語言中 popen 存在了一些缺陷,比如無法同時讀寫,如果要輸出標準 錯誤,需要在命令參數(shù)中額外的將標準錯誤重定向到標準輸出。
在 musl libc 的中,popen 的實現(xiàn)如下:
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 可能導致程序無限失去響應。
所以在筆者實現(xiàn) git-daemon 及其他 git 服務器時,都不會使用 popen 這個函數(shù)。
為了支持跨平臺和簡化編程,筆者在實現(xiàn) svn 代理服務器時就使用了 Boost Asio 庫,后來也用 Asio 實現(xiàn)過一個 git 遠程命令服務, 每一個客戶端與服務器連接后,服務器啟動程序,需要創(chuàng)建 3 條管道,分別是 子進程的標準輸入 輸出 錯誤,即 stdout stdin stderr, 然后注冊讀寫異步事件,將子進程的輸出與錯誤寫入到 socket 發(fā)送出去,讀取 socket 寫入到子進程的標準輸入中。
在 POSIX 系統(tǒng)中,boost 有一個文件描述符類?boost::asio::posix::stream_descriptor?這個類不能是常規(guī)文件,以前用 go 做 HTTP 前端 沒注意就 coredump 掉。
在 Windows 系統(tǒng)中,boost 有文件句柄類?boost::asio::windows::stream_handle?此處的文件應當支持隨機讀取,比如命名管道(當然 在 Windows 系統(tǒng)的,匿名管道實際上也是命名管道的一種特例實現(xiàn))。
以上兩種類都支持?async_read?async_write?,所以可以很方便的實現(xiàn)異步的讀取。
上面的做法,唯一的缺陷是性能并不是非常高,代碼邏輯也比較復雜,當然好處是,錯誤異??煽匾恍?。
在 Linux 網(wǎng)絡通信中,類似與 git 協(xié)議這樣讀取子進程輸入輸出的服務程序的傳統(tǒng)做法是,將 子進程的 IO 重定向到 socket, 值得注意的是 boost 中 socket 是異步非阻塞的,然而,git 命令的標準輸入標準錯誤標準輸出都是同步的,所以在 fork 子進程之 前,需要將 socket 設置為同步阻塞,當 fork 失敗時,要設置回來。
socket_.native_non_blocking(false);
另外,為了記錄子進程是否異常退出,需要注冊信號 SIGCHLD 并且使用 waitpid 函數(shù)去等待,boost 就有?boost::asio::signal_set::async_wait?當然,如果你開發(fā)這樣一個服務,會發(fā)現(xiàn),頻繁的啟動子進程,響應信號,管理連接,這些操作才是性能的短板。
一般而言,Windows 平臺的 IO 并不能重定向到 socket,實際上,你如果使用 IOCP 也可以達到相應的效率。還有,Windows 的 socket API WSASocket WSADuplicateSocket 復制句柄 DuplicateHandle ,這些可以好好利用。
其他
對于非代碼托管平臺的從業(yè)者來說,上面的相關內(nèi)容可能顯得無足輕重,不過,網(wǎng)絡編程都是殊途同歸,最后核心理念都是類似的。關于 git-daemon 如果筆者有時間會實現(xiàn)一個跨平臺的簡易版并開源。

熱AI工具

Undress AI Tool
免費脫衣服圖片

Undresser.AI Undress
人工智能驅動的應用程序,用于創(chuàng)建逼真的裸體照片

AI Clothes Remover
用于從照片中去除衣服的在線人工智能工具。

Clothoff.io
AI脫衣機

Video Face Swap
使用我們完全免費的人工智能換臉工具輕松在任何視頻中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的代碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
功能強大的PHP集成開發(fā)環(huán)境

Dreamweaver CS6
視覺化網(wǎng)頁開發(fā)工具

SublimeText3 Mac版
神級代碼編輯軟件(SublimeText3)