章节 ▾ 第二版

10.6 Git 内部原理 - 传输协议

传输协议

Git 可以通过两种主要方式在两个仓库之间传输数据: “dumb” 协议和 “smart” 协议。 本节将快速介绍这两种主要协议的运作方式。

Dumb 协议

如果你正在设置一个通过 HTTP 以只读方式提供的仓库,那么很可能会使用 dumb 协议。 这种协议被称为 “dumb”,因为它在传输过程中不需要服务器端有任何 Git 特定的代码; fetch 过程是一系列的 HTTP GET 请求,客户端可以假设服务器上 Git 仓库的布局。

注意

如今很少使用 dumb 协议。 它很难保护或私有化,因此大多数 Git 主机(无论是基于云的还是本地的)都会拒绝使用它。 通常建议使用 smart 协议,我们将在后面进一步描述。

让我们跟踪 simplegit 库的 http-fetch 过程

$ git clone http://server/simplegit-progit.git

此命令首先做的是拉取 info/refs 文件。 该文件由 update-server-info 命令写入,这就是为什么你需要启用该命令作为 post-receive 钩子,以便 HTTP 传输正常工作

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

现在你有了远程引用和 SHA-1 的列表。 接下来,你查找 HEAD 引用是什么,以便知道完成后要检出什么

=> GET HEAD
ref: refs/heads/master

完成该过程后,你需要检出 master 分支。 此时,你已准备好开始 walking 过程。 因为你的起点是你在 info/refs 文件中看到的 ca82a6 提交对象,所以你首先获取它

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

你得到一个对象 – 该对象在服务器上采用 loose 格式,你通过静态 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

Change version number

接下来,你还有两个对象要检索 – cfda3b,它是我们刚刚检索到的提交指向的内容树; 以及 085bb3,它是父提交

=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)

这为你提供了你的下一个提交对象。 获取树对象

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

哎呀 – 看起来该树对象在服务器上不是 loose 格式,因此你收到了 404 响应。 造成这种情况的原因有很多 – 该对象可能位于备用仓库中,也可能位于此仓库中的 packfile 中。 Git 首先检查任何列出的备用项

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

如果返回一个备用 URL 列表,Git 会在那里检查 loose 文件和 packfile – 这对于彼此分叉的项目来说是一个很好的机制,可以在磁盘上共享对象。 但是,由于在这种情况下没有列出任何备用项,因此你的对象必须位于 packfile 中。 要查看此服务器上可用的 packfile,你需要获取 objects/info/packs 文件,其中包含它们的列表(也由 update-server-info 生成)

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

服务器上只有一个 packfile,因此你的对象显然在那里,但你将检查索引文件以确保。 如果服务器上有多个 packfile,这也很有用,因此你可以查看哪个 packfile 包含你需要的对象

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

现在你有了 packfile 索引,你可以查看你的对象是否在其中 – 因为索引列出了 packfile 中包含的对象的 SHA-1 以及这些对象的偏移量。 你的对象在那里,所以继续获取整个 packfile

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

你拥有你的树对象,因此你可以继续 walking 你的提交。 它们也都在你刚刚下载的 packfile 中,因此你无需再向服务器发出任何请求。 Git 检出 master 分支的工作副本,该分支由你开始下载的 HEAD 引用指向。

Smart 协议

简单的协议虽然简单,但效率较低,并且无法处理客户端向服务器写入数据的情况。智能协议是一种更常见的数据传输方法,但它需要在远程端运行一个对 Git 有智能识别的进程——它可以读取本地数据,计算出客户端拥有和需要的内容,并为其生成自定义的 packfile。数据传输有两种进程集:一组用于上传数据,一组用于下载数据。

上传数据

要将数据上传到远程进程,Git 使用 send-packreceive-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-statusdelete-refs 和一些其他功能,包括客户端标识符)。

数据以块的形式传输。每个块都以一个 4 字符的十六进制值开头,指定块的长度(包括长度本身的 4 个字节)。块通常包含单行数据和一个尾随换行符。你的第一个块以 00a5 开头,这是十六进制的 165,意味着该块的长度为 165 字节。下一个块是 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 引用。如果要删除引用,你会在右侧看到相反的情况:全部为 '0'。

接下来,客户端发送一个 packfile,其中包含服务器尚未拥有的所有对象。最后,服务器会响应成功(或失败)的指示

000eunpack ok
HTTP(S)

通过 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 请求,其中包含 send-pack 提供的数据。

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

POST 请求包括 send-pack 的输出和 packfile 作为其负载。然后,服务器通过其 HTTP 响应指示成功或失败。

请记住,HTTP 协议可能会将此数据进一步包装在分块传输编码中。

下载数据

当你下载数据时,会涉及到 fetch-packupload-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 进程,开始发送其所需数据的 packfile。

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

同样,格式与上面相同。对此请求的响应指示成功或失败,并包括 packfile。

协议总结

本节包含传输协议的一个非常基本的概述。该协议包含许多其他功能,例如 multi_ackside-band 功能,但涵盖这些功能超出了本书的范围。我们试图让你了解客户端和服务器之间的一般来回通信;如果你需要比这更多的知识,你可能需要查看 Git 源代码。

scroll-to-top