溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Git的三種傳輸協(xié)議及實現(xiàn)的方法教程

發(fā)布時間:2021-10-18 10:08:34 來源:億速云 閱讀:146 作者:柒染 欄目:編程語言

本篇文章給大家分享的是有關Git的三種傳輸協(xié)議及實現(xiàn)的方法教程,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

HTTP 傳輸協(xié)議

Git HTTP 協(xié)議主要分為兩種,一種是啞協(xié)議(Dump),另外一種是智能協(xié)議(Smart),也是目前各個提供 Git 托管服務普遍所采用的協(xié)議。

官方文檔:https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt

HTTP 啞協(xié)議(Dump Protocol)

在 Git 1.6.6 版本之前是只提供啞協(xié)議的,啞協(xié)議只需要一個標準的 HTTP 靜態(tài)文件服務,這個服務只需要能夠提供文件的下載即可,Git 客戶端會自動進行文件的遍歷和拉取。

無論是啞協(xié)議還是智能協(xié)議,Git 在使用 HTTP 協(xié)議進行 Fetch 操作的時候,總是要先獲取info/refs文件,這個文件是在裸倉庫的目錄下的,如果你已經(jīng)有一個通過 Git 拉取的倉庫,這個文件就在倉庫根目錄的.git/info/refs。不過這個文件一般情況下是沒有的,它需要你在相應的目錄執(zhí)行git update-server-info進行生成:

?  .git git:(master) cat info/refs
21f45f60fa582d085497fb2d3bb50163e59891ee  refs/heads/history
ef8021acf4c29eb35e3084b7dc543c173d67ad2a  refs/heads/master

文件內(nèi)容主要是服務端上每個引用的版本,客戶端拿到這些引用之后,就可以跟本地的引用進行對比,對于缺失的對象文件,則通過 HTTP 的方式進行下載。

Tips1: 關于 Git 存儲格式請參見:https://github.com/git/git/blob/master/Documentation/gitrepository-layout.txt,后續(xù)文章會展開介紹

Tips2: 如果有更新都要手動執(zhí)行update-server-info?答案是 No,可以配置 Git 服務端的post-receive鉤子自動執(zhí)行更新

所以,一次通過啞協(xié)議 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:根據(jù) HEAD 中的默認分支執(zhí)行 checkout 操作檢出到本地

上面的那些地址是為了演示用,實際 Gitee 僅支持智能協(xié)議而不支持啞協(xié)議,畢竟對于一個公有云服務是不安全的。關于對象如何遍歷這里也不再展開,后續(xù)文章會介紹

啞協(xié)議的實現(xiàn)非常簡單,通過nginx即可簡單實現(xiàn),只需要配置一個靜態(tài)文件服務器,然后將 Git 倉庫以單目錄的形式放上去即可;也可以使用 Go 快速實現(xiàn)一個簡單的 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 智能協(xié)議(Smart Protocol)

HTTP 智能協(xié)議與啞協(xié)議最大的區(qū)別在于:啞協(xié)議在獲取想要的數(shù)據(jù)的時候要自行指定文件資源的網(wǎng)絡地址,并且通過多次的下載操作來達到目的;而智能協(xié)議的主動權則掌握在服務端,服務端提供的info/refs可以動態(tài)更新,并且可以通過客戶端傳來的參數(shù),決定本次交互客戶端所需要的最小對象集,并打包壓縮發(fā)給客戶端,客戶端會進行解壓來拿到自己想要的數(shù)據(jù)

通過監(jiān)聽對應端口,我們可以看到整個過程客戶端發(fā)送了兩次請求:

Git的三種傳輸協(xié)議及實現(xiàn)的方法教程

  1. 引用發(fā)現(xiàn) GET https://gitee.com/kesin/taskover/info/refs?service=git-{upload|receive}-pack

  2. 數(shù)據(jù)傳輸 POST https://gitee.com/kesin/taskover/git-{upload|receive}-pack

Git HTTP 協(xié)議要求無論是下載操作還是上傳操作,都必須先執(zhí)行引用發(fā)現(xiàn),也就是需要知道服務端的各個引用的版本信息,這樣的話才能讓服務端或者客戶端知道兩方之間的差異以及需要什么樣的數(shù)據(jù)。

1. 引用發(fā)現(xiàn)

與啞協(xié)議不同的是,智能協(xié)議的的服務端是動態(tài)服務器,能夠根據(jù)期望來提供相關的引用信息,你可以根據(jù)自己的業(yè)務需求來決定想讓客戶端知道什么樣的信息,通過抓包我們可以看到客戶端請求的數(shù)據(jù)以及 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 包含了一些約定:

  1. Cache-Control 必須禁止緩存,不然可能看不到最新的提交信息

  2. Content-Type 必須是application/x-$servicename-advertisement,不然客戶端會以啞協(xié)議的方式去處理

  3. 客戶端需要驗證返回的狀態(tài)碼,如果是401那么就提示輸入用戶名密碼

另外我們能看到返回的 Body 格式跟啞協(xié)議所用的info/refs內(nèi)容是不一樣的,這里是智能協(xié)議所約定的格式,客戶端根據(jù)這個來識別支持的屬性和驗證信息,這是一個pkt-line格式的數(shù)據(jù):

  1. 客戶端需要驗證第一首行的四個字符符合正則^[0-9a-f]{4}#,這里的四個字符是代表后面內(nèi)容的長度

  2. 客戶端需要驗證第一行是# service=$servicename

  3. 服務端得保證每一行結(jié)尾需要包含一個LF換行符

  4. 服務端需要以0000標識結(jié)束本次請求響應

HEAD引用后還有一系列的服務端能力的參數(shù),這些參數(shù)會告訴客戶端服務端具有什么樣的能力,比如可以通過multi_ack模式進行數(shù)據(jù)交互等,這里不在贅述。再往后就是具體的每一個引用的信息,每行的開頭四個字符均是本行的長度。

在介紹啞協(xié)議的時候,我們使用通過git update-server-info命令生成的info/refs文件,但是很明顯我們在智能協(xié)議這里無法直接使用,因為它不符合pkt-line的格式,所以 Git 提供另外一種方式:通過 git upload-pack命令來直接獲取最新的pkt-line格式的引用信息,來看看它的參數(shù)支持:

--[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是用來發(fā)送對象給客戶端的一個遠程調(diào)用模塊,但是它提供了--stateless-rpc--advertise-refs參數(shù),能夠讓我們快速拿到當前的引用狀態(tài)并退出,我們在服務端的裸倉庫目錄執(zhí)行就可以直接拿到最新的引用信息:

?  .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%

這里的內(nèi)容是不是似曾相識,跟上面我們抓包獲取到的 Gitee 返回的引用數(shù)據(jù)格式一樣,只是少了首行的# service=git-upload-pack,所以我們現(xiàn)在思路非常清晰,可以先來實現(xiàn)第一步引用發(fā)現(xiàn)的服務端的處理,通過對參數(shù)的解析,我們可以拿到倉庫名稱以及相應的操作名稱,就可以進一步整理出客戶端要的響應格式:

// 支持 upload-pack 和 receive-pack 兩種操作引用發(fā)現(xiàn)的處理
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 數(shù)據(jù)
  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")
}

上面我們也提到了,無論是拉取還是推送,都需要先進行引用發(fā)現(xiàn),實際上upload-packreceive-pack所處理的差別僅僅是調(diào)用的命令不同而已,這一點我們也在handleRefs函數(shù)里面做了相應的兼容處理,這里不再贅述。

2. 數(shù)據(jù)傳輸

數(shù)據(jù)傳輸分為兩部分:客戶端向服務端傳輸(Push)、服務端向客戶端傳輸(Fetch)。兩者的區(qū)別在于:

  1. Fetch 操作在獲取引用發(fā)現(xiàn)之后,由 服務端 計算出客戶端想要的數(shù)據(jù),并把數(shù)據(jù)以pkt-line的格式POST給服務端,由服務端進行Pack的計算和打包,將包作為 POST 的響應發(fā)送給客戶端,客戶端進行解壓和引用更新

  2. Push 操作獲取到服務端的引用列表后,由 客戶端 本地計算出客戶端所缺失的數(shù)據(jù),將這些數(shù)據(jù)打包,并POST給服務端,服務端接收到后進行解壓和引用更新

Fetch 操作其實用到了上面我們提到的upload-pack,它是一個發(fā)送對象給客戶端的遠程調(diào)用模塊,為了實現(xiàn)拉取功能,我們只需要在服務端啟動 git upload-pack --stateless-rpc ,這個命令阻塞的接收一串參數(shù),而這串參數(shù)是客戶端的第二次請求發(fā)送過來的,把它傳遞給這個命令,Git 就會自動的計算客戶端所需要的最小對象集并打包,以流的形式返回這個包數(shù)據(jù),我們只需要把這個包作為 POST 請求的響應發(fā)給客戶端就好了。

那么,在 Fetch 操作中,客戶端第二次 POST 請求發(fā)過來的數(shù)據(jù)是什么呢,我們也來抓個包分析一下:

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

客戶端在拿到第一次引用發(fā)現(xiàn)服務端返回的數(shù)據(jù)后,會根據(jù)服務端所提供的能力(capabilities)列表以及引用(refs)列表來計算出第二次需要發(fā)送的數(shù)據(jù),比如會根據(jù)服務端的能力列表來決定客戶端和服務端通信所需要的能力參數(shù),這些能力參數(shù)服務端必須全部支持。另外,客戶端發(fā)送的數(shù)據(jù)必須包含一個want指令,我們在 Clone 一個倉庫的時候所發(fā)送的數(shù)據(jù)全部都是want指令,而不包含have指令,因為本地什么都沒有;而在進行有數(shù)據(jù)的更新的 Fetch 操作的時候,就會有have指令??蛻舳藭鶕?jù)返回的引用信息計算出所需要的 Commit、Common Commit 以及 自己有服務端沒有的 Commit,并將這些數(shù)據(jù)一次性的通過第二次請求發(fā)給服務端,具體客戶端的協(xié)商過程可以參見http-protocol.txt,這里不再贅述。

服務端在收到這些數(shù)據(jù)之后,會先確認want指令所指定的對象是否都能夠在引用中找到,如果沒有want指令或者指令指定的對象中有不包含在服務端的,則會返回給客戶端錯誤信息,服務端根據(jù)這些信息計算出客戶端所需要的對象的集合,并把這些對象打包返回給客戶端,客戶端接收后解壓包并更新引用。

Push 操作大同小異,只不過在第二步的時候,客戶端會根據(jù)服務端的引用信息計算出服務端所需要的對象,直接通過 Post 請求發(fā)送給服務端,并同時附帶一些指令信息,比如新增、刪除、更新哪些引用,以及更新前后的版本,具體格式如下:

// 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....

這里的包數(shù)據(jù)格式為"PACK" <binary data>,會以PACK開頭。服務端接收到這些數(shù)據(jù)后,啟動一個遠程調(diào)用命令receive-pack,然后將數(shù)據(jù)以管道的形式傳給這個命令即可。

所以,整個數(shù)據(jù)傳輸?shù)倪^程無非就是客戶端與服務端的upload-packreceive-pack對規(guī)定格式的數(shù)據(jù)交換而已,根據(jù)這個思路,我們可以繼續(xù)完善我們的 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)

  // 啟動一個進程,通過標準輸入輸出進行數(shù)據(jù)交換
  cmdPack := exec.Command("git", service[4:], "--stateless-rpc", repoPath)
  cmdStdin, err := cmdPack.StdinPipe()
  cmdStdout, err := cmdPack.StdoutPipe()
  _ = cmdPack.Start()

  // 客戶端和服務端數(shù)據(jù)交換
  go func() {
    _, _ = io.Copy(cmdStdin, r.Body)
    _ = cmdStdin.Close()
  }()
  _, _ = io.Copy(w, cmdStdout)
  _ = cmdPack.Wait() // wait for std complete
}

Git && SSH 傳輸協(xié)議

Git 協(xié)議以及 SSH 協(xié)議都是四層的傳輸協(xié)議,而 HTTP 則是七層的傳輸協(xié)議,受限于 HTTP 協(xié)議的特點,HTTP 在 Git 相關的操作上存在傳輸限制、超時等問題,這個問題在大倉庫的傳輸中尤為明顯,相比與 HTTP 而言,Git 以及 SSH 協(xié)議在傳輸上更簡單而且更穩(wěn)定。

官方文檔:https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt

Git 協(xié)議

Git 協(xié)議最大的優(yōu)勢就是速度快,因為它沒有 HTTP 的傳輸協(xié)議的條條框框,也沒有 SSH 加解密的成本,但受限于協(xié)議的缺點,Git 協(xié)議常用于開源項目的下載,不作為私有項目的傳輸協(xié)議。

在上面我們研究 HTTP 智能協(xié)議的實現(xiàn)的時候,我們知道 Git 客戶端跟服務端的交互主要包含兩個步驟:

  1. 獲取服務端的引用

  2. 客戶端根據(jù)服務端的引用數(shù)據(jù)與服務端進行數(shù)據(jù)交換

Git 協(xié)議也是如此,只不過相比于 HTTP 協(xié)議,Git 協(xié)議直接在四層與服務端建立連接,通過這個長鏈接直接完成兩個步驟:

Git的三種傳輸協(xié)議及實現(xiàn)的方法教程MMwkPXVJQrK2g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 在使用 Git 協(xié)議操作的時候,首先客戶端會把相關的信息發(fā)給服務端,這個信息的格式同樣的采用pkt-line的格式:

003egit-upload-pack /project.git\0host=myserver.com\0\0version=1\0

其中包含了命令、倉庫名稱、Host 等相關信息,服務端建立連接之后,接收到這串信息,需要對其中的信息進行加工,找到對應的倉庫所在的位置也就是目錄,當所有的信息都符合要求之后,只需要在服務端啟動upload-pack命令即可,這里需要注意的是我們不需要添加--stateless-rpc參數(shù),直接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

這個時候我們所做的其實就是數(shù)據(jù)的轉(zhuǎn)發(fā),命令的標準輸出信息我們原封不動的發(fā)送給客戶端,客戶端則會進行跟 HTTP 協(xié)議類似的處理產(chǎn)生數(shù)據(jù),接著會把數(shù)據(jù)發(fā)給服務端,我們再原封不動的發(fā)給git upload-pack {repo_path}命令的標準輸入,然后服務端處理完成后會把相應的包通過標準輸出返回,我們原封不動的發(fā)給客戶端,就完成了一次 Fetch 操作,而 Push 的 receive-pack 操作原理相同,這里不再贅述。

需要注意的是,如果客戶端發(fā)送的信息不符合要求,或者處理過程中出現(xiàn)了問題,我們返回錯誤告知客戶端,這個錯誤的格式也是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()
}

客戶端接收到這個信息之后,就會打印信息并關閉連接,整個過程的數(shù)據(jù)均可以通過轉(zhuǎn)包獲取到,感興趣的同學可以通過抓包來進一步加深了解 Git 協(xié)議的傳輸過程。

了解了 Git 協(xié)議的過程之后,我們就可以通過代碼來實現(xiàn)一個簡單的 Git 協(xié)議服務器:

func handleRequest(conn net.Conn) {
    //處理首次客戶端發(fā)來的數(shù)據(jù),拿到 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()

    // 客戶端服務端數(shù)據(jù)交換
  go func() { _, _ = io.Copy(cmdStdin, conn) }()
  _, _ = io.Copy(conn, cmdStdout)
  err = cmdPack.Wait()
}

SSH 協(xié)議

SSH 協(xié)議也是應用的比較廣泛的一種 Git 傳輸協(xié)議,相比于 Git 協(xié)議,SSH 協(xié)議從數(shù)據(jù)傳輸和權限認證上都相對安全,但是受限于加解密的成本,速度會稍慢,但是這個時間成本在安全面前絕對是可以接受的。與 Git 協(xié)議比較,不同點是 SSH 協(xié)議傳輸?shù)臄?shù)據(jù)經(jīng)過加密,相同點是 SSH 協(xié)議的傳輸過程與 Git 協(xié)議一致

SSH 的下載地址一般都是 git@gitee.com:kesin/go-git-protocols.git 這種形式的,在執(zhí)行 Clone 或者 Push 的時候,會拆解成:

ssh user@example.com "git-upload-pack '/project.git'"

所以 SSH 協(xié)議在首次傳參的時候與 Git 協(xié)議的格式不同,其他情況基本一致,比如引用發(fā)現(xiàn)、Packfile 機制、錯誤處理等等,這里都不再做延伸,可以參加官方文檔。

明白 SSH 協(xié)議是怎么回事后,我們想要實現(xiàn)一個 Git SSH 服務器就比較明確了,只需要實現(xiàn)一個 SSH Server 并且在對應的 Session 做對應的數(shù)據(jù)傳輸即可,我們來實現(xiàn)一個簡單的 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)
    // 啟動標準輸入輸出進行數(shù)據(jù)交換,下面的處理是否似曾相識?沒錯,Git 協(xié)議也是同樣的處理方式
  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的三種傳輸協(xié)議及實現(xiàn)的方法教程,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

git
AI