本周帮同事做一个测试用的工具,一个模拟的服务器,根据请求中的用户ID从数据库中查找预先设置好的响应返回给客户端。前一段时间看了《Effective Go》和《Network Programming with Go》,正想练练手,就决定用Go来写。
本文涉及的内容:
- 网络编程
- Json解析
- 数据库操作
准备工作
安装和配置Go语言开发环境,过程参见官方文档。
安装MySQL驱动:
$ go get github.com/go-sql-driver/mysql
服务器框架
Go语言天生就是为服务器开发而设计的,因而对网络接口的封装非常友好。在主函数main()中,调用net.Listen()创建一个Listener监听服务器端口。然后在主循环中用Accept()接受客户端连接请求。Go语言内建了对协程的支持,称作goroutine。在调用函数前加上“go”关键字,就可以创建一个goroutine来执行该函数。这里对每一个客户端连接建立一个协程处理请求。协程可以简化并发编程(concurrent programming)。不过需要注意的是,默认情况下使用goroutine并不能利用多核处理器的并行性来提高性能。Go语言默认对每个进程只使用一个线程,因此即使使用了多个goroutine,在CPU上仍然是串行执行的。如果要使用多线程,需要调用runtime.GOMAXPROCS(NCPU)来设置使用的CPU核数。详请可以参考《Effective Go》中的“并发”一节。
handleConnection()函数负责从客户端接收请求。由于使用TCP协议,客户端请求以字节流的方式传输,因此服务器端需要进行切包。在这个应用场景里,请求为Json字符串,以0表示结束。每次从客户端连接读取到数据之后,都去查找是否有0值,来确定请求字符串是否接收完。
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"encoding/json"
"flag"
"io"
"log"
"net"
"os"
)
var addr = flag.String("addr", "0.0.0.0:10000", "server address")
func handleConnection(conn net.Conn) {
request_buf := make([]byte, 1024)
request_offset := 0
for {
if request_offset >= 1024 {
log.Fatal("receive buffer overflow")
os.Exit(1)
}
readlen, err := conn.Read(request_buf[request_offset:])
if err == io.EOF {
log.Println("connection closed")
conn.Close()
return
} else if err != nil {
log.Println("error reading: ", err.Error())
conn.Close()
return
}
log.Printf("%d bytes read\n", readlen)
i := request_offset
request_offset += readlen
for ; i < request_offset; i++ {
if request_buf[i] == 0 {
var req Request
err := json.Unmarshal(request_buf[:i], &req)
if err != nil {
log.Printf("failed parsing request: %v, %v\n", request_buf[:i], err.Error())
} else {
req.conn = conn
handleRequest(conn, req)
}
// if there are any bytes left, move them to the front of the buffer
i += 1
if i < request_offset {
copy(request_buf, request_buf[i:request_offset])
request_offset = request_offset - i
i = 0
} else {
request_offset = 0
break
}
}
}
}
}
func main() {
flag.Parse()
log.Println("starting server on: ", *addr)
l, err := net.Listen("tcp", *addr)
if err != nil {
log.Fatal("failed listening", err.Error())
os.Exit(1)
}
defer l.Close()
for {
// Listen for an incoming connection.
conn, err := l.Accept()
if err != nil {
log.Println("Error accepting: ", err.Error())
}
go handleConnection(conn)
}
}
请求解析
包encoding/json里含了对Json串进行编解码函数。使用json.Marshal()可以将一个对象串行化成Json字符串,使用json.Unmarshal()可以将Json字符串反串行化。声明一个Request结构,其成员变量对应想要解析的Json字段,行末的`json:"userid"`指定了成员变量和Json字段的对应关系。注意UserID首字母必须大写,否则在调用Marshal()和Unmarshal()时会被忽略。请求串里包含了多个字段,但是我们只需要userid这一个,因此只也需要一个成员变量。
type Request struct {
UserID string `json:"userid"`
}
如果结构体的成员名字和Json字段的名字一致,比如这里的用户ID在Json串中也叫“UserID”,就可以更简单一点:
type Request struct {
UserID string
}
访问数据库
Go运行时里包含了对SQL数据库的支持,但是要访问数据库还需要自行安装对应的驱动。这里的数据库是MySQL,驱动安装方法见第1节。
在main()函数中初始化数据库。db_addr是用于连接数据库的地址,其格式可以看这里。db.Prepare()函数创建一个查询语句,后续可以直接通过这个Stmt对象用不同的参数进行查询。这一步不是必须的,也可以直接调用db.Query()或者db.QueryRow()通过SQL语句进行查询。如果查询语句需要多次被使用的话,还是先Prepare()一下比较好。
db_addr := *db_user + ":" + *db_pass + "@tcp(" + *db_host + ":" + *db_port + ")/" + *db_name
db, err := sql.Open("mysql", db_addr)
if err != nil {
log.Fatal("failed connecting db: ", err.Error())
os.Exit(1)
}
defer db.Close()
stmt, err = db.Prepare("SELECT s_response FROM tbmockdata WHERE s_userid = ?")
if err != nil {
log.Println("db.Prepare() failed", err.Error())
os.Exit(1)
}
defer stmt.Close()
实现handleRequest()函数。db.QueryRow()从数据库中查询一行数据,返回Row对象。Scan()方法的参数为interface类型,将查询出的数据转换成指定的类型并输出。完成查询之后,将响应写通过conn写回给客户端。
func handleRequest(conn net.Conn, req Request) {
log.Println("received request: ", req.UserID)
var response []byte
err := stmt.QueryRow(req.UserID).Scan(&response)
switch {
case err == sql.ErrNoRows:
log.Println("no response found")
case err != nil:
log.Println("failed query: ", err.Error())
default:
log.Println("response: ", string(response))
}
response = append(response, 0)
req.conn.Write(response)
}
总结
说一下自己对Go语言的一些理解和体会:
-
Go语言在语法设计上做了很多新的尝试,有些确实解决了以前用C和C++编程的痛点。比如函数可以有两个返回值,一个是函数的输出,一个是错误信息。在C和C++里编程里,通常只能用某些特殊返回值(比如-1, NULL)表示执行错误,或者是返回值错误码,而真正的输出则通过参数传递出来。另外,defer这个特性很有用。实际开发中常常会碰到这样一种场景:一个操作需要经过若干个步骤才能完成,其中每一个都有可能出错,如果在其中某一步出错,就要取消前面步骤所造成的影响(比如分配内存、打开文件等),然后退出。以前读Linux内核代码时,这种情况特别常见,内核代码都是用goto来解决这个问题的。有了defer,问题就简单多了,比如像下面这样把分配和释放写在一起。
alloc() defer free()
不过Go语言也有些特性我表示不是很能理解。比如声明了变量而没有使用,又或者import了某个package而没有使用,在Go语言里就是一个error而不是warning。像本文中的代码里,import了"github.com/go-sql-driver/mysql"这个package,但是没有显式的调用,这就比较尴尬了。为了解决这个问题,Go语言又引入了“_”这种空白标识符。。。
Goroutine。内建的协程支持部分的解决了并发编程的问题。我们在工作中进行业务开发的时候,也大量的使用到了协程。使用协程进行并发编程的时候,跟写串行程序没有太大区别,使得开发效率大大提升。但是协程并不是万能的。比如我就碰到过一个坑:客户端请求并发量过大,导致服务端创建了大量协程来处理,而每个协程又需要创建一个跟下游服务的连接,导致下游连接数爆掉。这种情况下就需要共享下游连接,协程并不能帮你解决。
虽然是C语言之父设计的,但是从使用者的角度来看,Go语言更接近Java:自带GC,无需手动分配和释放内存;运行时内建了丰富的函数库,还自带包管理机制,简化开发。相比C/C++来说,Go语言应该算是一个很大的进步,在大大提升开发效率的同时,也尽可能的保留了高性能。对于互联网行业的后台开发人员,还是很值得一试的。