Git内部原理之深入解析传输协议

网友投稿 471 2022-05-29

一、哑协议

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

现在已经很少使用哑协议了,使用哑协议的版本库很难保证安全性和私有化,所以大多数 Git 服务器宿主(包括云端和本地)都会拒绝使用它,一般情况下都建议使用智能协议。

通过 simplegit 版本库来看看 http-fetch 的过程:

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

1

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

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

1

2

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

=> GET HEAD ref: refs/heads/master

1

2

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

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

1

2

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

$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949 tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 author Scott Chacon 1205815931 -0700 committer Scott Chacon 1240030591 -0700 changed the version number

1

2

3

4

5

6

7

接下来,还要再获取两个对象,一个是树对象 cfda3b,它包含有我们刚刚获取的提交对象所指向的内容,另一个是它的父提交 085bb3:

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

1

2

这样就取得了下一个提交对象,再抓取树对象:

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

1

2

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

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

1

2

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

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

1

2

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

Git内部原理之深入解析传输协议

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

1

2

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

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

1

2

现在也有了树对象,可以继续在提交记录上漫游,它们全部都在这个刚下载的包文件里面,所以不用继续向服务端请求更多下载了。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

1

2

3

4

5

git-receive-pack 命令会立即为它所拥有的每一个引用发送一行响应,例子中就只有 master 分支和它的 SHA-1 值,第一行响应中也包含了一个服务端能力的列表(这里是 report-status、delete-refs 和一些其它的,包括客户端的识别码)。

每一行以一个四位的十六进制值开始,用于指明本行的长度,可以看到第一行以 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

1

2

3

4

5

第一行也包括了客户端的能力,这里的全为 0 的 SHA-1 值表示之前没有过这个引用,因为正要添加新的 experiment 引用;删除引用时,将会看到相反的情况:右边的 SHA-1 值全为 0。

然后,客户端会发送一个包含了所有服务端上所没有的对象的包文件,最终,服务端会响应一个成功(或失败)的标识:

000eunpack ok

1

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

1

2

3

4

5

6

这完成了客户端和服务端的第一次数据交换。接下来客户端发起另一个请求,这次是一个 POST 请求,这个请求中包含了 send-pack 提供的数据:

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

1

这个 POST 请求的内容是 send-pack 的输出和相应的包文件,服务端在收到请求后相应地作出成功或失败的 HTTP 响应。请牢记,HTTP 协议有可能会进一步用分块传输编码将数据包裹起来。

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

SSH:

如果通过 SSH 使用抓取功能,fetch-pack 会像这样运行:

$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"

1

在 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

1

2

3

4

5

6

这与 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

1

2

3

4

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

1

2

3

4

5

6

7

8

这和通过 SSH 使用 git-upload-pack 是非常相似的,但是第二个数据交换则是一个单独的请求:

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

1

2

3

4

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

Git TCP/IP

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【Python从入门到精通】(十三)Python面向对象的开发,没有对象怎么能行呢?|【生长吧!Python】
下一篇:华为云带你探秘Xtrabackup备份原理和常见问题分析
相关文章