Golang在搭建web服務器方面的能力是毋庸置疑的。官方已經有提供net/http包為搭建http服務器做准備。使用這個包能很簡單地對web的路由,靜態文件,模版,cookie等數據進行設置。至於這個包是否好用,這個就見仁見智了。你可以從net包開始封裝一個web框架,當然也可以基於http包封裝一個web框架。但是不論你是打算怎么樣做,了解基本的net/http包一定是你借鑒的基礎。
需求
我們要做兩個簡單的后台web系統。這個系統簡單到只有兩個頁面:登陸和首頁。
1 登陸頁面
登陸頁面需要提交用戶名和密碼,將用戶名和密碼和mysql數據庫中的用戶名密碼比對達到驗證的效果。mysql數據庫的go語言驅動推薦使用mymysql(https://github.com/ziutek/mymysql)。
當用戶名和密碼正確的時候,需要在cookie中種下用戶名和加密后的密鑰來進行cookie認證。我們不對cookie設置ExpireTime,這樣這個cookie的有效期就是瀏覽器打開到瀏覽器關閉的session期間。
另外,這個頁面還需要加載一個js。提交用戶名和密碼的是由js進行ajax post命令進行查詢的。
這個頁面需要加載css,進行頁面排版
2 首頁
首頁是非常簡單,但它是一個動態頁面。
首先右上角的”歡迎登陸, 管理員: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"))的路徑是怎么算的要注意下了。相對路徑都是從當前執行路徑路徑下開始算的,這里設置的路徑樹是這樣:
關於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中的路徑問題也是很容易犯的問題。這里的填寫相對路徑,則文件是從當前執行的路徑開始算。
比如這個路徑,執行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上