从去年开始就对go语言产生一点兴趣,总感觉java有时太过臃肿,是时候尝试一种新语言了。我看了几本关于go的书,不过看完就忘了,最近开发一个微信公众号项目,使用ngrok做内网穿透,顺道研究一下ngrok源码,巩固一下go语言。
网上有两篇文章已对ngrok原理讲得很清晰了:
https://blog.messyidea.com/archives/41/
https://tonybai.com/2015/05/14/ngrok-source-intro/
我试图记录一些细节,然后实现在个简单的ngrok。
- 源码/ngrok/cache/lru.go,这是随手点开的一个文件,从名字可以看出它的用处:实现一个lru(Least Recently Used)缓存,在以往的项目中,我使用的本地缓存是guava提供的工具,很久前我还用过WeakReferenceMap作为一个简单缓存。以下是ngrok中的源码
type LRUCache struct {
mu sync.Mutex
// list & table of *entry objects
list *list.List
table map[string]*list.Element
// Our current size, in bytes. Obviously a gross simplification and low-grade
// approximation.
size uint64
// How many bytes we are limiting the cache to.
capacity uint64
}
type Item struct {
Key string
Value Value
}
虽然算法很简单,但有几个地方还是值得把玩的。list是一个链表,存储的是item,它可以看成是一个具有优先级的队列,每次访问一个元素时,都会对将相应元素移至队首。这样当list长度超过capacity时,就可以移除队尾的元素。而table是用作索引,通过一个键查询缓存时,通过table得到list中一个element的引用。而list保存的item有一个key属性对应用table中的key,这样很方便对table做移除操作。
以前我想在java中用PriorityBolckingQueue,和ConcurrentHashMap实现一个类似的缓存(不仅是LRU缓存,它们组合实现多种缓存算法),但只想到用map保存键值对,而queue用来做优先队列(这种双向索引的方式我没想到)。说不定用JDK中的HashLinkedMap更容易实现LRU,因为他已同时具备链表和索引的功能。
完整代码如下:
// Copyright 2012, Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The implementation borrows heavily from SmallLRUCache (originally by Nathan
// Schrenk). The object maintains a doubly-linked list of elements in the
// When an element is accessed it is promoted to the head of the list, and when
// space is needed the element at the tail of the list (the least recently used
// element) is evicted.
package cache
import (
"container/list"
"encoding/gob"
"fmt"
"io"
"os"
"sync"
"time"
)
type LRUCache struct {
mu sync.Mutex
// list & table of *entry objects
list *list.List
table map[string]*list.Element
// Our current size, in bytes. Obviously a gross simplification and low-grade
// approximation.
size uint64
// How many bytes we are limiting the cache to.
capacity uint64
}
// Values that go into LRUCache need to satisfy this interface.
type Value interface {
Size() int
}
type Item struct {
Key string
Value Value
}
type entry struct {
key string
value Value
size int
time_accessed time.Time
}
func NewLRUCache(capacity uint64) *LRUCache {
return &LRUCache{
list: list.New(),
table: make(map[string]*list.Element),
capacity: capacity,
}
}
func (lru *LRUCache) Get(key string) (v Value, ok bool) {
lru.mu.Lock()
defer lru.mu.Unlock()
element := lru.table[key]
if element == nil {
return nil, false
}
lru.moveToFront(element)
return element.Value.(*entry).value, true
}
func (lru *LRUCache) Set(key string, value Value) {
lru.mu.Lock()
defer lru.mu.Unlock()
if element := lru.table[key]; element != nil {
lru.updateInplace(element, value)
} else {
lru.addNew(key, value)
}
}
func (lru *LRUCache) SetIfAbsent(key string, value Value) {
lru.mu.Lock()
defer lru.mu.Unlock()
if element := lru.table[key]; element != nil {
lru.moveToFront(element)
} else {
lru.addNew(key, value)
}
}
func (lru *LRUCache) Delete(key string) bool {
lru.mu.Lock()
defer lru.mu.Unlock()
element := lru.table[key]
if element == nil {
return false
}
lru.list.Remove(element)
delete(lru.table, key)
lru.size -= uint64(element.Value.(*entry).size)
return true
}
func (lru *LRUCache) Clear() {
lru.mu.Lock()
defer lru.mu.Unlock()
lru.list.Init()
lru.table = make(map[string]*list.Element)
lru.size = 0
}
func (lru *LRUCache) SetCapacity(capacity uint64) {
lru.mu.Lock()
defer lru.mu.Unlock()
lru.capacity = capacity
lru.checkCapacity()
}
func (lru *LRUCache) Stats() (length, size, capacity uint64, oldest time.Time) {
lru.mu.Lock()
defer lru.mu.Unlock()
if lastElem := lru.list.Back(); lastElem != nil {
oldest = lastElem.Value.(*entry).time_accessed
}
return uint64(lru.list.Len()), lru.size, lru.capacity, oldest
}
func (lru *LRUCache) StatsJSON() string {
if lru == nil {
return "{}"
}
l, s, c, o := lru.Stats()
return fmt.Sprintf("{\"Length\": %v, \"Size\": %v, \"Capacity\": %v, \"OldestAccess\": \"%v\"}", l, s, c, o)
}
func (lru *LRUCache) Keys() []string {
lru.mu.Lock()
defer lru.mu.Unlock()
keys := make([]string, 0, lru.list.Len())
for e := lru.list.Front(); e != nil; e = e.Next() {
keys = append(keys, e.Value.(*entry).key)
}
return keys
}
func (lru *LRUCache) Items() []Item {
lru.mu.Lock()
defer lru.mu.Unlock()
items := make([]Item, 0, lru.list.Len())
for e := lru.list.Front(); e != nil; e = e.Next() {
v := e.Value.(*entry)
items = append(items, Item{Key: v.key, Value: v.value})
}
return items
}
func (lru *LRUCache) SaveItems(w io.Writer) error {
items := lru.Items()
encoder := gob.NewEncoder(w)
return encoder.Encode(items)
}
func (lru *LRUCache) SaveItemsToFile(path string) error {
if wr, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
return err
} else {
defer wr.Close()
return lru.SaveItems(wr)
}
}
func (lru *LRUCache) LoadItems(r io.Reader) error {
items := make([]Item, 0)
decoder := gob.NewDecoder(r)
if err := decoder.Decode(&items); err != nil {
return err
}
lru.mu.Lock()
defer lru.mu.Unlock()
for _, item := range items {
// XXX: copied from Set()
if element := lru.table[item.Key]; element != nil {
lru.updateInplace(element, item.Value)
} else {
lru.addNew(item.Key, item.Value)
}
}
return nil
}
func (lru *LRUCache) LoadItemsFromFile(path string) error {
if rd, err := os.Open(path); err != nil {
return err
} else {
defer rd.Close()
return lru.LoadItems(rd)
}
}
func (lru *LRUCache) updateInplace(element *list.Element, value Value) {
valueSize := value.Size()
sizeDiff := valueSize - element.Value.(*entry).size
element.Value.(*entry).value = value
element.Value.(*entry).size = valueSize
lru.size += uint64(sizeDiff)
lru.moveToFront(element)
lru.checkCapacity()
}
func (lru *LRUCache) moveToFront(element *list.Element) {
lru.list.MoveToFront(element)
element.Value.(*entry).time_accessed = time.Now()
}
func (lru *LRUCache) addNew(key string, value Value) {
newEntry := &entry{key, value, value.Size(), time.Now()}
element := lru.list.PushFront(newEntry)
lru.table[key] = element
lru.size += uint64(newEntry.size)
lru.checkCapacity()
}
func (lru *LRUCache) checkCapacity() {
// Partially duplicated from Delete
for lru.size > lru.capacity {
delElem := lru.list.Back()
delValue := delElem.Value.(*entry)
lru.list.Remove(delElem)
delete(lru.table, delValue.key)
lru.size -= uint64(delValue.size)
}
}