37

Golang构建简单web框架

 4 years ago
source link: https://studygolang.com/articles/20354?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用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

谢谢!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK