# 傳輸協議
Git 可以通過兩種主要的方式在版本庫之間傳輸數據:“啞(dumb)”協議和“智能(smart)”協議。 本節將會帶你快速瀏覽這兩種協議的運作方式。
## 啞協議
如果你正在架設一個基于 HTTP 協議的只讀版本庫,一般而言這種情況下使用的就是啞協議。 這個協議之所以被稱為“啞”協議,是因為在傳輸過程中,服務端不需要有針對 Git 特有的代碼;抓取過程是一系列 HTTP 的?`GET`?請求,這種情況下,客戶端可以推斷出服務端 Git 倉庫的布局。
> #### NOTE
> 現在已經很少使用啞協議了。 使用啞協議的版本庫很難保證安全性和私有化,所以大多數 Git 服務器宿主(包括云端和本地)都會拒絕使用它。 一般情況下都建議使用智能協議,我們會在后面進行介紹。
讓我們通過 simplegit 版本庫來看看?`http-fetch`?的過程:
~~~
$ git clone http://server/simplegit-progit.git
~~~
它做的第一件事就是拉取?`info/refs`?文件。 這個文件是通過?`update-server-info`?命令生成的,這也解釋了在使用HTTP傳輸時,必須把它設置為?`post-receive`?鉤子的原因:
~~~
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
~~~
現在,你得到了一個遠程引用和 SHA-1 值的列表。 接下來,你要確定 HEAD 引用是什么,這樣你就知道在完成后應該被檢出到工作目錄的內容:
~~~
=> GET HEAD
ref: refs/heads/master
~~~
這說明在完成抓取后,你需要檢出?`master`?分支。 這時,你就可以開始遍歷處理了。 因為你是從`info/refs`?文件中所提到的?`ca82a6`?提交對象開始的,所以你的首要操作是獲取它:
~~~
=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)
~~~
你取回了一個對象——這是一個在服務端以松散格式保存的對象,是你通過使用靜態 HTTP GET 請求獲取的。 你可以使用 zlib 解壓縮它,去除其頭部,查看提交記錄的內容:
~~~
$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
changed the version number
~~~
接下來,你還要再獲取兩個對象,一個是樹對象?`cfda3b`,它包含有我們剛剛獲取的提交對象所指向的內容,另一個是它的父提交?`085bb3`:
~~~
=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)
~~~
這樣就取得了你的下一個提交對象。 再抓取樹對象:
~~~
=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)
~~~
噢——看起來這個樹對象在服務端并不以松散格式對象存在,所以你得到了一個 404 響應,代表在 HTTP 服務端沒有找到該對象。 這有好幾個可能的原因——這個對象可能在替代版本庫里面,或者在包文件里面。 Git 會首先檢查所有列出的替代版本庫:
~~~
=> GET objects/info/http-alternates
(empty file)
~~~
如果這返回了一個包含替代版本庫 URL 的列表,那么 Git 就會去那些地址檢查松散格式對象和文件——這是一種能讓派生項目共享對象以節省磁盤的好方法。 然而,在這個例子中,沒有列出可用的替代版本庫。所以你所需要的對象肯定在某個包文件中。 要檢查服務端有哪些可用的包文件,你需要獲取?`objects/info/packs`?文件,這里面有一個包文件列表(它也是通過執行?`update-server-info`?所生成的):
~~~
=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
~~~
服務端只有一個包文件,所以你要的對象顯然就在里面。但是你要先檢查它的索引文件以確認。 即使服務端有多個包文件,這也是很有用的,因為這樣你就可以知道你所需要的對象是在哪一個包文件里面:
~~~
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)
~~~
現在你有這個包文件的索引,你可以查看你要的對象是否在里面——因為索引文件列出了這個包文件所包含的所有對象的 SHA-1 值,和該對象存在于包文件中的偏移量。 你的對象就在這里,接下來就是獲取整個包文件:
~~~
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)
~~~
現在你也有了你的樹對象,你可以繼續在提交記錄上漫游。 它們全部都在這個你剛下載的包文件里面,所以你不用繼續向服務端請求更多下載了。 Git 會將開始時下載的 HEAD 引用所指向的`master`?分支檢出到工作目錄。
## 智能協議
啞協議雖然很簡單但效率略低,且它不能從客戶端向服務端發送數據。 智能協議是更常用的傳送數據的方法,但它需要在服務端運行一個進程,而這也是 Git 的智能之處——它可以讀取本地數據,理解客戶端有什么和需要什么,并為它生成合適的包文件。 總共有兩組進程用于傳輸數據,它們分別負責上傳和下載數據。
#### 上傳數據
為了上傳數據至遠端,Git 使用?`send-pack`?和?`receive-pack`?進程。 運行在客戶端上的`send-pack`?進程連接到遠端運行的?`receive-pack`?進程。
#### SSH
舉例來說,在項目中使用命令?`git push origin master`?時,?`origin`?是由基于 SSH 協議的 URL 所定義的。 Git 會運行?`send-pack`?進程,它會通過 SSH 連接你的服務器。 它會嘗試通過 SSH 在服務端執行命令,就像這樣:
~~~
$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000
~~~
`git-receive-pack`?命令會立即為它所擁有的每一個引用發送一行響應——在這個例子中,就只有`master`?分支和它的 SHA-1 值。 第一行響應中也包含了一個服務端能力的列表(這里是?`report-status`、`delete-refs`?和一些其它的,包括客戶端的識別碼)。
每一行以一個四位的十六進制值開始,用于指明本行的長度。 你看到第一行以 005b 開始,這在十六進制中表示 91,意味著第一行有 91 字節。 下一行以 003e 起始,也就是 62,所以下面需要讀取 62 字節。 再下一行是 0000,表示服務端已完成了發送引用列表過程。
現在它知道了服務端的狀態,你的?`send-pack`?進程會判斷哪些提交記錄是它所擁有但服務端沒有的。?`send-pack`?會告知?`receive-pack`?這次推送將會更新的各個引用。 舉個例子,如果你正在更新?`master`?分支,并且增加?`experiment`?分支,這個?`send-pack`?的響應將會是像這樣:
~~~
0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
refs/heads/experiment
0000
~~~
Git 會為每一個將要更新的引用發送一行數據,包括該行長度,舊 SHA-1 值,新 SHA-1 值和將要更新的引用。 第一行也包括了客戶端的能力。 這里的全為?*0*?的 SHA-1 值表示之前沒有過這個引用——因為你正要添加新的 experiment 引用。 刪除引用時,將會看到相反的情況:右邊的 SHA-1 值全為?*0*。
接下來,客戶端會發送一個包文件,它包含了所有服務端還沒有的對象。 最后,服務端會以成功(或失敗)響應:
~~~
000eunpack ok
~~~
#### HTTP(S)
HTTPS 與 HTTP 相比較,除了在“握手”過程略有不同外,其他基本相似。 連接是從下面這個請求開始的:
~~~
=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000
~~~
這完成了客戶端和服務端的第一次數據交換。 接下來客戶端發起另一個請求,這次是一個?`POST`?請求,這個請求中包含了?`git-upload-pack`?提供的數據。
~~~
=> POST http://server/simplegit-progit.git/git-receive-pack
~~~
這個?`POST`?請求的內容是?`send-pack`?的輸出和相應的包文件。 服務端在收到請求后相應地作出成功或失敗的 HTTP 響應。
#### 下載數據
當你在下載數據時,?`fetch-pack`?和?`upload-pack`?進程就起作用了。 客戶端啟動?`fetch-pack`?進程,連接至遠端的?`upload-pack`?進程,以協商后續傳輸的數據。
#### SSH
如果你通過 SSH 使用抓取功能,`fetch-pack`?會像這樣運行:
~~~
$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"
~~~
在?`fetch-pack`?連接后,`upload-pack`?會返回類似下面的內容:
~~~
00dfca82a6dff817ec66f44342007202690a93763949 HEAD multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000
~~~
這與?`receive-pack`?的響應很相似,但是這里所包含的能力是不同的。 而且它還包含 HEAD 引用所指向內容(`symref=HEAD:refs/heads/master`),這樣如果客戶端執行的是克隆,它就會知道要檢出什么。
這時候,`fetch-pack`?進程查看它自己所擁有的對象,并響應 “want” 和它需要的對象的 SHA-1 值。 它還會發送“have”和所有它已擁有的對象的 SHA-1 值。 在列表的最后,它還會發送“done”以通知?`upload-pack`?進程可以開始發送它所需對象的包文件:
~~~
003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
~~~
#### HTTP(S)
抓取操作的握手需要兩個 HTTP 請求。 第一個是向和啞協議中相同的端點發送?`GET`?請求:
~~~
=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed no-done symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000
~~~
這和通過 SSH 使用?`git-upload-pack`?是非常相似的,但是第二個數據交換則是一個單獨的請求:
~~~
=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000
~~~
這個輸出格式還是和前面一樣的。 這個請求的響應包含了所需要的包文件,并指明成功或失敗。
## 協議總結
這一章節是傳輸協議的一個概貌。 傳輸協議還有很多其它的特性,像是?`multi_ack`?或?`side-band`,但是這些內容已經超出了本書的范圍。 我們希望能給你展示客戶端和服務端之間的基本交互過程;如果你需要更多的相關知識,你可以參閱 Git 的源代碼。
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令