まとめ
では前回の記事、私たちは中にHTTPサーバGolangを実現する方法をおしゃべり。しかし最終的にはDefaultServeMux
、ルーティング分布関数として使用できますが、その機能も不完全であることがわかります。
DefaultServeMux
ルート配布を行うことでRESTful
スタイルAPI を実装することは不可能であり、リクエストで必要なメソッドを定義する方法も、API
パスにquery
パラメーターを追加することもできません。次に、ルーティングルックアップをより効率的にしたいと考えています。
したがって、この記事では、httprouter
このパッケージを分析し、前述の機能をソースレベルから実装する方法を研究します。また、このパッケージで最も重要な接頭辞ツリーについては、この記事はグラフィックスとテキストの組み合わせで説明されます。
1回の使用
また、使用方法から始めて、上から下まで学習しhttprouter
ます。公式ドキュメントの小さな例を見てみましょう:
package main
import (
"fmt"
"net/http"
"log"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
复制代码
実際、ここnet/http
でのアプローチは、Golang独自のパッケージを使用するアプローチに似ていることがわかります。すべてが対応するURIと関数を最初に登録します。つまり、ルートをプロセッサと照合します。
登録時に、使用router.XXX
たとえば、対応するメソッドを登録する方法をGET
、POST
そして上のように。
登録後、を使用してhttp.ListenAndServe
監視を開始します。
理由については、後の章で詳しく紹介しますが、ここでは最初に練習を理解するだけで済みます。
2作成
コードの1行目を見てみましょう。1つを定義して宣言しましたRouter
。Router
これの構造を見て、この記事に関係のない他の属性は省略します。
type Router struct {
//这是前缀树,记录了相应的路由
trees map[string]*node
//记录了参数的最大数目
maxParams uint16
}
复制代码
このRouter
構造を作成した後、router.XXX
メソッドを使用してルートを登録します。ルートの登録方法を引き続き確認します。
func (r *Router) GET(path string, handle Handle) {
r.Handle(http.MethodGet, path, handle)
}
func (r *Router) POST(path string, handle Handle) {
r.Handle(http.MethodPost, path, handle)
}
...
复制代码
ここにはメソッドの長いリストがあり、それらはすべて同じです。
r.Handle(http.MethodPost, path, handle)
复制代码
この方法。もう一度見てみましょう:
func (r *Router) Handle(method, path string, handle Handle) {
...
if r.trees == nil {
r.trees = make(map[string]*node)
}
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
r.globalAllowed = r.allowed("*", "")
}
root.addRoute(path, handle)
...
}
复制代码
この方法では、多くの詳細も省略されています。この記事に関連するものにのみ焦点を当てます。このメソッドでは、tree
初期化されていない場合、接頭辞ツリーが最初に初期化されていることがわかります。
次に、このツリーがmap
構造であることに気付きました。つまり、メソッドはツリーに対応します。次に、このツリーに対応して、addRoute
メソッドURI
を呼び出しHandle
、対応するを保存します。
3接頭辞ツリー
3.1定義
単語検索ツリーとも呼ばれるTrieツリーは、ツリー構造であり、ハッシュツリーのバリアントです。典型的なアプリケーションは、多数の文字列(文字列に限定されない)をカウント、ソート、および保存することです。そのため、テキストワードの頻度統計のために検索エンジンシステムでよく使用されます。その利点は次のとおりです。文字列の共通のプレフィックスを使用してクエリ時間を短縮し、不要な文字列比較を最小限に抑え、クエリ効率はハッシュツリーよりも高いです。
簡単に言えば、あなたが探しているもの、この木の特定の道をたどっている限り、あなたはそれを見つけることができます。
例えば、検索エンジンは、あなたが入力したカイを:
彼にはこれらの関連付けがあり、これは接頭辞ツリーとしても理解できます。
別の例:
このGET
メソッドのプレフィックスツリーには、次のルートが含まれています。
- /うわ〜すごい
- /テスト
- /こんにちは世界
- / hello / china
- / hello / chinese
この時点で、このツリーを構築する過程で、任意の2つのノードが同じプレフィックスを持っている限り、同じパーツが1つのノードにマージされることを理解する必要があります。
3.2グラフィカルな構造
上記のaddRoute
方法は、この接頭辞ツリーを挿入する方法です。数が空であると仮定して、ここでこのツリーの構造をグラフィカルに説明するつもりです。
挿入する必要がある3つのルートは次のとおりです。
- /こんにちは世界
- / hello / china
- / hello / chinese
(1)挿入/hello/world
この時点ではツリーは空なので、直接挿入できます。
(2)挿入/hello/china
このケースでは、我々が見つかりました。/hello/world
と/hello/china
同じプレフィックスを持ちます/hello/
。
次に、元の/hello/world
ノードを最初に分割し、次に挿入するノードを子ノード/hello/china
として切り離し/hello/world
ます。
(3)挿入/hello/chinese
この時点で、我々は挿入する必要があり/hello/chinese
ますが、発見、/hello/chinese
およびノードが/hello/
共通のプレフィックスを持っている/hello/
私たちが見に行ったので、/hello/
このノードの子ノードを。
ノードにはと呼ばれる属性があることに注意してくださいindices
。これは、このノードの子ノードの最初の文字を記録するため、簡単に見つけることができます。たとえば、この/hello/
ノードには彼のindices
値がありますwc
。挿入するノードは/hello/chinese
、共通のプレフィックスを削除した後chinese
の最初の文字なc
ので、china
このノードに入ります。
このとき、最初に挿入/hello/china
したときの状態に戻ったと思いますか?当時、パブリックプレフィックスはでしたが/hello/
、現在、パブリックプレフィックスはchin
です。
したがって、このノードの子chin
となるノードとしても切り出しa
ます。また、ese
子ノードとも見なされます。
3.3構築アルゴリズムのまとめ
この時点で、建設は終わりました。アルゴリズムを要約しましょう。
特定の注釈付きコードはこの記事の最後に記載されています。詳しく知りたい場合は、自分で確認できます。まずここでプロセスを理解します。
(1)ツリーが空の場合は直接挿入する
(2)そうでない場合は、現在のノードに挿入するノードURI
と共通のプレフィックスがあるかどうかを確認する(3)共通のプレフィックスがない場合は直接挿入する(4)共通のプレフィックスがある場合は判断する現在のノードを分割する必要がありますか
(5)分割する必要がある場合は、共通部分を親ノードとして使用し、残りを子ノードとして使用します
(6)分割する必要がない場合は、同じプレフィックスを持つ子ノードを探します
(7)プレフィックスがある場合同じである場合は、(4)にスキップします。(
8)同じプレフィックスがない場合は
、最後のノードに(9)を直接挿入し、このルートをHandle
しかし、ここで、いくつかの学生は尋ねなければなりません:なぜここにパラメータなしのルートがあるのですか?
実際、上記のプロセスを理解していれば、パラメーターは同じです。ロジックはこれです:各挿入前に、現在の経路(すなわち、そこには走査されていないまたはパラメータを指定せずにノードを挿入するためにスキャンされる/
又は*
)。パラメータがある場合wildChild
は、現在のノードの属性をに設定してからtrue
、パラメータ部分を新しい子ノードに設定します。
4モニター
ルーティング登録について話した後、ルーティング監視について話しましょう。
では前の記事内容、我々はこれを言及しました。
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
复制代码
そのときに、Handle
メソッドを渡さない場合、GolangはデフォルトのDefaultServeMux
メソッドを使用してリクエストを処理することを述べました。これでが渡されたrouter
のでrouter
、リクエストの処理に使用されます。
したがって、メソッドrouter
も実現されServeHTTP
ます。見てみましょう(一部の手順も省略されています)。
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
path := req.URL.Path
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
if ps != nil {
handle(w, req, *ps)
r.putParams(ps)
} else {
handle(w, req, nil)
}
return
}
}
...
// Handle 404
if r.NotFound != nil {
r.NotFound.ServeHTTP(w, req)
} else {
http.NotFound(w, req)
}
}
复制代码
ここでは、リクエストメソッドに対応するプレフィックスツリーを選択し、メソッドを呼び出しますgetValue
。
このメソッドを簡単に説明します。このメソッドpath
では、このルートに対応するHandle
メソッドが見つかるまで、現在のパスとノードを継続的に照合します。
この期間中、ルートがRESTfulであり、ルートにパラメーターが含まれている場合、ルートはに保存されParam
ます。ここでのParam
構造は次のとおりです。
type Param struct {
Key string
Value string
}
复制代码
対応するルートが見つからない場合は、次の404メソッドが呼び出されます。
5処理
この時点では、以前とほとんど同じです。
対応するルートを取得した後Handle
、この関数を呼び出します。
前のnet/http
パッケージHandler
との唯一の違いは、ここHandle
では、APIから取得されたパラメーターがカプセル化されることです。
type Handle func(http.ResponseWriter, *http.Request, Params)
复制代码
最後に6
ご覧いただきありがとうございます〜
この時点で、httprouterの導入が完了しました。最も重要なのは接頭辞ツリーの構築です。上記では、グラフィックとテキストの組み合わせを使用して、接頭辞ツリーの構築プロセスをシミュレートしましたが、接頭辞ツリーが何であるかを理解していただければ幸いです。もちろん、まだ質問がある場合は、WeChatでメッセージを残したり、私と通信したりできます〜
もちろん、あなたがこれに満足していない場合、あなたが見ることができるの背面に取り付けたカタログ、プレフィックスツリーがあり、完全なコードのコメントは。
もちろん、作者はまだ始まったばかりです。したがって、多くの省略があるかもしれません。読解の過程で不適切な説明がある場合、または理解に逸脱がある場合は、メッセージを残して訂正してください。
ありがとうございます〜
PS:他に質問がある場合は、公開アカウントで著者を見つけることもできます。さらに、すべての記事は最初にパブリックアカウントで更新されます。
7ソースコードの読み取り
7.1ツリー構造
type node struct {
path string //当前结点的URI
indices string //子结点的首字母
wildChild bool //子节点是否为参数结点
nType nodeType //结点类型
priority uint32 //权重
children []*node //子节点
handle Handle //处理器
}
复制代码
7.2 addRoute
func (n *node) addRoute(path string, handle Handle) {
fullPath := path
n.priority++
// 如果这是个空树,那么直接插入
if len(n.path) == 0 && len(n.indices) == 0 {
//这个方法其实是在n这个结点插入path,但是会处理参数
//详细实现在后文会给出
n.insertChild(path, fullPath, handle)
n.nType = root
return
}
//设置一个flag
walk:
for {
// 找到当前结点path和要插入的path中最长的前缀
// i为第一位不相同的下标
i := longestCommonPrefix(path, n.path)
// 此时相同的部分比这个结点记录的path短
// 也就是说需要把当前的结点分裂开
if i < len(n.path) {
child := node{
// 把不相同的部分设置为一个切片,作为子节点
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handle: n.handle,
priority: n.priority - 1,
}
// 将新的结点作为这个结点的子节点
n.children = []*node{&child}
// 把这个结点的首字母加入indices中
// 目的是查找更快
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handle = nil
n.wildChild = false
}
// 此时相同的部分只占了新URI的一部分
// 所以把path后面不相同的部分要设置成一个新的结点
if i < len(path) {
path = path[i:]
// 此时如果n的子节点是带参数的
if n.wildChild {
n = n.children[0]
n.priority++
// 判断是否会不合法
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
n.nType != catchAll &&
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
} else {
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
}
// 把截取的path的第一位记录下来
idxc := path[0]
// 如果此时n的子节点是带参数的
if n.nType == param && idxc == '/' && len(n.children) == 1 {
n = n.children[0]
n.priority++
continue walk
}
// 这一步是检查拆分出的path,是否应该被合并入子节点中
// 具体例子可看上文中的图解
// 如果是这样的话,把这个子节点设置为n,然后开始一轮新的循环
for i, c := range []byte(n.indices) {
if c == idxc {
// 这一部分是为了把权重更高的首字符调整到前面
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// 如果这个结点不用被合并
if idxc != ':' && idxc != '*' {
// 把这个结点的首字母也加入n的indices中
n.indices += string([]byte{idxc})
child := &node{}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
// 新建一个结点
n = child
}
// 对这个结点进行插入操作
n.insertChild(path, fullPath, handle)
return
}
// 直接插入到当前的结点
if n.handle != nil {
panic("a handle is already registered for path '" + fullPath + "'")
}
n.handle = handle
return
}
}
复制代码
7.3 insertChild
func (n *node) insertChild(path, fullPath string, handle Handle) {
for {
// 这个方法是用来找这个path是否含有参数的
wildcard, i, valid := findWildcard(path)
// 如果不含参数,直接跳出循环,看最后两行
if i < 0 {
break
}
// 条件校验
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
}
// 同样判断是否合法
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
if len(n.children) > 0 {
panic("wildcard segment '" + wildcard +
"' conflicts with existing children in path '" + fullPath + "'")
}
// 如果参数的第一位是`:`,则说明这是一个参数类型
if wildcard[0] == ':' {
if i > 0 {
// 把当前的path设置为参数之前的那部分
n.path = path[:i]
// 准备把参数后面的部分作为一个新的结点
path = path[i:]
}
//然后把参数部分作为新的结点
n.wildChild = true
child := &node{
nType: param,
path: wildcard,
}
n.children = []*node{child}
n = child
n.priority++
// 这里的意思是,path在参数后面还没有结束
if len(wildcard) < len(path) {
// 把参数后面那部分再分出一个结点,continue继续处理
path = path[len(wildcard):]
child := &node{
priority: 1,
}
n.children = []*node{child}
n = child
continue
}
// 把处理器设置进去
n.handle = handle
return
} else { // 另外一种情况
if i+len(wildcard) != len(path) {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
}
// 判断在这之前有没有一个/
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}
n.path = path[:i]
// 设置一个catchAll类型的子节点
child := &node{
wildChild: true,
nType: catchAll,
}
n.children = []*node{child}
n.indices = string('/')
n = child
n.priority++
// 把后面的参数部分设置为新节点
child = &node{
path: path[i:],
nType: catchAll,
handle: handle,
priority: 1,
}
n.children = []*node{child}
return
}
}
// 对应最开头的部分,如果这个path里面没有参数,直接设置
n.path = path
n.handle = handle
}
复制代码
最も重要な方法はここにあります。ここにいるあなたに拍手を送りましょう!
この部分は理解するのが難しく、数回読む必要があるかもしれません。
それでもわからないところがある場合は、メッセージを残して交換するか、直接公開番号に来て見つけてください〜