您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關Git的三種傳輸協議及實現的方法教程,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
Git HTTP 協議主要分為兩種,一種是啞協議(Dump),另外一種是智能協議(Smart),也是目前各個提供 Git 托管服務普遍所采用的協議。
官方文檔:https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
在 Git 1.6.6 版本之前是只提供啞協議的,啞協議只需要一個標準的 HTTP 靜態文件服務,這個服務只需要能夠提供文件的下載即可,Git 客戶端會自動進行文件的遍歷和拉取。
無論是啞協議還是智能協議,Git 在使用 HTTP 協議進行 Fetch 操作的時候,總是要先獲取info/refs
文件,這個文件是在裸倉庫的目錄下的,如果你已經有一個通過 Git 拉取的倉庫,這個文件就在倉庫根目錄的.git/info/refs
。不過這個文件一般情況下是沒有的,它需要你在相應的目錄執行git update-server-info
進行生成:
? .git git:(master) cat info/refs 21f45f60fa582d085497fb2d3bb50163e59891ee refs/heads/history ef8021acf4c29eb35e3084b7dc543c173d67ad2a refs/heads/master
文件內容主要是服務端上每個引用的版本,客戶端拿到這些引用之后,就可以跟本地的引用進行對比,對于缺失的對象文件,則通過 HTTP 的方式進行下載。
Tips1: 關于 Git 存儲格式請參見:https://github.com/git/git/blob/master/Documentation/gitrepository-layout.txt,后續文章會展開介紹
Tips2: 如果有更新都要手動執行
update-server-info
?答案是 No,可以配置 Git 服務端的post-receive
鉤子自動執行更新
所以,一次通過啞協議 Clone 的過程如下:(U:用戶 C:客戶端 S:服務端)
U:git clone https://gitee.com/kesin/taskover.git C:GET https://gitee.com/kesin/taskover.git/info/refs S:Response with taskover.git/info/refs C:GET https://gitee.com/kesin/taskover.git/HEAD (默認分支) S:Response with taskover.git/HEAD C:Get https://gitee.com/kesin/taskover.git/objects/ef/8021acf4c29eb35e3084b7dc543c173d67ad2a 開始遍歷對象,找出那些本地沒有的,去服務端獲取,如果服務端無法直接獲取,則從 Pack 文件中進行抓取,直到全部拿到 C:根據 HEAD 中的默認分支執行 checkout 操作檢出到本地
上面的那些地址是為了演示用,實際 Gitee 僅支持智能協議而不支持啞協議,畢竟對于一個公有云服務是不安全的。關于對象如何遍歷這里也不再展開,后續文章會介紹
啞協議的實現非常簡單,通過nginx
即可簡單實現,只需要配置一個靜態文件服務器,然后將 Git 倉庫以單目錄的形式放上去即可;也可以使用 Go 快速實現一個簡單的 Git HTTP Dump Server:
// From: https://gitee.com/kesin/go-git-protocols/blob/master/http-dumb/git-http-dumb.go // Usage: ./http-dumb -repo=/xxx/xxx/xxx/ -port=8881 func main() { repo := flag.String("repo", "/Users/zoker/Tmp/repositories", "Specify a repositories root dir.") port := flag.String("port", "8881", "Specify a port to start process.") flag.Parse() http.Handle("/", http.FileServer(http.Dir(*repo))) fmt.Printf("Dumb http server start at port %s on dir %s \n", *port, *repo) _ = http.ListenAndServe(fmt.Sprintf(":%s", *port), nil) }
HTTP 智能協議與啞協議最大的區別在于:啞協議在獲取想要的數據的時候要自行指定文件資源的網絡地址,并且通過多次的下載操作來達到目的;而智能協議的主動權則掌握在服務端,服務端提供的info/refs
可以動態更新,并且可以通過客戶端傳來的參數,決定本次交互客戶端所需要的最小對象集,并打包壓縮發給客戶端,客戶端會進行解壓來拿到自己想要的數據
通過監聽對應端口,我們可以看到整個過程客戶端發送了兩次請求:
引用發現 GET https://gitee.com/kesin/taskover/info/refs?service=git-{upload|receive}-pack
數據傳輸 POST https://gitee.com/kesin/taskover/git-{upload|receive}-pack
Git HTTP 協議要求無論是下載操作還是上傳操作,都必須先執行引用發現,也就是需要知道服務端的各個引用的版本信息,這樣的話才能讓服務端或者客戶端知道兩方之間的差異以及需要什么樣的數據。
與啞協議不同的是,智能協議的的服務端是動態服務器,能夠根據期望來提供相關的引用信息,你可以根據自己的業務需求來決定想讓客戶端知道什么樣的信息,通過抓包我們可以看到客戶端請求的數據以及 Gitee 服務端返回的引用信息格式
# 請求體 GET http://git.oschina.net/kesin/getingblog.git/info/refs?service=git-upload-pack HTTP/1.1 Host: git.oschina.net User-Agent: git/2.24.3 (Apple Git-128) Accept-Encoding: deflate, gzip Proxy-Connection: Keep-Alive Pragma: no-cache # Gitee 響應 HTTP/1.1 200 OK Cache-Control: no-cache, max-age=0, must-revalidate Connection: keep-alive Content-Type: application/x-git-upload-pack-advertisement Expires: Fri, 01 Jan 1980 00:00:00 GMT Pragma: no-cache Server: nginx X-Frame-Options: DENY X-Gitee-Server: Brzox/3.2.3 X-Request-Id: 96e0af82-dffe-4352-9fa5-92f652ed39c7 Transfer-Encoding: chunked 001e# service=git-upload-pack 0000 010fca6ce400113082241c1f45daa513fabacc66a20d HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/testbody object-format=sha1 agent=git/2.29.2 003c351bad7fdb498c9634442f0c3f60396e8b92f4fb refs/heads/dev 004092ad3c48e627782980f82b0a8b05a1a5221d8b74 refs/heads/dev-pro 0040ae747d0a0094af3d27ee86c33e645139728b2a9a refs/heads/develop 0000
我們需要關注的信息在 Header 和 Body,這里簡單介紹一下,更詳細的介紹請參見上面提到的http-protocol.txt
文檔 Header 包含了一些約定:
Cache-Control 必須禁止緩存,不然可能看不到最新的提交信息
Content-Type 必須是application/x-$servicename-advertisement
,不然客戶端會以啞協議的方式去處理
客戶端需要驗證返回的狀態碼,如果是401
那么就提示輸入用戶名密碼
另外我們能看到返回的 Body 格式跟啞協議所用的info/refs
內容是不一樣的,這里是智能協議所約定的格式,客戶端根據這個來識別支持的屬性和驗證信息,這是一個pkt-line
格式的數據:
客戶端需要驗證第一首行的四個字符符合正則^[0-9a-f]{4}#
,這里的四個字符是代表后面內容的長度
客戶端需要驗證第一行是# service=$servicename
服務端得保證每一行結尾需要包含一個LF
換行符
服務端需要以0000
標識結束本次請求響應
在HEAD
引用后還有一系列的服務端能力的參數,這些參數會告訴客戶端服務端具有什么樣的能力,比如可以通過multi_ack
模式進行數據交互等,這里不在贅述。再往后就是具體的每一個引用的信息,每行的開頭四個字符均是本行的長度。
在介紹啞協議的時候,我們使用通過git update-server-info
命令生成的info/refs
文件,但是很明顯我們在智能協議這里無法直接使用,因為它不符合pkt-line
的格式,所以 Git 提供另外一種方式:通過 git upload-pack
命令來直接獲取最新的pkt-line
格式的引用信息,來看看它的參數支持:
--[no-]strict Do not try <directory>/.git/ if <directory> is no Git directory. --timeout=<n> Interrupt transfer after <n> seconds of inactivity. --stateless-rpc Perform only a single read-write cycle with stdin and stdout. This fits with the HTTP POST request processing model where a program may read the request, write a response, and must exit. --advertise-refs Only the initial ref advertisement is output, and the program exits immediately. This fits with the HTTP GET request model, where no request content is received but a response must be produced. <directory> The repository to sync from.
upload-pack
是用來發送對象給客戶端的一個遠程調用模塊,但是它提供了--stateless-rpc
和--advertise-refs
參數,能夠讓我們快速拿到當前的引用狀態并退出,我們在服務端的裸倉庫目錄執行就可以直接拿到最新的引用信息:
? .git git:(master) git upload-pack --stateless-rpc --advertise-refs . 010aef8021acf4c29eb35e3084b7dc543c173d67ad2a HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/2.24.3.(Apple.Git-128) 003fef8021acf4c29eb35e3084b7dc543c173d67ad2a refs/heads/master 0000%
這里的內容是不是似曾相識,跟上面我們抓包獲取到的 Gitee 返回的引用數據格式一樣,只是少了首行的# service=git-upload-pack
,所以我們現在思路非常清晰,可以先來實現第一步引用發現的服務端的處理,通過對參數的解析,我們可以拿到倉庫名稱以及相應的操作名稱,就可以進一步整理出客戶端要的響應格式:
// 支持 upload-pack 和 receive-pack 兩種操作引用發現的處理 func handleRefs(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) repoName := vars["repo"] repoPath := fmt.Sprintf("%s%s", *repoRoot, repoName) service := r.FormValue("service") pFirst := fmt.Sprintf("# service=%s\n", service) // 本示例僅處理 protocol v1 handleRefsHeader(&w, service) // Headers 處理 cmdRefs := exec.Command("git", service[4:], "--stateless-rpc", "--advertise-refs", repoPath) refsBytes, _ := cmdRefs.Output() // 獲取 pkt-line 數據 responseBody := fmt.Sprintf("%04x# service=%s\n0000%s", len(pFirst)+4, service, string(refsBytes)) // 拼接 Body _, _ = w.Write([]byte(responseBody)) } // 按照要求設置 Headers func handleRefsHeader(w *http.ResponseWriter, service string) { cType := fmt.Sprintf("application/x-%s-advertisement", service) (*w).Header().Add("Content-Type", cType) (*w).Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") (*w).Header().Set("Pragma", "no-cache") (*w).Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") }
上面我們也提到了,無論是拉取還是推送,都需要先進行引用發現,實際上upload-pack
和receive-pack
所處理的差別僅僅是調用的命令不同而已,這一點我們也在handleRefs
函數里面做了相應的兼容處理,這里不再贅述。
數據傳輸分為兩部分:客戶端向服務端傳輸(Push)、服務端向客戶端傳輸(Fetch)。兩者的區別在于:
Fetch 操作在獲取引用發現之后,由 服務端 計算出客戶端想要的數據,并把數據以pkt-line
的格式POST
給服務端,由服務端進行Pack
的計算和打包,將包作為 POST
的響應發送給客戶端,客戶端進行解壓和引用更新
Push 操作獲取到服務端的引用列表后,由 客戶端 本地計算出客戶端所缺失的數據,將這些數據打包,并POST
給服務端,服務端接收到后進行解壓和引用更新
Fetch 操作其實用到了上面我們提到的upload-pack
,它是一個發送對象給客戶端的遠程調用模塊,為了實現拉取功能,我們只需要在服務端啟動 git upload-pack --stateless-rpc
,這個命令阻塞的接收一串參數,而這串參數是客戶端的第二次請求發送過來的,把它傳遞給這個命令,Git 就會自動的計算客戶端所需要的最小對象集并打包,以流的形式返回這個包數據,我們只需要把這個包作為 POST 請求的響應發給客戶端就好了。
那么,在 Fetch 操作中,客戶端第二次 POST 請求發過來的數據是什么呢,我們也來抓個包分析一下:
POST http://gitee.com/kesin/bigRepo/git-upload-pack HTTP/1.1 Host: gitee.com User-Agent: git/2.24.3 (Apple Git-128) Accept-Encoding: deflate, gzip Proxy-Connection: Keep-Alive Content-Type: application/x-git-upload-pack-request Accept: application/x-git-upload-pack-result Content-Length: 443 00b4want bee4d57e3adaddf355315edf2c046db33aa299e8 multi_ack_detailed no-done side-band-64k thin-pack include-tag ofs-delta deepen-since deepen-not agent=git/2.24.3.(Apple.Git-128) 00000032have 82a8768e7fd48f76772628d5a55475c51ea4fa2f 0032have 4f7a2ea0920751a5501debbbc1debc403b46d7a0 0032have 7c141974a30bd218d4257d4292890a9008d30111 0032have f6bb00364bd5000a45185b9b16c028f485e842db 0032have 47b7bd17fcb7de646cf49a26efb43c7b708498f3 0009done
客戶端在拿到第一次引用發現服務端返回的數據后,會根據服務端所提供的能力(capabilities
)列表以及引用(refs
)列表來計算出第二次需要發送的數據,比如會根據服務端的能力列表來決定客戶端和服務端通信所需要的能力參數,這些能力參數服務端必須全部支持。另外,客戶端發送的數據必須包含一個want
指令,我們在 Clone 一個倉庫的時候所發送的數據全部都是want
指令,而不包含have
指令,因為本地什么都沒有;而在進行有數據的更新的 Fetch 操作的時候,就會有have
指令。客戶端會根據返回的引用信息計算出所需要的 Commit
、Common Commit
以及 自己有服務端沒有的 Commit
,并將這些數據一次性的通過第二次請求發給服務端,具體客戶端的協商過程可以參見http-protocol.txt
,這里不再贅述。
服務端在收到這些數據之后,會先確認want
指令所指定的對象是否都能夠在引用中找到,如果沒有want
指令或者指令指定的對象中有不包含在服務端的,則會返回給客戶端錯誤信息,服務端根據這些信息計算出客戶端所需要的對象的集合,并把這些對象打包返回給客戶端,客戶端接收后解壓包并更新引用。
Push 操作大同小異,只不過在第二步的時候,客戶端會根據服務端的引用信息計算出服務端所需要的對象,直接通過 Post 請求發送給服務端,并同時附帶一些指令信息,比如新增、刪除、更新哪些引用,以及更新前后的版本,具體格式如下:
// https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt#L474 C: POST $GIT_URL/git-receive-pack HTTP/1.0 C: Content-Type: application/x-git-receive-pack-request C: C: ....0a53e9ddeaddad63ad106860237bbf53411d11a7 441b40d833fdfa93eb2908e52742248faf0ee993 refs/heads/maint\0 report-status C: 0000 C: PACK....
這里的包數據格式為"PACK" <binary data>
,會以PACK
開頭。服務端接收到這些數據后,啟動一個遠程調用命令receive-pack
,然后將數據以管道的形式傳給這個命令即可。
所以,整個數據傳輸的過程無非就是客戶端與服務端的upload-pack
和receive-pack
對規定格式的數據交換而已,根據這個思路,我們可以繼續完善我們的 Smart Git HTTP Server,來增加對第二步的處理能力:
func processPack(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) repoName := vars["repo"] // request repo not end with .git is supported with upload-pack repoPath := fmt.Sprintf("%s%s", *repoRoot, repoName) service := vars["service"] handlePackHeader(&w, service) // 啟動一個進程,通過標準輸入輸出進行數據交換 cmdPack := exec.Command("git", service[4:], "--stateless-rpc", repoPath) cmdStdin, err := cmdPack.StdinPipe() cmdStdout, err := cmdPack.StdoutPipe() _ = cmdPack.Start() // 客戶端和服務端數據交換 go func() { _, _ = io.Copy(cmdStdin, r.Body) _ = cmdStdin.Close() }() _, _ = io.Copy(w, cmdStdout) _ = cmdPack.Wait() // wait for std complete }
Git 協議以及 SSH 協議都是四層的傳輸協議,而 HTTP 則是七層的傳輸協議,受限于 HTTP 協議的特點,HTTP 在 Git 相關的操作上存在傳輸限制、超時等問題,這個問題在大倉庫的傳輸中尤為明顯,相比與 HTTP 而言,Git 以及 SSH 協議在傳輸上更簡單而且更穩定。
官方文檔:https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
Git 協議最大的優勢就是速度快,因為它沒有 HTTP 的傳輸協議的條條框框,也沒有 SSH 加解密的成本,但受限于協議的缺點,Git 協議常用于開源項目的下載,不作為私有項目的傳輸協議。
在上面我們研究 HTTP 智能協議的實現的時候,我們知道 Git 客戶端跟服務端的交互主要包含兩個步驟:
獲取服務端的引用
客戶端根據服務端的引用數據與服務端進行數據交換
Git 協議也是如此,只不過相比于 HTTP 協議,Git 協議直接在四層與服務端建立連接,通過這個長鏈接直接完成兩個步驟:
MMwkPXVJQrK2g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 在使用 Git 協議操作的時候,首先客戶端會把相關的信息發給服務端,這個信息的格式同樣的采用pkt-line
的格式:
003egit-upload-pack /project.git\0host=myserver.com\0\0version=1\0
其中包含了命令、倉庫名稱、Host 等相關信息,服務端建立連接之后,接收到這串信息,需要對其中的信息進行加工,找到對應的倉庫所在的位置也就是目錄,當所有的信息都符合要求之后,只需要在服務端啟動upload-pack
命令即可,這里需要注意的是我們不需要添加--stateless-rpc
參數,直接git upload-pack {repo_path}
,這個命令啟動后會馬上返回相關的引用信息并且阻塞等待下一次信息的輸入:
? hello git:(master) ? git upload-pack . 010234d8ed9a9f73d2cac9f50a8c8c03e4643990a2bf HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master agent=git/2.24.3.(Apple.Git-128) 003f34d8ed9a9f73d2cac9f50a8c8c03e4643990a2bf refs/heads/master 0000
這個時候我們所做的其實就是數據的轉發,命令的標準輸出信息我們原封不動的發送給客戶端,客戶端則會進行跟 HTTP 協議類似的處理產生數據,接著會把數據發給服務端,我們再原封不動的發給git upload-pack {repo_path}
命令的標準輸入,然后服務端處理完成后會把相應的包通過標準輸出返回,我們原封不動的發給客戶端,就完成了一次 Fetch 操作,而 Push 的 receive-pack
操作原理相同,這里不再贅述。
需要注意的是,如果客戶端發送的信息不符合要求,或者處理過程中出現了問題,我們返回錯誤告知客戶端,這個錯誤的格式也是pkt-line
格式的,以ERR
開頭:
// error-line = PKT-LINE("ERR" SP explanation-text) func exitSession(conn net.Conn, err error) { errStr := fmt.Sprintf("ERR %s", err.Error()) pktData := fmt.Sprintf("%04x%s", len(errStr)+4, errStr) _, _ = conn.Write([]byte(pktData)) _ = conn.Close() }
客戶端接收到這個信息之后,就會打印信息并關閉連接,整個過程的數據均可以通過轉包獲取到,感興趣的同學可以通過抓包來進一步加深了解 Git 協議的傳輸過程。
了解了 Git 協議的過程之后,我們就可以通過代碼來實現一個簡單的 Git 協議服務器:
func handleRequest(conn net.Conn) { //處理首次客戶端發來的數據,拿到 action 以及 倉庫信息 service, repo, err := parseData(conn) // 僅支持 Push 和 Fetch 操作 if service != "git-upload-pack" && service != "git-receive-pack" { exitSession(conn, errors.New("Not allowed command. \n")) } repoPath := fmt.Sprintf("%s%s", *repoRoot, repo) cmdPack := exec.Command("git", service[4:], repoPath) cmdStdin, err := cmdPack.StdinPipe() cmdStdout, err := cmdPack.StdoutPipe() _ = cmdPack.Start() // 客戶端服務端數據交換 go func() { _, _ = io.Copy(cmdStdin, conn) }() _, _ = io.Copy(conn, cmdStdout) err = cmdPack.Wait() }
SSH 協議也是應用的比較廣泛的一種 Git 傳輸協議,相比于 Git 協議,SSH 協議從數據傳輸和權限認證上都相對安全,但是受限于加解密的成本,速度會稍慢,但是這個時間成本在安全面前絕對是可以接受的。與 Git 協議比較,不同點是 SSH 協議傳輸的數據經過加密,相同點是 SSH 協議的傳輸過程與 Git 協議一致
SSH 的下載地址一般都是 git@gitee.com:kesin/go-git-protocols.git
這種形式的,在執行 Clone 或者 Push 的時候,會拆解成:
ssh user@example.com "git-upload-pack '/project.git'"
所以 SSH 協議在首次傳參的時候與 Git 協議的格式不同,其他情況基本一致,比如引用發現、Packfile 機制、錯誤處理等等,這里都不再做延伸,可以參加官方文檔。
明白 SSH 協議是怎么回事后,我們想要實現一個 Git SSH 服務器就比較明確了,只需要實現一個 SSH Server 并且在對應的 Session 做對應的數據傳輸即可,我們來實現一個簡單的 Git SSH 服務,代碼如下:
func main() { // init host key and public key authentication var hostOption ssh.Option hostOption = ssh.HostKeyFile(*hostKeyPath) keyHandler := func(ctx ssh.Context, key ssh.PublicKey) bool { // replace your public key auth logic here pubKeyStr := gossh.MarshalAuthorizedKey(key) return true // set false to use password authentication } keyOption := ssh.PublicKeyAuth(keyHandler) // password validate authentication pwdHandler := func(ctx ssh.Context, password string) bool { // replace your own password auth logic here if ctx.User() == "zoker" && password == "zoker" { return true } return false } pwdOption := ssh.PasswordAuth(pwdHandler) // process ssh session pack ssh.Handle(func(s ssh.Session) { handlePack(s) // 處理請求 }) addr := fmt.Sprintf(":%s", *port) log.Printf("Starting ssh server on port %s \n", *port) log.Fatal(ssh.ListenAndServe(addr, nil, hostOption, pwdOption, keyOption)) } func handlePack(s ssh.Session) { args := s.Command() service := args[0] repoName := args[1] // allowed command if service != "git-upload-pack" && service != "git-receive-pack" { exitSession(s, errors.New("Not allowed command. \n")) } repoPath := fmt.Sprintf("%s%s", *repoRoot, repoName) // 啟動標準輸入輸出進行數據交換,下面的處理是否似曾相識?沒錯,Git 協議也是同樣的處理方式 cmdPack := exec.Command("git", service[4:], repoPath) cmdStdin, err := cmdPack.StdinPipe() cmdStdout, err := cmdPack.StdoutPipe() _ = cmdPack.Start() go func() { _, _ = io.Copy(cmdStdin, s) }() _, _ = io.Copy(s, cmdStdout) _ = cmdPack.Wait() }
以上就是Git的三種傳輸協議及實現的方法教程,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。