yes! gvisor里竟然包含了tcpip的golang实现。本文来撸一撸。
本人读的版本commit: 28291a5a5d25633c8bdf45ed5affe90f779c74b4
PacketBuffer是一个报文的结构封装贯穿于整个流程中。所以先把 PacketBuffer 了解一下。这里先了解一下 PacketBuffer 的底层buf细节。因为整个流程都用到这个结构,所以每一个field都可谓别有洞天,需要一个一个来消化,比如网卡、路由、conntrack等。
这个package抽象了内存表示。其中buffer.View是离散的内存封装,内存最小的单位是buffer:
///home/coder/go/src/github.com/google/gvisor/pkg/buffer/buffer.go:24
type buffer struct {
data []byte
read int
write int
bufferEntry
}
两个指针read/write表示读取的长度和写的长度。函数 Full/ReadSize/WriteSize 可以看出来用意。 bufferEntry用于把buffer串起来。
封装了个函数可以去抠掉一部分data的接口: Remove,其他的都很简洁,这个Remove说一下。
为了表示区间增加了个Range的表示
// A Range specifies a range of buffer.
type Range struct {
begin int
end int
}
//应该还是这种范围[begin, end)
Range的Intersect方法实在是骚。
///home/coder/go/src/github.com/google/gvisor/pkg/buffer/buffer.go:51
// Remove removes r from the unread portion. It returns false if r does not
// fully reside in b.
func (b *buffer) Remove(r Range) bool {
sz := b.ReadSize()
switch {
case r.Len() != r.Intersect(Range{end: sz}).Len():// 确保可读的区间要 >= range
return false
case r.Len() == 0: // 抠掉的区间长度为空啥也不干
// Noop
case r.begin == 0: // 从read的开始的地方抠,假装抠掉的数据被读走了
b.read += r.end
case r.end == sz: // 从write地方往前抠,假装写的少了
b.write -= r.Len()
default:
// Remove from the middle of b.data.
// 从中间抠掉,读不改变,假装写的少了
// |--------|
// b.r |---|
// r.b r.e
copy(b.data[b.read+r.begin:], b.data[b.read+r.end:b.write])
b.write -= r.Len()
}
return true
}
好,buffer清楚之后就是bufferList了。 bufferList是个有头尾指针的双链表,用于串buffer,buffer里面的bufferEntry属性就是被它调用设置next/prev。作为数据结构的内容这里不描述了。 接下来是pool
///home/coder/go/src/github.com/google/gvisor/pkg/buffer/pool.go:37
type pool struct {
bufferSize int
avail []buffer `state:"nosave"`
embeddedStorage [embeddedCount]buffer `state:"wait"`
}
avail最先指向embeddedStorage的某个index,后面用满了之后指向新make的slice。buffer在pool这个结构中仅关心位置,里面的buffer里面的data另外初始化。注意从pool里拿一个buf的初始大小也会被设置(get 方法体现)
///home/coder/go/src/github.com/google/gvisor/pkg/buffer/pool.go:51
// get gets a new buffer from p without initializing it.
func (p *pool) getNoInit() *buffer {
//最一开始的情况, avail没有初始化,先绑定
if p.avail == nil {
p.avail = p.embeddedStorage[:]
}
// 这个case是avail已经被切片用完了,此时avail不为nil,但是len为0
// 需要重新开辟空间
if len(p.avail) == 0 {
p.avail = make([]buffer, embeddedCount)
}
if p.bufferSize <= 0 {
p.bufferSize = defaultBufferSize
}
buf := &p.avail[0]
// 配合的是上面第二个判断
p.avail = p.avail[1:]
return buf
}
接下来就是View了。
// /home/coder/go/src/github.com/google/gvisor/pkg/buffer/view.go:31
type View struct {
data bufferList
size int64 // size表示的所有buffer加起来的长度,而不是分片buffer的个数
pool pool
}
方法就不贴代码了,要贴得贴满了。
构造函数: 静态创建即可,没有New…的pattern。
以下函数介绍按出场顺序介绍:
- TrimFront(count int64) ==> 从前面砍掉多少个字节,核心实现在 advanceRead 里面。实现方法就是从双链表的头开始一个一个的切。 当前的这个buf还分两种case,够砍的和不够砍的,够砍的砍完结束(break),不够砍的这个buf直接砍掉(从链表里Remove),相应更新下一轮数据和全局的data长度size。 最后还判断一下进来的场景是不是砍掉的字节比总长度小,如果不满足就panic。这里也发现整个框架在不可能出现的case地方都是直接panic的。k8s里面的代码panic的数量远小于这里的。
- Remove(offset, length int) bool ==> 从某个位置开始抠掉一些数据显然就比上面直接从头砍要细节很多了。 offset,length基于全局的。
- 首先确保区间的正确: 待抠的range要在整个数据区间之内
- 抠的时候还要考虑区间跨buf的case。甚至是跨多个buf的情况。用的方法是一个curr区间,每次遍历bufferList的时候先更新curr.end为当前end,当然表示还是全局的表示,当和input比较时,有交集就清理这个交集,没有交集继续跳。curr.begin在当前buf比较结束时更新。区间更新的时机巧妙。删除的时候要把全局位移转变成当前buff的位移,所以有个设置Offset的行为。
- ReadAt(p []byte, offset int64) (int, error) ==> 从offset位置开始read,并且read满。
- Truncate(length int64) ==> 强制缩到这个大小,不会长的,要求length必须 < size
- Grow(length int64, zero bool) ==> 设置View的大小至length,zero表示是否用0填充
- 判断最后一个是否为空或者还有空间可写?满足的话就从pool里拿个新的buffer。
- 对这个可写的buf(最后一个或者是新拿的)进行写操作(更新buf.write指针),稍微判断一下写空间是否绰绰有余,多的话就按照left要求来
- Prepend(data []byte) ==> 将data塞到前面去
- Append(data []byte) ==> 一直写,写到data尽头
- AppendOwned(data []byte) ==> data包裹上一个buf放到最后
- PullUp(offset, length int) ([]byte, bool) ==> offset开始length长度放到连续空间并返回
- Flatten() []byte ==> 就是打平所有的buf到一个上面并返回
- Size() int64 ==> 返回size属性,好轻松, phew~
- Copy() (other View) ==> 将当前view插入other后面
- Apply(fn func([]byte)) ==> 对每一个buf的data apply fn
- SubApply(offset, length int, fn func([]byte)) ==> 取offset处length长度的byte来apply fn
需要判断offset是否是当前起始位置以及length是否超出当前buf,注意两个条件判断即可。
- Merge(other *View) ==> 把other吸溜过来放到最后
- WriteFromReader(r io.Reader, count int64) (int64, error) ==> 从r里读count过来,然后写到后面去
- ReadToWriter(w io.Writer, count int64) (int64, error) ==> View中从头开始读count字节写到w中。
于是乎,View的结构我们看完了,这根桩我们打完了,看后面的。
除了顶级的buffer之外tcpip package里面还有一个buffer package。这里就相对简单一点了,主要是两个结构:
- View ==> []byte
- VectorisedView ==> [][]byte,不涉及到诸多指针操作,相对简单,不赘述API
- 也有个PullUp 意思相近,返回连续空间的byte,不过是从头开始,没有offset。
- Prependable ==> 倒着长的buf,方便协议栈的前插,从data往前插tcp/network/link层的头
- 里面有个属性: usedIdx表示从最后到 usedIdx 都被占了。所以初始化一个空的 Prependable 这个指针应该停在buf的len(buf)上,注意看 UsedLength 以及 AvailableLength 就好理解了。
/home/coder/go/src/github.com/google/gvisor/pkg/tcpip/stack/registration.go 以及 /home/coder/go/src/github.com/google/gvisor/pkg/tcpip/tcpip.go 存放了一些全局属性,用到的时候再切过来
铺垫差不多,现在看关键结构里面的buf实现。
按照代码中的文档描述,buf结构配备了三个指针来分别在inbound和outbound中都可以方便的使用这个buf。
///home/coder/go/src/github.com/google/gvisor/pkg/tcpip/stack/packet_buffer.go:97
// buf is the underlying buffer for the packet. See struct level docs for
// details.
buf *buffer.Buffer
reserved int
pushed int
consumed int
- 出向时要构造报文,所以会有reserve字段。入向时 reserved 字段为0,整个生命周期 reserved 字段值保持不变。
- pushed用于标记push header,下文会结合代码进行描述
- consumed用于parse header,同上。
下文对相关的代码进行说明:
- NewPacketBuffer(opts PacketBufferOptions) *PacketBuffer ==> 构造函数:传入的option会指出是保留reserve字段,有就创建指定的头部长度。
- 两个位移函数: push/consume
需要注意push标记的是与 reserved 之间的距离。往左, 而consume则往右。理解了这个方向一些API的操作才会更清楚。另外还有头的push并不一定是按照link/network/transport这样的顺序来的,可以是任何顺序,而怎么访问则定义了一个 headerInfo 每一个头信息在 PacketBuffer这个结构里都缓存着。字段名称叫: headers [numHeaderType]headerInfo ,但是从 PayloadSince 函数又看出,其实头还是按顺序排放的。(link/network/transport)
- HeaderSize() int ==> header长度
- pushed + consumed ? 一个方向中总有一个值为0, 出入向复用 PacketBuffer 的体现之一。
- dataOffset() int ==> data的起始位置
- reserved + consumed 也是一样的操作,一个方向中总有一个值为0
- PacketHeader/PacketData ==> 外围封装,提供对应的API,侧重点分别在header以及data部分的处理
其实夯下buf的基础看这些就比较接近api的调用了。比较简单,这里就不展开了。
12 字节 = 源IP + 目的IP + tcpprotocol + tcptotal length 4 4 2 2 checksum 计算方式:
- checksum的初始值自动被设置为0
- 然后,以16bit为单位,两两相加,对于该例子,即为:E34F + 2396 + 4427 + 99F3 = 1E4FF
- 若计算结果大于0xFFFF,则将,高16位加到低16位上,对于该例子,即为,0xE4FF + 0x0001 = E500
- 然后,将该值取反,即为~(E500)=1AFF