内容简介:使用Golang构建web服务还是比较简单的,使用net/http和gorilla/mux就能快速的构建一个简易的web serverpackage mainimport {
使用Golang构建web服务还是比较简单的,使用net/http和gorilla/mux就能快速的构建一个简易的web server
package main
import {
“net/http”
“github.com/gorilla/mux”
}
func main() {
router = mux.NewRouter().StrictSlash(true)
router.Handle(“/”, http.FileServer(http.Dir(“/static”)))
http.ListenAndServe(“:8080”, nil)
}
这样一个简易的静态服务器就构建成功了。
当然我们不可能就这么满足了,我们当然希望这个服务器是可以处理一些业务逻辑的。比如登录:
router.HandleFunc(“/login”, handlers.LoginHandler)
handler怎么写呢:
func LoginHandler(w http.ResponseWriter, r *http.Request) {
controllers.LoginIndexAction(w,r);
}
controller(使用mymysql连接数据库):
func LoginAction(w http.ResponseWriter, r *http.Request) {
w.Header().Set(“content-type”, “application/json”)
err := r.ParseForm()
if err != nil { Response(w, "Param error.", "PARAM_ERROR",403) return } admin_name := r.FormValue("admin_name") admin_password := r.FormValue("admin_password") if admin_name == "" || admin_password == ""{ Response(w, "Param error.", "PARAM_ERROR",403) return } db := mysql.New("tcp", "", "127.0.0.1:3306", "user", "pass", "database") if err := db.Connect(); err != nil { log.Println(err) Response(w, "Param error.", "PARAM_ERROR",403) return } defer db.Close() rows, res, err := db.Query("select * from webdemo_admin where admin_name = '%s'", admin_name) if err != nil { log.Println(err) Response(w, "Database error.", "DATABASE_ERROR",503) return } name := res.Map("admin_password") admin_password_db := rows[0].Str(name) if admin_password_db != admin_password { Response(w, "Password error.", "PASSWORD_ERROR",403) return } cookie := http.Cookie{Name: "admin_name", Value: rows[0].Str(res.Map("admin_name")), Path: "/"} http.SetCookie(w, &cookie) Response(w, "Login success.", "SUCCESS",200) return
}
type response struct{
Status int json:"status"
Description string json:"description"
Code string json:"code"
}
func Response(w http.ResponseWriter, description string,code string, status int) {
out := &response{status, description, code}
b, err := json.Marshal(out)
if err != nil {
return
}
w.WriteHeader(status)
w.Write(b)
}
将用户名放到cookie里就当登录成功了。
如果有多个路由需要处理呢,情形就会变成这样:
router.HandleFunc(“/url1”, handlers.Handler1)
router.HandleFunc(“/url2”, handlers.Handler1)
router.HandleFunc(“/url3”, handlers.Handler1)
router.HandleFunc(“/url4”, handlers.Handler1)
router.HandleFunc(“/url5”, handlers.Handler1)
router.HandleFunc(“/url6”, handlers.Handler1)
router.HandleFunc(“/url7”, handlers.Handler1)
…
好像也无伤大雅,但是如果有更一步的需求,每个URL需要做权限验证,记录日志,这种方式显然就不太合理了,我们需要对router做统一的管理,这里我们跳过了handler层,直接由controller来处理,我觉得更简洁一点。
//先定义Route的结构体
type Route struct {
Name string
Method string
Pattern string
Auth bool
HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes{
Route{
“url1”,
“GET”,
“/url1”,
true,
controllers.Url1,
},
Route{
“url2”,
“POST”,
“/url2”,
false,
controllers.Url2,
},
}
var router *mux.Router
func NewRouter() *mux.Router {
if router == nil {
router = mux.NewRouter().StrictSlash(true)
}
for _, route := range routes {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(route.HandlerFunc)
}
return router
}
这时候如果添加权限验证,只有通过登录验证的用户才有权限调用,这就需要中间件(我个人比较喜欢称它装饰器)出场了:
func Auth(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(“admin_name”)
if err != nil || cookie.Value == “”{
Response(w, “token not found.”, “AUTH_FAILED”,403)
return;
}
rows, res, err := db.Query("select * from user where user_name= '%d'", cookie.Value) if err != nil { Response(w, "can not connect database.", "DB_ERROR",500) return } if len(rows) == 0 { Response(w, "user not found.", "NOT_FOUND",404) return } row := rows[0] user := controllers.User{ User_id:row.Int(res.Map("user_id")), User_name:row.Str(res.Map("user_name")), User_type:row.Str(res.Map("user_type")), Add_time:row.Str(res.Map("add_time"))} session.CurrentUser = user log.Printf("user_id:%v",controllers.CurrentUser.User_id) inner.ServeHTTP(w, r) })
}
func NewRouter() *mux.Router {
if router == nil {
router = mux.NewRouter().StrictSlash(true)
}
for _, route := range routes {
if(route.Auth){
handler = decorates.Auth(route.HandlerFunc)
} router. Methods(route.Method). Path(route.Pattern). Name(route.Name). Handler(handler) } return router
}
显然这样管理session是比较粗糙的,怎么办,有现成的解决方案,jwt(JSON Web Tokens),我们可以使用jwt-go来生成token,如果一个请求cookie或者header里面含有token,并且可以验证通过,我就认为这个用户是合法用户:
//生成token
func Generate(key string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
“key”: key,
“exp”: (time.Now().Add(time.Minute * 60 * 24 * 2)).Unix(),
})
tokenString, err := token.SignedString(settings.HmacSampleSecret) return tokenString, err
}
//验证token
func Valid(tokenString string) (string, error) {
token1, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf(“Unexpected signing method: %v”, token.Header[“alg”])
}
return settings.HmacSampleSecret, nil
})
if claims, ok := token1.Claims.(jwt.MapClaims); ok && token1.Valid { return fmt.Sprintf("%v", claims["key"]), nil } else { return "", err }
}
Auth中间件就可以变成下面的样子:
func Auth(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("token") if err != nil || cookie.Value == ""{ Response(w, "token not found.", "AUTH_FAILED",403) return; } user_id, err := token.Valid(cookie.Value) if err != nil { Response(w, "bad token.", "AUTH_FAILED",403) return; } rows, res, err := db.Query("select * from user where user_id= '%d'", user_id) if err != nil { Response(w, "can not connect database.", "DB_ERROR",500) return } if len(rows) == 0 { Response(w, "user not found.", "NOT_FOUND",404) return } row := rows[0] user := controllers.User{ User_id:row.Int(res.Map("user_id")), User_name:row.Str(res.Map("user_name")), User_type:row.Str(res.Map("user_type")), Add_time:row.Str(res.Map("add_time"))} session.CurrentUser = user log.Printf("user_id:%v",controllers.CurrentUser.User_id) inner.ServeHTTP(w, r) })
}
我们还可以对每个URL实现log记录:
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() inner.ServeHTTP(w, r) log.Printf( "%s\t%s\t%s\t%s", r.Method, r.RequestURI, name, time.Since(start), ) })
}
func NewRouter() *mux.Router {
if router == nil {
router = mux.NewRouter().StrictSlash(true)
}
for _, route := range routes {
var handler http.Handler = decorates.Logger(route.HandlerFunc, route.Name)
if(route.Auth){
handler = decorates.Auth(handler)
}
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
return router
}
有跨域的需求?好办:
func CorsHeader(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Add("Access-Control-Allow-Method","POST, OPTIONS, GET, HEAD, PUT, PATCH, DELETE") w.Header().Add("Access-Control-Allow-Headers","Origin, X-Requested-With, X-HTTP-Method-Override,accept-charset,accept-encoding , Content-Type, Accept, Cookie") w.Header().Set("Content-Type","application/json") inner.ServeHTTP(w, r) })
}
func NewRouter() *mux.Router {
if router == nil {
router = mux.NewRouter().StrictSlash(true)
}
for _, route := range routes {
var handler http.Handler = decorates.Logger(route.HandlerFunc, route.Name)
if(route.Auth){
handler = decorates.Auth(handler)
}
handler = decorates.CorsHeader(handler)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
router.
Methods(“OPTIONS”).
Path(route.Pattern).
Name(“cors”).
Handler(decorates.CorsHeader(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return
})))
}
return router
}
session管理好像还有一些问题,每个request请求都会改变全局的CurrenUser,如果有并发的情况下,这就容易产生混乱了,可以需要用户信息的时候通过token去数据库来取,效率会有影响,但并发的问题可以解决了:
func CurrentUser(r *http.Request) *models.User {
cookie, err := r.Cookie(“token”)
if err != nil || cookie.Value == “” {
return &models.User{}
}
key, err := token.Valid(cookie.Value)
if err != nil {
return &models.User{}
}
if !strings.Contains(key, “|”) {
return &models.User{}
}
keys := strings.Split(key, “|”)
rows, res, err := db.QueryNonLogging(“select * from user where user_id = ‘%v’ and user_pass = ‘%v’”, keys[0], keys[1])
if err != nil { return &models.User{} } if len(rows) == 0 { return &models.User{} } row := rows[0] user := models.User{ User_id: row.Int(res.Map("user_id")), User_name: row.Str(res.Map("user_name")), User_type: row.Str(res.Map("user_type")), Add_time: row.Str(res.Map("add_time"))} return &user
}
日志的问题好像还没有解决,毕竟日志需要写到文件里面并且需要一些详细的信息,比如行号,文件,才能利于排查问题,或者做统计:
func Printf(format string, params …interface{}) {
, f, line, := runtime.Caller(1)
log.Printf(format, params…)
file, err := os.OpenFile(settings.LogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.ModePerm)
if err != nil {
log.Printf(“%v”, err)
return
}
defer file.Close()
_, err = file.Seek(0, os.SEEK_END)
if err != nil {
return
}
args := strings.Split(f, “/”)
f = args[len(args)-1]
msg := fmt.Sprintf(“%v:%v(%v)”, line, format, f)
logger := log.New(file, “”, log.LstdFlags)
logger.Printf(msg, params…)
}
func Println(v …interface{}) {
, f, line, := runtime.Caller(1)
log.Println(v…)
file, err := os.OpenFile(settings.LogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.ModePerm)
if err != nil {
log.Printf(“%v”, err)
return
}
defer file.Close()
_, err = file.Seek(0, os.SEEK_END)
if err != nil {
return
}
args := strings.Split(f, “/”)
f = args[len(args)-1]
msg := fmt.Sprintf(“%v:%v(%v)”, line, fmt.Sprintln(v…), f)
logger := log.New(file, “”, log.LstdFlags)
logger.Println(msg)
}
日志写到文件的问题解决了,又面临新的问题,日志文件太大,怎么办,需要归档(每隔12小时就查看一下日志文件多大了,如果太大了就压缩一下归档):
var ticker = time.NewTicker(time.Minute * 60 * 12)
func init() {
go func() {
for _ = range ticker.C {
archive()
}
}()
}
func archive() error {
info, _ := os.Stat(settings.LogFile)
if info.Size() > 1024*1024*50 {
target := fmt.Sprintf(“%v.%v.tar.gz”,
shortFileName(settings.LogFile),
time.Now().Format(“2006-01-02-15-04”),
)
tmp := fmt.Sprintf(“%v.%v.tmp”,
shortFileName(settings.LogFile),
time.Now().Format(“2006-01-02-15-04”),
)
in := bytes.NewBuffer(nil)
cmd := exec.Command(“sh”)
cmd.Stdin = in
go func() {
in.WriteString(fmt.Sprintf(“cd %v\n”, shortFileDir(settings.LogFile)))
in.WriteString(fmt.Sprintf(“cp %v %v\n”, shortFileName(settings.LogFile), tmp))
in.WriteString(fmt.Sprintf(“echo ” > %v\n”, shortFileName(settings.LogFile)))
in.WriteString(fmt.Sprintf(“tar -czvf %v %v\n”, target, tmp))
in.WriteString(fmt.Sprintf(“rm %v\n”, tmp))
in.WriteString(“exit\n”)
}()
if err := cmd.Run(); err != nil {
fmt.Println(err)
return err
}
}
return nil
}
基本的功能好像都能解决了,饱暖思淫欲,错误处理感觉用起来不怎么舒服,有更优雅的办法:
type Handler func(http.ResponseWriter, *http.Request) *models.APPError
func (fn Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil {
utils.Response(w, e.Message, e.Code, e.Status)
}
}
//装饰器就变成了这样
func (inner Handler) Auth() Handler {
return Handler(func(w http.ResponseWriter, r *http.Request) *models.APPError {
tokenString := “”
cookie, _ := r.Cookie(“token”)
if cookie != nil {
tokenString = cookie.Value
}
if tokenString == “” {
if r.Header != nil {
if authorization := r.Header[“Authorization”]; len(authorization) > 0 {
tokenString = authorization[0]
}
}
}
key, err := token.Valid(tokenString)
if err != nil {
return &models.APPError{err, “bad token.”, “AUTH_FAILED”, 403}
}
if !strings.Contains(key, “|”) {
return &models.APPError{err, “user not found.”, “NOT_FOUND”, 404}
}
keys := strings.Split(key, “|”)
rows, _, err := db.QueryNonLogging(“select * from user where user_id = ‘%v’ and user_pass = ‘%v’”, keys[0], keys[1])
if err != nil {
return &models.APPError{err, “can not connect database.”, “DB_ERROR”, 500}
}
if len(rows) == 0 {
return &models.APPError{err, “user not found.”, “NOT_FOUND”, 404}
}
go log.Printf(“user_id:%v”, keys[0])
inner.ServeHTTP(w, r)
return nil
})
}
//router画风也变了
type Route struct {
Name string
Method string
Pattern string
HandlerFunc Handler
ContentType string
}
type Routes []Route
var BRoutes = Routes{
Route{
“nothing”,
“GET”,
“/”,
Config,
contenttype.JSON,
},
Route{
“authDemo”,
“GET”,
“/demo1”,
Handler(Config).
Auth(),
contenttype.JSON,
},
Route{
“verifyDemo”,
“GET”,
“/demo2”,
Handler(Config).
Verify(),
contenttype.JSON,
},
Route{
“verifyAndAuthDemo”,
“GET”,
“/demo3”,
Handler(Config).
Auth().
Verify(),
contenttype.JSON,
},
}
这样基本的web框架就完成了,想添加一些命令行工具,比如测试,自动生成app,推荐用kingpin来实现:
var (
app = kingpin.New(“beauty”, “A command-line tools of beauty.”)
demo = app.Command(“demo”, “Demo of web server.”)
generate = app.Command(“generate”, “Generate a new app.”)
name = generate.Arg(“name”, “AppName for app.”).Required().String()
)
func main() {
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
case generate.FullCommand():
GOPATH := os.Getenv(“GOPATH”)
appPath := fmt.Sprintf(“%v/src/%v”, GOPATH, *name)
origin := fmt.Sprintf(“%v/src/github.com/yang-f/beauty/etc/demo.zip”, GOPATH)
dst := fmt.Sprintf(“%v.zip”, appPath)
_, err := utils.CopyFile(dst, origin)
if err != nil {
fmt.Println(err.Error())
}
utils.Unzip(dst, appPath)
os.RemoveAll(dst)
helper := utils.ReplaceHelper{
Root: appPath,
OldText: “{appName}”,
NewText: *name,
}
helper.DoWrok()
log.Printf(“Generate %s success.”, *name)
case demo.FullCommand():
log.Printf(“Start server on port %s”, settings.Listen)
router := router.NewRouter()
log.Fatal(http.ListenAndServe(settings.Listen, router))
}
}
执行命令行是这样的:
usage: beauty [] [ …]
A command-line tools of beauty.
Flags:
–help Show context-sensitive help (also try –help-long and –help-man).
Commands:
help […]
Show help.
demo
Demo of web server.
generate
Generate a new app.
到此,这个框架还在不断的优化中,希望能有人提供宝贵的批评和建议。
以下是代码地址:
yang-f/beauty
谢谢!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 测试框架原理,构建成功的基石
- [译] 构建微服务的十大 Go 框架/库
- 【02-中间件】构建go web框架
- asp.net mvc构建自己的源码框架
- 前端下半场:构建跨框架的 UI 库
- Kubernetes 原生 CI/CD 构建框架 Argo 详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。