【go语言实现服务器接收http请求以及出现泄漏时的解决方案】
一、关于基础的程序的实现
刚开始的时候程序是这样实现的:
// Hello package main import ( "database/sql" "fmt" "io/ioutil" "log" "net/http" "strings" "time" _ "github.com/Go-SQL-Driver/MySQL" ) func main() { fmt.Println(time.Now().Format("2006-01-02 15:04:05")) openHttpListen() //saveToDb() } func openHttpListen() { http.HandleFunc("/monkeytest", receiveClientRequest) fmt.Println("go server start running...") err := http.ListenAndServe("1.2.3.4:5555", nil) //这里监听的地址要换成你自己的IP和端口;比如说你通过ifconfig查看自己的IP是15.34.67.23,则这里就要替换成这个IP,不能是其他的IP,要不然会报错 if err != nil { log.Fatal("ListenAndServe: ", err) } } func receiveClientRequest(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Println("收到客户端请求: ", r.Form) fmt.Println("method:", r.Method) fmt.Println("path:", r.URL.Path) fmt.Println("scheme:", r.URL.Scheme) fmt.Println("url", r.URL) for k, v := range r.Form { fmt.Printf("----------\n") fmt.Println("key:", k) fmt.Println("value:", strings.Join(v, ", ")) } var className string var pkgName string var pkgVer string var leakRoot string var leakDetail string var pkgbuildtime string if len(r.Form["className"]) > 0 { className = r.Form["className"][0] } if len(r.Form["pkgName"]) > 0 { pkgName = r.Form["pkgName"][0] } if len(r.Form["pkgVer"]) > 0 { pkgVer = r.Form["pkgVer"][0] } if len(r.Form["leakRoot"]) > 0 { leakRoot = r.Form["leakRoot"][0] } if len(r.Form["leakDetail"]) > 0 { leakDetail = r.Form["leakDetail"][0] } if len(r.Form["buildtime"]) > 0 { pkgbuildtime = r.Form["buildtime"][0] } body, _ := ioutil.ReadAll(r.Body) //r.Body.Close() body_str := string(body) fmt.Println("body_str:", body_str) fmt.Println("header", r.Header) //fmt.Println("Customerid", r.Header.Get("Customerid")) w.Header().Set("Access-Control-Allow-Origin", "origin") var result string if len(leakDetail) != 0 { result = saveToDb(className, pkgName, pkgVer, leakRoot, leakDetail, pkgbuildtime) } else { result = "error" } fmt.Fprintf(w, result) } func saveToDb(className string, pkgName string, pkgVer string, leakRoot string, leakDetail string, pkgbuildtime string) string { db, err := sql.Open("mysql", "username:passwd@tcp(2.3.4.5:3306)/myku?charset=utf8") //这里的数据库要换成自己的用户名:密码@数据库地址:端口/数据库名 checkErr(err) if err != nil { return "error" } //插入数据 stmt, err := db.Prepare("insert mytable SET className=?,pkgName=?,pkgVer=?,leakRoot=?,leakDetail=?,leakDate=?,pkgbuildtime=?") //这里的mytable换成自己的table表名 //stmt, err := db.Prepare("insert into mytable(className,pkgName,pkgVer,leakRoot,leakDetail,leakDate,pkg) values (?,?,?,?,?,?)") //rows, err := db.Query("select * from mytable") checkErr(err) if err != nil { return "error" } /*fmt.Println("res.", rows) for rows.Next() { var className string rows.Columns() err = rows.Scan(&className) checkErr(err) fmt.Println(className) }*/ //checkErr(err) res, err := stmt.Exec(className, pkgName, pkgVer, leakRoot, leakDetail, time.Now().Format("2006-01-02 15:04:05"), pkgbuildtime) fmt.Println("res.", res) if err != nil { return "error" } else { return "success" } } func checkErr(err error) { if err != nil { fmt.Println("error.") //panic(err) } }
后来因为提示存在too many open files的问题,就做了一版修改,改成了:
// Hello package main import ( "database/sql" "fmt" "io/ioutil" "log" "net/http" "strings" "time" _ "github.com/Go-SQL-Driver/MySQL" ) func main() { fmt.Println(time.Now().Format("2006-01-02 15:04:05")) openHttpListen() //saveToDb() } func openHttpListen() { srv := &http.Server{ Addr: "1.2.3.4:5555", ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } http.HandleFunc("/monkeytest", receiveClientRequest) fmt.Println("go server start running...") err := srv.ListenAndServe() if err != nil { log.Fatal("ListenAndServe: ", err) } } func receiveClientRequest(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Println("收到客户端请求: ", r.Form) fmt.Println("method:", r.Method) fmt.Println("path:", r.URL.Path) fmt.Println("scheme:", r.URL.Scheme) fmt.Println("url", r.URL) for k, v := range r.Form { fmt.Printf("----------\n") fmt.Println("key:", k) fmt.Println("value:", strings.Join(v, ", ")) } var className string var pkgName string var pkgVer string var leakRoot string var leakDetail string var pkgbuildtime string if len(r.Form["className"]) > 0 { className = r.Form["className"][0] } if len(r.Form["pkgName"]) > 0 { pkgName = r.Form["pkgName"][0] } if len(r.Form["pkgVer"]) > 0 { pkgVer = r.Form["pkgVer"][0] } if len(r.Form["leakRoot"]) > 0 { leakRoot = r.Form["leakRoot"][0] } if len(r.Form["leakDetail"]) > 0 { leakDetail = r.Form["leakDetail"][0] } if len(r.Form["buildtime"]) > 0 { pkgbuildtime = r.Form["buildtime"][0] } defer r.Body.Close() body, _ := ioutil.ReadAll(r.Body) //r.Body.Close() body_str := string(body) fmt.Println("body_str:", body_str) fmt.Println("header", r.Header) //fmt.Println("Customerid", r.Header.Get("Customerid")) w.Header().Set("Access-Control-Allow-Origin", "origin") var result string if len(leakDetail) != 0 { result = saveToDb(className, pkgName, pkgVer, leakRoot, leakDetail, pkgbuildtime) } else { result = "error" } fmt.Fprintf(w, result) } func saveToDb(className string, pkgName string, pkgVer string, leakRoot string, leakDetail string, pkgbuildtime string) string { db, err := sql.Open("mysql", "username:passwd@tcp(2.3.4.5:3306)/myku?charset=utf8") defer db.Close() checkErr(err) if err != nil { return "error" } //插入数据 stmt, err := db.Prepare("insert mytable SET className=?,pkgName=?,pkgVer=?,leakRoot=?,leakDetail=?,leakDate=?,pkgbuildtime=?") //stmt, err := db.Prepare("insert into mytable (className,pkgName,pkgVer,leakRoot,leakDetail,leakDate,pkg) values (?,?,?,?,?,?)") //rows, err := db.Query("select * from mytable") checkErr(err) if err != nil { return "error" } /*fmt.Println("res.", rows) for rows.Next() { var className string rows.Columns() err = rows.Scan(&className) checkErr(err) fmt.Println(className) }*/ //checkErr(err) res, err := stmt.Exec(className, pkgName, pkgVer, leakRoot, leakDetail, time.Now().Format("2006-01-02 15:04:05"), pkgbuildtime) fmt.Println("res.", res) if err != nil { return "error" } else { return "success" } } func checkErr(err error) { if err != nil { fmt.Println("error.") //panic(err) } }
然后在linux下通过nohup go run Hello.go &之后,程序正式跑起来,(注意:服务器的IP一定是本地的IP地址才可以)就可以在浏览器里面输入:
http://1.2.3.4:5555/monkeytest,然后就能将请求提交到go的服务器端
二、关于go中出现的问题的解决(包括发现问题、解决问题的过程)
将一中的程序,在linux下运行之后,通过nohup go run Hello.go &运行之后,会将实时的信息全部打印到nohup.out中,查看这个文件,会出现这个提示:
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 5ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 10ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 20ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 40ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 80ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 160ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 320ms
2017/07/28 01:51:35 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 640ms
2017/07/28 01:51:36 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 1s
2017/07/28 01:51:37 http: Accept error: accept tcp 10.252.149.243:9090: accept4: too many open files; retrying in 1s
这种情况下就会导致该写入到数据库中的内容无法写入。
因为:在linux下一切都是文件,所有不管是nohup.out还是socket连接都是文件,所以这里就可以进行查找在当前pid下的文件数有几个了,可以通过下面2的(2)中的方式查看某个pid下的文件数及详情
经过查找之后,发现这种情况可以通过以下的方式先缓解,首先通过:
1、ulimit -n查看最大连接数,如果是1024的话,可以尝试将其修改为4096
2、这样无法根本上解决问题,继续查:
(1)打开文件太多,是否说明文件句柄出现了泄漏,或者是:db操作出现了泄漏,那么是否程序中没有关闭呢?
查看之后,确实没有关闭,因此增加:defer db.close()和defer f.close()的处理
这里defer的含义是:代表在return之前执行关闭,但是有弊端,尤其是跟带命名的返回参数一起使用时,具体是:https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html 文章中描述的这样:
函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。
其实原因就是:return xxx并不是一条原子指令
但是因为close的操作中没有增加这个返回参数,所以影响不大可以这样用
(2)然后修改之后,重新启动程序,不断创造连接数据库及打开文件的处理操作:
通过下面的命令:
首先获取go的pid值:ps aux | grep go,例如得到的结果是:29927
然后再执行:ls -l /proc/29927/fd/ | wc -l ,得到的结果就是:6,说明socket的数目没有增长
然后再执行:ls -l /proc/29927/fd/ ,得到的结果就是:当前进程打开的连接数的信息详情
备注:在(1)中未增加defer close()操作之前, socket的数目会随着http的请求和数据库的连接增多
后面会继续关注这里,查看问题是否完全解决了。