1  版权声明
2 起步
2.1  关于版本控制
2.2  Git 简史
2.3  Git 基础
2.4  命令行
2.5  安装 Git
2.6  初次运行Git前的配置
2.7  获取帮助
2.8  总结
3 Git 基础
3.1  获取 Git 仓库
3.2  记录每次更新到仓库
3.3  查看提交历史
3.4  撤消操作
3.5  远程仓库的使用
3.6  打标签
3.7  Git 别名
3.8  总结
4 Git 分支
4.1  分支简介
4.2  分支的新建与合并
4.3  分支管理
4.4  分支开发工作流
4.5  远程分支
4.6  变基
4.7  总结
5 服务器上的 Git
5.1  协议
5.2  在服务器上搭建 Git
5.3  生成 SSH 公钥
5.4  配置服务器
5.5  Git 守护进程
5.6  Smart HTTP
5.7  GitWeb
5.8  GitLab
5.9  第三方托管的选择
5.10  总结
6 分布式 Git
6.1  分布式工作流程
6.2  向一个项目贡献
6.3  维护项目
6.4  总结
7 GitHub
7.1  账户的创建和配置
7.2  对项目做出贡献
7.3  维护项目
7.4  管理组织
7.5  脚本 GitHub
7.6  总结
8 Git工具
8.1  选择修订版本
8.2  交互式暂存
8.3  储藏与清理
8.4  签署工作
8.5  搜索
8.6  重写历史
8.7  重置揭密
8.8  高级合并
8.9  Rerere
8.10  使用 Git 调试
8.11  子模块
8.12  打包
8.13  替换
8.14  凭证存储
8.15  总结
9 自定义Git
9.1  配置 Git
9.2  Git 属性
9.3  Git 钩子
9.4  使用强制策略的一个例子
10 Git与其他系统
10.1  作为客户端的 Git
10.2  迁移到 Git
10.3  总结
11 Git内部原理
11.1  底层命令和高层命令
11.2  Git 对象
11.3  Git 引用
11.4  包文件
11.5  引用规格
11.6  传输协议
11.7  维护与数据恢复
11.8  环境变量
11.9  总结

传输协议

2018-06-21 10:48:53
git
1017
最后编辑:GavinHsueh 于 2018-06-21 10:57:48

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 的源代码。

发表评论
评论通过审核后显示。