go语言实现服务器接收http请求以及出现泄漏时的解决方案

Posted 可可_小虾米

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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:[email protected](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:[email protected](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的请求和数据库的连接增多

 

后面会继续关注这里,查看问题是否完全解决了。

 

以上是关于go语言实现服务器接收http请求以及出现泄漏时的解决方案的主要内容,如果未能解决你的问题,请参考以下文章

go语言实现简单的聊天室

Go分布式缓存 HTTP 服务端(day3)

Go分布式缓存 HTTP 服务端(day3)

5.Go语言高并发与微服务实战 --- 构建 Go Web 服务器

Go语言异步服务器框架原理和实现

Go http包执行流程