总体设计
Client通过反射实现为rpc函数绑定rpc的调用过程,中间通过获取封装好的连接池中的连接,发送序列化好gob二进制数据到 RPC Server 端。等待 RPC Server 放回结果后,反序列化和处理获取到结果。通过 RPC 函数绑定,函数的调用如同调用本地函数一样简单。
Server通过实现RPC函数的抽象,完成具体的RPC函数逻辑,监听 Client 端的请求并处理,将 Client 传输过来的数据反序列化,通过函数名获取 RPC Router 中的Map 中对应的函数,通过反射调用此函数,得到调用结果后,进行序列化返回给 Client 端
// ClientAutoRegister 客户端的自动注册funcClientAutoRegister(cli*client.Client, rpcFunction*function.RPCFunction) {
valueOf:=reflect.ValueOf(rpcFunction).Elem()
typeOf:=valueOf.Type()
numField:=valueOf.NumField()
fori:=0; i<numField; i++ {
name:=typeOf.Field(i).Namefn:=valueOf.Field(i)
cli.CallRPC(name, fn.Addr().Interface())
}
}
// ServerAutoRegister 服务端的自动注册funcServerAutoRegister(sr*server.RPCServer, rpcFunction*function.RPCFunction) {
valueOf:=reflect.ValueOf(rpcFunction).Elem()
typeOf:=valueOf.Type()
numField:=valueOf.NumField()
fori:=0; i<numField; i++ {
name:=typeOf.Field(i).Namefn:=valueOf.Field(i).Interface()
sr.Register(name, fn)
}
}通过自动注册优化增加 RPC 函数的编码数量,只需添加新的 RPC 函数并发 function.RPCFunction 中添加此函数即可。Client 端调用 RPC 方法的时候就像在调用一个普通的方法一样便捷,而函数绑定、序列化与 TCP 传输都在组件内部完成。
rpc/dataserial/data.go 是对序列化的封装
rpc/transport/transport.go 是 TLV decode 和 encode 并通过 socket 接受发送的封装
rpc/function/function.go 是抽象函数的封装
rpc/register/register.go 自动组册的封装
rpc/client/client.go 是对 rpc 客户端的封装
rpc/server/server.go 是对 rpc 服务端的封装
因为 HandlerFunc 的设计类似洋葱模型,以层次递进的,原生实现的路由组册不带请求方法的组册,在实际开发中可能会存在GET方法和POST方法所用的鉴权 middleware 不同,而如果使用默认实现的路由,会造成 middleware 的层次关系混乱。
如图所示
而为了开发降低代码的耦合度,我们期待在路由组册所实现的模型如下图所示。
路由组册设计,实现这个设计,我们要实现对同一个 URL 对方法对区分,于是我们定义一个 map[string]handleFunc 来实现这个功能,而 key(string) 为 [method:url] 如 POST:/api/login ,为了确定 method 是否支持,我们还要设计一个 map[url]int 来实现。同时通过 bitmap 来确定方法是否支持。
map[string]int{
http.MethodGet: 1<<0,
http.MethodPost: 1<<1,
http.MethodPut: 1<<2,
http.MethodDelete: 1<<3,
}对于一个 url ,如果是GET方法,需要对 method Map 中对 method[url] | http.MethodGet( 1<<0 ),此时 method[url] = 0001,如果又支持Post 方法 method[url] | http.MethodPost(1 << 1),此时 method[url] = 0011,而判断一个方法是否被支持只需要 method[url] & http.MethodGet 如果等于 1 则是被支持,0 则是不支持。之后找到对应注册的 handleFunc 即可,逻辑如下
func (s*HttpServer) ServeHTTP(w http.ResponseWriter, r*http.Request) {
start:=time.Now()
path:=r.URL.Pathifcode, ok:=s.method[path]; ok {
method:=r.Methodifs.reqMethod[method]&code==0 {
http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
end:=time.Now()
log.Println("[YuanCheng] | 405 | ", s.Addr, " | ", end.Sub(start).String(), " | ", method, path)
return
}
route:=fmt.Sprint(method, ":", path)
ifhandler, ok:=s.router[route]; ok {
w.Header().Set("Content-Type", "application/json")
handler.ServeHTTP(w, r)
end:=time.Now()
log.Println("[YuanCheng] | 200 | ", s.Addr, " | ", end.Sub(start).String(), " | ", method, path)
}
} else {
http.NotFound(w, r)
end:=time.Now()
log.Println("[YuanCheng] | 404 | ", s.Addr, " | ", end.Sub(start).String(), " | ", path)
}
}
func (s*HttpServer) addRoute(methodstring, routestring, f http.HandlerFunc) {
s.method[route] =s.method[route] |s.reqMethod[method]
sprint:=fmt.Sprint(method, ":", route)
if_, ok:=s.router[sprint]; ok {
log.Println(method, " Method : '", route, "' is exist")
return
}
s.router[sprint] =f
}如此即可完成我们所需要的路由组册设计。
此外为了方便数据校验,设计了数据和绑定设计。根据 golang 的 tag 设计。我们为我们需要自动校验和填充的结构体定义两个 tag ,json 和 pattern,一个是对应请求的 form data 的 key 和请求参数要求匹配的 pattern。例如
typeUserPostInputstruct {
Namestring`json:"name" pattern:"^[a-zA-Z0-9_-]{6,15}$"`Passwordstring`json:"password" pattern:"^().{6,20}$"`
}之后通过 bind.Struct 函数来实现绑定,例
funcHandler(w http.ResponseWriter, r*http.Request) {
// 参数绑定与校验input:=UserPostInput{}
iferr:=bind.Struct(r, &input); err!=nil {
return
}
}funcStruct(r*http.Request, iinterface{}) error {
v:=reflect.ValueOf(i).Elem()
t:=v.Type()
fori:=0; i<v.NumField(); i++ {
field:=v.Field(i)
fieldType:=t.Field(i)
tag:=fieldType.Tag// json获取jsonName:=tag.Get("json")
s:=r.FormValue(jsonName)
// 正则校验pattern:=tag.Get("pattern")
ifpattern!="" {
match, err:=regexp.Match(pattern, []byte(s))
iferr!=nil {
returnerr
}
if!match {
returnerrors.New(fmt.Sprint(fieldType.Name, " param is regexp match ", pattern))
} }
err:=setValue(s, field)
iferr!=nil {
returnerr
}
}
returnnil
}此方法会根据 json 与 pattern 实现数据绑定。
File: http/server/server.go
typeHttpServerstruct {
Addrstringroutermap[string]http.HandlerFuncmethodmap[string]intreqMethodmap[string]int
}funcNewHttpServer(addrstring) *HttpServerfunc (s*HttpServer) ServeHTTP(w http.ResponseWriter, r*http.Request)
func (s*HttpServer) Run()
func (s*HttpServer) addRoute(methodstring, routestring, f http.HandlerFunc)
func (s*HttpServer) GET(routestring, f http.HandlerFunc)
func (s*HttpServer) POST(routestring, f http.HandlerFunc)
func (s*HttpServer) PUT(routestring, f http.HandlerFunc)
func (s*HttpServer) DELETE(routestring, f http.HandlerFunc)返回一个httpServer结构体
reqMethod: map[string]int{
http.MethodGet: 1<<0,
http.MethodPost: 1<<1,
http.MethodPut: 1<<2,
http.MethodDelete: 1<<3,
}核心部分介绍,这部分是用来标记某个URL所支持的方法,HttpServer.method 存储所支持的方法都 code。初始化为0,如果添加某个方法,就将 code 与reqMethod[方法]位于算或,判断此方法是否支持则用 code 与 reqMethod[方法]位运算与,结果为0则为不支持。
为http监听的封装的具体实现,实现路由转化
启动服务
为GET/POST/...等方法提供统一的路由注册逻辑
注册GET方法HandlerFunc
注册POST方法HandlerFunc
注册PUT方法HandlerFunc
注册DELETE方法HandlerFunc
File: http/bind/bind.go
funcStruct(r*http.Request, iinterface{}) error提供从request中获取请求参数,i 为 &model,在model struct 中标记tag:json(请求参数的变量名) tag:pattern(校验的正则),通过反射完成数据的校验与填充。
维护 rpc Server 并实现 rpc 抽象函数,完成对应的数据库操作逻辑




