=== 传输协议

Git 可以通过两种主要的方式在版本库之间传输数据：“哑（dumb）”协议和“智能（smart）”协议。
本节将会带你快速浏览这两种协议的运作方式。

==== 哑协议

如果你正在架设一个基于 HTTP 协议的只读版本库，一般而言这种情况下使用的就是哑协议。
这个协议之所以被称为“哑”协议，是因为在传输过程中，服务端不需要有针对 Git 特有的代码；抓取过程是一系列 HTTP 的 `GET` 请求，这种情况下，客户端可以推断出服务端 Git 仓库的布局。

[NOTE]
====
现在已经很少使用哑协议了。
使用哑协议的版本库很难保证安全性和私有化，所以大多数 Git 服务器宿主（包括云端和本地）都会拒绝使用它。
一般情况下都建议使用智能协议，我们会在后面进行介绍。
====

让我们通过 simplegit 版本库来看看 `http-fetch` 的过程：

[source,console]
----
$ git clone http://server/simplegit-progit.git
----

它做的第一件事就是拉取 `info/refs` 文件。
这个文件是通过 `update-server-info` 命令生成的，这也解释了在使用 HTTP 传输时，必须把它设置为 `post-receive` 钩子的原因：

[source]
----
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master
----

现在，你得到了一个远程引用和 SHA-1 值的列表。
接下来，你要确定 HEAD 引用是什么，这样你就知道在完成后应该被检出到工作目录的内容：

[source]
----
=> GET HEAD
ref: refs/heads/master
----

这说明在完成抓取后，你需要检出 `master` 分支。
这时，你就可以开始遍历处理了。
因为你是从 `info/refs` 文件中所提到的 `ca82a6` 提交对象开始的，所以你的首要操作是获取它：

[source]
----
=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)
----

你取回了一个对象——这是一个在服务端以松散格式保存的对象，是你通过使用静态 HTTP GET 请求获取的。
你可以使用 zlib 解压缩它，去除其头部，查看提交记录的内容：

[source,console]
----
$ 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`：

[source]
----
=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)
----

这样就取得了你的下一个提交对象。
再抓取树对象：

[source]
----
=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)
----

噢——看起来这个树对象在服务端并不以松散格式对象存在，所以你得到了一个 404 响应，代表在 HTTP 服务端没有找到该对象。
这有好几个可能的原因——这个对象可能在替代版本库里面，或者在包文件里面。
Git 会首先检查所有列出的替代版本库：

[source]
----
=> GET objects/info/http-alternates
(empty file)
----

如果这返回了一个包含替代版本库 URL 的列表，那么 Git 就会去那些地址检查松散格式对象和文件——这是一种能让派生项目共享对象以节省磁盘的好方法。
然而，在这个例子中，没有列出可用的替代版本库。所以你所需要的对象肯定在某个包文件中。
要检查服务端有哪些可用的包文件，你需要获取 `objects/info/packs` 文件，这里面有一个包文件列表（它也是通过执行 `update-server-info` 所生成的）：

[source]
----
=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
----

服务端只有一个包文件，所以你要的对象显然就在里面。但是你要先检查它的索引文件以确认。
即使服务端有多个包文件，这也是很有用的，因为这样你就可以知道你所需要的对象是在哪一个包文件里面：

[source]
----
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)
----

现在你有这个包文件的索引，你可以查看你要的对象是否在里面——因为索引文件列出了这个包文件所包含的所有对象的 SHA-1 值，和该对象存在于包文件中的偏移量。
你的对象就在这里，接下来就是获取整个包文件：

[source]
----
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)
----

现在你也有了你的树对象，你可以继续在提交记录上漫游。
它们全部都在这个你刚下载的包文件里面，所以你不用继续向服务端请求更多下载了。
Git 会将开始时下载的 HEAD 引用所指向的 `master` 分支检出到工作目录。

==== 智能协议

哑协议虽然很简单但效率略低，且它不能从客户端向服务端发送数据。
智能协议是更常用的传送数据的方法，但它需要在服务端运行一个进程，而这也是 Git 的智能之处——它可以读取本地数据，理解客户端有什么和需要什么，并为它生成合适的包文件。
总共有两组进程用于传输数据，它们分别负责上传和下载数据。

===== 上传数据

(((git commands, send-pack)))(((git commands, receive-pack)))
为了上传数据至远端，Git 使用 `send-pack` 和 `receive-pack` 进程。
运行在客户端上的 `send-pack` 进程连接到远端运行的 `receive-pack` 进程。

====== SSH

举例来说，在项目中使用命令 `git push origin master` 时, `origin` 是由基于 SSH 协议的 URL 所定义的。
Git 会运行 `send-pack` 进程，它会通过 SSH 连接你的服务器。
它会尝试通过 SSH 在服务端执行命令，就像这样：

[source,console]
----
$ 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` 的响应将会是像这样：

[source]
----
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'。

接下来，客户端会发送一个包文件，它包含了所有服务端还没有的对象。
最后，服务端会以成功（或失败）响应：

[source]
----
000eunpack ok
----

====== HTTP(S)

HTTPS 与 HTTP 相比较，除了在“握手”过程略有不同外，其他基本相似。
连接是从下面这个请求开始的：

[source]
----
=> 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` 提供的数据。

[source]
----
=> POST http://server/simplegit-progit.git/git-receive-pack
----

这个 `POST` 请求的内容是 `send-pack` 的输出和相应的包文件。
服务端在收到请求后相应地作出成功或失败的 HTTP 响应。

===== 下载数据

(((git commands, fetch-pack)))(((git commands, upload-pack)))
当你在下载数据时， `fetch-pack` 和 `upload-pack` 进程就起作用了。
客户端启动 `fetch-pack` 进程，连接至远端的 `upload-pack` 进程，以协商后续传输的数据。

====== SSH

如果你通过 SSH 使用抓取功能，`fetch-pack` 会像这样运行：

[source,console]
----
$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"
----

在 `fetch-pack` 连接后，`upload-pack` 会返回类似下面的内容：

[source]
----
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` 进程可以开始发送它所需对象的包文件：

[source]
----
003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
----

====== HTTP(S)

抓取操作的握手需要两个 HTTP 请求。
第一个是向和哑协议中相同的端点发送 `GET` 请求：

[source]
----
=> 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` 是非常相似的，但是第二个数据交换则是一个单独的请求：

[source]
----
=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000
----

这个输出格式还是和前面一样的。
这个请求的响应包含了所需要的包文件，并指明成功或失败。

==== 协议总结

这一章节是传输协议的一个概貌。
传输协议还有很多其它的特性，像是 `multi_ack` 或 `side-band`，但是这些内容已经超出了本书的范围。
我们希望能给你展示客户端和服务端之间的基本交互过程；如果你需要更多的相关知识，你可以参阅 Git 的源代码。
