使用Golang 搭建http web服務器


Golang在搭建web服務器方面的能力是毋庸置疑的。官方已經有提供net/http包為搭建http服務器做准備。使用這個包能很簡單地對web的路由,靜態文件,模版,cookie等數據進行設置。至於這個包是否好用,這個就見仁見智了。你可以從net包開始封裝一個web框架,當然也可以基於http包封裝一個web框架。但是不論你是打算怎么樣做,了解基本的net/http包一定是你借鑒的基礎。

需求

我們要做兩個簡單的后台web系統。這個系統簡單到只有兩個頁面:登陸和首頁。

1 登陸頁面

clip_image001[4]

 

登陸頁面需要提交用戶名和密碼,將用戶名和密碼和mysql數據庫中的用戶名密碼比對達到驗證的效果。mysql數據庫的go語言驅動推薦使用mymysql(https://github.com/ziutek/mymysql)。

 

當用戶名和密碼正確的時候,需要在cookie中種下用戶名和加密后的密鑰來進行cookie認證。我們不對cookie設置ExpireTime,這樣這個cookie的有效期就是瀏覽器打開到瀏覽器關閉的session期間。

 

另外,這個頁面還需要加載一個js。提交用戶名和密碼的是由js進行ajax post命令進行查詢的。

 

這個頁面需要加載css,進行頁面排版

 

2 首頁

clip_image002[4]

首頁是非常簡單,但它是一個動態頁面。

 

首先右上角的”歡迎登陸, 管理員:yejianfeng“。這里的用戶名yejianfeng是根據不同的用戶會進行變化的。這里需要用到模版,我們又會用到了一個模版包html/template。這個包的作用就是加載模版。

 

其次這個頁面也需要的是css,js(退出系統的刪除cookie操作由js控制)

路由

分析下訪問路徑,會有幾個文件:

/admin/index -- 首頁

/login/index --登陸頁顯示

/ajax/login -- 登陸的ajax動作

/css/main.css -- 獲取main的css文件

/js/login.js -- 獲取登陸的js文件

 

在net/http包中,動態文件的路由和靜態文件的路由是分開的,動態文件使用http.HandleFunc進行設置靜態文件就需要使用到http.FileServer

package main

import (
    "net/http"
)

func main() {
    http.Handle("/css/", http.FileServer(http.Dir("template")))
    http.Handle("/js/", http.FileServer(http.Dir("template")))
    
    http.HandleFunc("/admin/", adminHandler)
    http.HandleFunc("/login/",loginHandler)
    http.HandleFunc("/ajax/",ajaxHandler)
    http.HandleFunc("/",NotFoundHandler)
    http.ListenAndServe(":8888", nil)

}

這里的http.FileServer(http.Dir("template"))的路徑是怎么算的要注意下了。相對路徑都是從當前執行路徑路徑下開始算的,這里設置的路徑樹是這樣:

clip_image003[4]

關於http.HandleFunc如果不理解請參考我的上一篇文章

http://www.cnblogs.com/yjf512/archive/2012/08/22/2650873.html

 

處理器

這里需要定制4個handler對應相應的一級路徑。我們將這些個handler都放入到route.go文件中

頁面404處理器

main中的邏輯是當/admin/ /login/ /ajax/都不符合路徑的時候就進入404頁面處理器NotFoundHandler

func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        http.Redirect(w, r, "/login/index", http.StatusFound)
    }
    
    t, err := template.ParseFiles("template/html/404.html")
    if (err != nil) {
        log.Println(err)
    }
    t.Execute(w, nil)

}

 

這段邏輯是很清晰的:如果路徑是"/" 即訪問路徑是http://192.168.100.166:8888/ 的時候,訪問會跳轉到登陸頁面,否則當訪問路徑不滿足制定的路由,讀取404模版,顯示404。

 

強烈推薦使用template.ParseFile模版文件解析

template包中也提供了Parse這類直接在代碼中寫上模版的函數。使用起來,你會發現這類函數遠沒有ParseFile好用,原因很明顯,如果你把模版寫在代碼中,一旦模版需要進行小細節的修改,也需要重新編譯。並且我們使用模版的目的就是將顯示邏輯和業務邏輯分離,Parse會導致整個代碼是不可維護!

當然有人會考慮到效率問題,一個是讀取文件,一個是直接讀取內存。但是我覺得這點效率差別用戶根本不會察覺到,犧牲極小的性能保證工程性是很值得的。

 

ParseFile中的路徑問題也是很容易犯的問題。這里的填寫相對路徑,則文件是從當前執行的路徑開始算。

clip_image004[4]

比如這個路徑,執行bin/webdemo,template/html/404.html就對應/go/gopath/template/html/404.html

 

這一步后續動作是在template/html文件夾中創建404.html頁面

登陸頁面處理器

func loginHandler(w http.ResponseWriter, r *http.Request) {
    pathInfo := strings.Trim(r.URL.Path, "/")
    parts := strings.Split(pathInfo, "/")
    var action = ""
    if len(parts) > 1 {
        action = strings.Title(parts[1]) + "Action"
    }

    login := &loginController{}
    controller := reflect.ValueOf(login)
    method := controller.MethodByName(action)
    if !method.IsValid() {
        method = controller.MethodByName(strings.Title("index") + "Action")
    }
    requestValue := reflect.ValueOf(r)
    responseValue := reflect.ValueOf(w)
    method.Call([]reflect.Value{responseValue, requestValue})
}

 

 

根據MVC思想,對具體的邏輯內容使用不同的Controller,這里定義了一個loginController, 使用reflect將pathInfo映射成為controller中的方法。

這樣做的好處顯而易見:

1 清晰的文件邏輯。不同的一級目錄分配到不同的控制器,不同的控制器中有不同的方法處理。這些控制器放在一個獨立的文件中,目錄結構清晰干凈。

2 路由和業務邏輯分開。 main中的http.HandleFunc處理一級路由,route處理二級路由,controller處理業務邏輯,每個單元分開處理。

 

好了,下面需要定義loginContrller,我們另外起一個文件loginController.go

package main

import (
    "net/http"
    "html/template"
)

type loginController struct {
}

func (this *loginController)IndexAction(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("template/html/login/index.html")
    if (err != nil) {
        log.Println(err)
    }
    t.Execute(w, nil)
}

 

 

下面需要創建template/html/login/index.html

這個文件中包含mian.css, jquery.js, base.js。 創建這些css和js。具體的css和js內容請看github源碼

js中的邏輯是這樣寫的:當login表單提交的時候,會調用/ajax/login進行驗證操作,下面就開始寫ajax的處理器

ajax處理器

route中的ajaxHandler和loginHandler是大同小異,這里就忽略不說了,具體說一下ajaxController

package main

import (
    "net/http"
    "github.com/ziutek/mymysql/autorc"
    _ "github.com/ziutek/mymysql/thrsafe"
    "encoding/json"
)

type Result struct{
    Ret int
    Reason string
    Data interface{}
}

type ajaxController struct {
}

func (this *ajacController)LoginAction(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("content-type", "application/json")
    err := r.ParseForm()
    if err != nil {
        OutputJson(w, 0, "參數錯誤", nil)
        return
    }
    
    admin_name := r.FormValue("admin_name")
    admin_password := r.FormValue("admin_password")
    
    if admin_name == "" || admin_password == ""{
        OutputJson(w, 0, "參數錯誤", nil)
        return
    }
    
    db := mysql.New("tcp", "", "192.168.199.128", "root", "test", "webdemo")
    if err := db.Connect(); err != nil {
        OutputJson(w, 0, "數據庫操作失敗", nil)
        return
    }
    defer db.Close()
    
    rows, res, err := db.Query("select * from webdemo_admin where admin_name = '%s'", admin_name)
    if err != nil {
        OutputJson(w, 0, "數據庫操作失敗", nil)
        return
    }
    
    name := res.Map("admin_password")
    admin_password_db := rows[0].Str(name)
    
    if admin_password_db != admin_password {
        OutputJson(w, 0, "密碼輸入錯誤", nil)
        return
    }
    
    // 存入cookie,使用cookie存儲
    expiration := time.Unix(1, 0)
    cookie := http.Cookie{Name: "admin_name", Value: rows[0].Str(res.Map("admin_name")), Path: "/"}
    http.SetCookie(w, &cookie)
    
    OutputJson(w, 1, "操作成功", nil)
    return
}

func OutputJson(w http.ResponseWriter, ret int, reason string, i interface{}) {
    out := &Result{ret, reason, i}
    b, err := json.Marshal(out)
    if err != nil {
        return
    }
    w.Write(b)
}

 

這段代碼有幾個地方可以看看:

如何設置header:

w.Header().Set("content-type", "application/json")

 

如何解析參數:

err := r.ParseForm()

admin_name := r.FormValue("admin_name")

 

如何連接數據庫

db := mysql.New("tcp", "", "192.168.199.128", "root", "test", "webdemo")

 

當然這里得需要有個數據庫和數據表,建表和建表的sql文件在github上能看到

create table webdemo_admin

(

admin_id int not null auto_increment,

admin_name varchar(32) not null,

admin_password varchar(32) not null,

primary key(admin_id)

);

 

如何設置cookie

cookie := http.Cookie{Name: "admin_name", Value: rows[0].Str(res.Map("admin_name")), Path: "/"}

http.SetCookie(w, &cookie)

 

主頁處理器

adminHandler的邏輯比其他Handler多的一個是需要 獲取cookie

cookie, err := r.Cookie("admin_name")

 

並且在傳遞給controller的時候需要將admin_name傳遞進去

func adminHandler(w http.ResponseWriter, r *http.Request) {
    // 獲取cookie
    cookie, err := r.Cookie("admin_name")
    if err != nil || cookie.Value == ""{
        http.Redirect(w, r, "/login/index", http.StatusFound)
    }
    
    pathInfo := strings.Trim(r.URL.Path, "/")
    parts := strings.Split(pathInfo, "/")
    
    admin := &adminController{}
    controller := reflect.ValueOf(admin)
    method := controller.MethodByName(action)
    if !method.IsValid() {
        method = controller.MethodByName(strings.Title("index") + "Action")
    }
    requestValue := reflect.ValueOf(r)
    responseValue := reflect.ValueOf(w)
    userValue := reflect.ValueOf(cookie.Value)
    method.Call([]reflect.Value{responseValue, requestValue, Uservalue})
}

其他的部分都是一樣的了。

它對應的Controller的Action是

type User struct {
    UserName string
}

type adminController struct {
}

func (this *adminController)IndexAction(w http.ResponseWriter, r *http.Request, user string) {
    t, err := template.ParseFiles("template/html/admin/index.html")
    if (err != nil) {
        log.Println(err)
    }
    t.Execute(w, &User{user})
}

這里就將user傳遞出去給admin/index模版

模版內部使用{{.UserName}}進行參數顯示

 

后記

至此,這個基本的webdemo就完成了。啟動服務之后,就會在8888端口開啟了web服務。

當然,這個web服務還有許多東西可以優化,個人信息驗證,公共模板的提煉使用,數據庫的密碼使用密文等。但是這個例子中已經用到了搭建web服務器最基本的幾個技能了。

 

源代碼

本文所涉及的源代碼放在github上

https://github.com/jianfengye/webdemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM