Golang构建简单web框架

栏目: Go · 发布时间: 4年前

内容简介:使用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

谢谢!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

iBATIS实战

iBATIS实战

(加)Clinton Begin、(加)Brandon Goodin、(加)Larry Meadors / 叶俊 / 人民邮电出版社 / 2008-5 / 45.00元

《图灵程序设计丛书•Java系列•iBATIS in Action iBATIS实战》是讲述iBATIS框架的权威著作。书中既详实地介绍了iBATIS的设计理念和基础知识,也讨论了动态SQL、高速缓存、DAD框架等高级主题,还讲解了iBATIS在实际开发中的应用。《图灵程序设计丛书•Java系列•iBATIS in Action iBATIS实战》的最后给出了一个设计优雅、层次清晰的示例程序JGa......一起来看看 《iBATIS实战》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换