GIT 傳輸協議實現

GIT 傳輸協議實現

在 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 若是筆者有時間會實現一個跨平臺的簡易版並開源。

相關文章
相關標籤/搜索