/* ================================================================================= * License: GPL-2.0 license * Author: 众产® https://ciy.cn/code * Version: 0.1.3 ================================================================================= Web服务器 NewCiyWebServer ================================================================================= ================================================================================= 示例: ================================================================================= Nginx配置示例: ================================================================================= */ package zciyon import ( "context" "fmt" "net" "net/http" "net/http/fcgi" "net/http/httputil" "net/url" "os" "path/filepath" "runtime" "strings" ) type CiyWebServer struct { pathfn map[string]map[string]func(http.ResponseWriter, *http.Request) bool mock func(http.ResponseWriter, *http.Request) error //Mock回调 phpproxy *httputil.ReverseProxy //PHP代理服务器 exts map[string]int //静态服务器允许的扩展名,不设置全部允许 } type contextKey string var GhttpKey contextKey = contextKey("ciy") type RequestContext struct { CiyAuth string } //在根上建立HandleFunc // func NewCiyWebServer() (*CiyWebServer, error) { web := &CiyWebServer{} web.exts = map[string]int{} web.pathfn = map[string]map[string]func(http.ResponseWriter, *http.Request) bool{} http.HandleFunc("/", web.WithRequestContext(web.runMainPath)) http.HandleFunc("/ws/", web.runWebsocket) return web, nil } func (web *CiyWebServer) WithRequestContext(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), GhttpKey, &RequestContext{ CiyAuth: "", }) next(w, r.WithContext(ctx)) } } func (thos *CiyWebServer) registerPath(path, fnname string, fn func(http.ResponseWriter, *http.Request) bool) { _, ok := thos.pathfn[path] if ok { thos.pathfn[path][fnname] = fn } else { thos.pathfn[path] = map[string]func(http.ResponseWriter, *http.Request) bool{fnname: fn} } } func (thos *CiyWebServer) SetExt(extstr string) { //允许的静态文件类型 .jpg;.png。不设置全部允许 exts := strings.Split(extstr, ";") for _, ext := range exts { ext = strings.ToLower(strings.TrimSpace(ext)) if !strings.HasPrefix(ext, ".") { continue } thos.exts[ext] = 1 } } func (thos *CiyWebServer) runWebsocket(w http.ResponseWriter, r *http.Request) { funname := GetQuery("func", r) Clog("websocket", funname) clsmothod := strings.Split(funname, ".") if len(clsmothod) != 2 { Clog("websocket", fmt.Sprintf("func format error:%v", funname)) return } for path := range thos.pathfn { if path == clsmothod[0] { for fnname, fn := range thos.pathfn[path] { if clsmothod[1] == fnname { fn(w, r) return } } Clog("websocket", fmt.Sprintf("RouterFunc not find mothod[%v] in [%v]", clsmothod[1], clsmothod[0])) return } } Clog("RouterFunc not find class:%v", clsmothod[0]) } func (thos *CiyWebServer) runMainPath(w http.ResponseWriter, r *http.Request) { Clog("root", r.URL.Path) //将大于n毫秒的请求,保存到log //可以统计每个path.func的请求次数用时 rets := make([]string, 0) if err := thos.execFunc(w, r); err == nil { return } else { rets = append(rets, err.Error()) } if thos.mock != nil { if err := thos.execMock(w, r); err == nil { return } else { rets = append(rets, err.Error()) } } if thos.phpproxy != nil { if err := thos.execPHP(w, r); err == nil { return } else { rets = append(rets, err.Error()) } } if CiyRunMode == CIYRUN_DEV { ErrJSON(w, strings.Join(rets, "; "), nil) } else { http.NotFound(w, r) } } func (thos *CiyWebServer) execMock(w http.ResponseWriter, r *http.Request) error { Clog("execMock", r.URL.Path) //找文件或数据库mocks,若有返回。 return thos.mock(w, r) } func (thos *CiyWebServer) execPHP(w http.ResponseWriter, r *http.Request) error { Clog("execPHP", r.URL.Path) //找本地goc文件,若有转发请求。 //找到 func指向的.php文件是否存在,打开文件检查是否存在public static function json_xxx() static function sse_xxx( thos.phpproxy.ServeHTTP(w, r) return nil } func (thos *CiyWebServer) execFunc(w http.ResponseWriter, r *http.Request) error { defer func() { if rc := recover(); rc != nil { funname := r.URL.Path if funname == "" { funname = GetQuery("sse", r) } errmsg := fmt.Sprintf("%v", rc) Log.Error(funname, errmsg) ErrJSON(w, errmsg) } }() Clog("execFunc", r.URL.Path) clsmothod := strings.Split(r.URL.Path, ".") if len(clsmothod) != 2 { return fmt.Errorf("RouterFunc format error:%v", r.URL.Path) } for path := range thos.pathfn { if path == clsmothod[0] { for fnname, fn := range thos.pathfn[path] { if clsmothod[1] == fnname { sttime := Timems() boo := fn(w, r) CiyVars.Func_runms += Timems() - sttime if boo { CiyVars.Func_succ++ } else { CiyVars.Func_fail++ } return nil } } return fmt.Errorf("RouterFunc not find mothod[%v] in [%v]", clsmothod[1], clsmothod[0]) } } return fmt.Errorf("RouterFunc not find class:%v", clsmothod[0]) } func (thos *CiyWebServer) runStatic(w http.ResponseWriter, r *http.Request) bool { Clog("runStatic", r.URL.Path) //模拟nginx功能 //判断文件扩展名是否允许 //判断文件@参数,如果无缓存则压缩图片,存到文件缓存,返回文件缓存地址 //文件在10分钟内,被访问超过n次,则进入内存缓存。 //缓存达到容量后,自动清理10分钟内访问次数最少的几个文件缓存 //如果文件扩展名包含 file := CiyWebDir + r.URL.Path if len(thos.exts) > 0 { ext := strings.ToLower(filepath.Ext(file)) if ext != "" { if _, ok := thos.exts[ext]; !ok { http.NotFound(w, r) return false } } } err := FileExist(file) if err != nil { http.NotFound(w, r) } http.ServeFile(w, r, file) return true } func (thos *CiyWebServer) RouterStatic(path string) { //允许的静态目录 thos.registerPath(path, "", thos.runStatic) } func (thos *CiyWebServer) RouterFunc(path string, fnmap map[string]map[string]func(http.ResponseWriter, *http.Request) bool) { for subpath, fnmap2 := range fnmap { for funstr, fn := range fnmap2 { thos.registerPath(path+"/"+subpath, funstr, fn) } } } func (thos *CiyWebServer) RouterHandle(path string, fn func(http.ResponseWriter, *http.Request)) { http.HandleFunc(path, fn) } func (thos *CiyWebServer) SetPHP(host string) { if host == "" { return } if ind := strings.Index(host, "://"); ind > -1 { host = host[ind+3:] } thos.phpproxy = httputil.NewSingleHostReverseProxy( &url.URL{ Scheme: "http", Host: host, }, ) _, err := http.Get("http://" + host) if err != nil { Clog("WebRun PHP err:", err) return } Clog("WebRun support PHP:", host) } func (thos *CiyWebServer) execMockFile(w http.ResponseWriter, r *http.Request) error { funname := r.URL.Path mockfile := CiyWebDir + "/ud/mock/" + funname + ".json" err := FileExist(mockfile) if err != nil { return err } http.ServeFile(w, r, mockfile) return nil } func (thos *CiyWebServer) SetMockFile() { if thos.mock != nil { return } thos.mock = thos.execMockFile Clog("WebRun support Mock File") } func (thos *CiyWebServer) SetMockFn(fn func(http.ResponseWriter, *http.Request) error) { if thos.mock != nil { return } thos.mock = fn Clog("WebRun support Mock Callback") } func (thos *CiyWebServer) Run(mode, ipsk string) { if mode == "fastcgi" { listener, err := net.Listen("tcp", ipsk) if err != nil { Clog("WebRun fastcgi listen err:", err) return } defer listener.Close() Clog("WebRun fastcgi running: " + ipsk) err = fcgi.Serve(listener, nil) if err != nil { Clog("WebRun fastcgi err:", err) return } } else if mode == "unixsock" { // sigchan := make(chan os.Signal, 1) // signal.Notify(sigchan, os.Interrupt) // signal.Notify(sigchan, syscall.SIGTERM) var sockf string switch runtime.GOOS { case "linux": sockf = "/run/" + ipsk default: Clog("WebRun unixsock err: unknown system " + runtime.GOOS) return } os.Remove(sockf) listener, err := net.Listen("unix", sockf) if err != nil { Clog("WebRun unixsock listen err:", err) return } defer listener.Close() Clog("WebRun unixsock running: " + sockf) err = fcgi.Serve(listener, nil) if err != nil { Clog("WebRun unixsock err:", err) return } // <-sigchan // os.Remove(sockf) } else { Clog("WebRun http running: " + ipsk) err := http.ListenAndServe(ipsk, nil) if err != nil { Clog("WebRun http err:", err) return } } } func (thos *CiyWebServer) RunHTTP2(parm string) { if parm == "" { return } parms := strings.Split(parm, ",") if len(parms) != 3 { Clog("WebRun http2 param err:", parm) } err := http.ListenAndServeTLS(parms[0], parms[1], parms[2], nil) if err != nil { Clog("WebRun http2 err:", err) } Clog("WebRun http2 running: " + parm) } func GetQuery(key string, r *http.Request) string { query := r.URL.Query() if query[key] == nil { return "" } return query[key][0] } func ErrJSON(w http.ResponseWriter, errmsg string, argext ...any) bool { jsn := map[string]any{ "errmsg": errmsg, } var err error if len(argext) > 0 { for i, v := range argext { if i == 0 { if daterr, ok := v.(error); ok { err = daterr continue } } if datint, ok := v.(int); ok { if datint != 1 { jsn["code"] = datint } } else if datmap, ok := v.(map[string]any); ok { for key, val := range datmap { if key != "code" { jsn[key] = val } } } } } if err != nil { Log.Info("ErrJSON", errmsg+":"+err.Error()) } w.Write(JSON_Byte(jsn)) return false } func SuccJSON(w http.ResponseWriter, r *http.Request, obj ...map[string]any) bool { jsn := map[string]any{ "code": 1, } for _, kv := range obj { for k, v := range kv { if k != "code" { jsn[k] = v } } } reqCtx := r.Context().Value(GhttpKey).(*RequestContext) if reqCtx != nil && reqCtx.CiyAuth != "" { jsn["_ciyauth"] = reqCtx.CiyAuth } w.Write(JSON_Byte(jsn)) return true }