背景
目前后台业务系统的大部分接口都是以同步阻塞式的方式工作,资源利用率低,单机qps有限。因为go语言原生支持协程,能够同时满足开发效率和程序性能,于是决定引入go语言进行改造。
主要是分享以下三点心得:
- C/C++库的封装
- map内部成员赋值,以及protobuf协议的支持
- 网络I/O超时处理
C/C++库的封装
环境搭建,语法这些就不赘述了。阅读 A tour of go 可以很好的认识go语言。
引入go语言,碰到的第一个问题是如何复用已有的经过了长期线上检验的C/C++基础库,所幸go语言通过Cgo可以很方便的调用C库中函数。具体方法见Command cgo。在使用过程中发现,Cgo可以支持C,但无法完美支持C++,在import C关键字上面include进来的头文件中,如果有包含C++的头文件时例如string,Cgo将会报错,另外C++的namespace也会让我们无法用go语言访问namespace下的函数或变量。解决办法是用C来包裹C++,通过C来调用C++,go来调用C解决。如我有一个用C++编写的库libtest.a和test.h,其中test.h中有包含C++中的string,这时候按照Command cgo中的方法,在test.go文件中引入:
packege test
// #cgo LDFLAGS: -L/usr/local/comm/lib -ltest -lstdc++
// #cgo CPPFLAGS: -I/usr/local/comm/include
// #include "test.h"
编译过程中会报错。注意,上面使用的都是绝对路径,这是因为当我们在build go项目的时候,相对路径是以执行build命令时的路径来作为起点的,否则一旦build时的目录改变,就会导致Cgo封装失败。
这时候,为了调用test库,我们只好再写一个t.h和t.cpp(随便取的)
其中t.h的内容
#ifdef __cplusplus
extern "C" {
#endif
void C_test();//我们通过该函数来调用test库中的函数,这里的命名都是随便取的
#ifdef __cplusplus
}
#endif
在t.cpp文件的内容:
#include "t.h"
#include "test.h"
#ifdef __cplusplus
extern "C" {
#endif
void C_test(){
以C_test函数来封装test库中的函数
...
}
#ifdef __cplusplus
}
#endif
通过将t.cpp编译成目标文件:
g++ -c t.cpp -Iabcd -Lefg -ltest
有两种方式,一是将t.o打包进原来的libtest.a中,第二种当然就是将其打包为独立的一个静态库,这里推崇第二种方式:
ar -r libt.a t.o
这时候在对应的go文件中引入t.h和t.a即可完成go对C++库的调用。
packege test
// #cgo LDFLAGS: -L./ -lt -L/usr/local/comm/lib -ltest -lstdc++
// #cgo CPPFLAGS: -I./ -I/usr/local/comm/include
// #include "t.h"
map内部成员赋值,以及protobuf协议的支持
在go语言的使用过程中,发现map结构中的value为自定义类型时,无法对该自定义类型内部的成员进行进行"写"操作(map结构中返回的value不可对其成员寻址),如运行以下代码:
package main
import "fmt"
type A struct{
T int
}
func main(){
m := make(map[int]A)
a := A{1}
m[1] = a
m[1].T = 2
fmt.Println(m)
}
编译器会返回不可对m[1].T赋值的错误。但是当map中的value为指针时即可,如将第7,8,9行代码换成如下:
m := make(map[int]*A)
a := A{1}
m[1] = &a
这时即可完成对m[1].T的赋值。另外在map结构中的value类型为go的原生类型时则不存在这个问题(即byte/int8/16/32/64 float32/64,string,map,slice等等),如:
m := make(map[int]map[int]int)
ma := make(map[int]int)
ma[1] = 1
m[1] = ma
m[1][1] = -1
fmt.Println(m)
这时读写都没问题。但是在slice中却不存在这个问题。
在go中,将protobuf协议文件生成相应的go文件之后,对于每个字段生成的变量都是指针类型。protobuf协议在git上有golang开源的protobuf协议(毕竟都是google的东西),详细使用可以参见链接这里简单谈一下对于用指针的理解,使用指针可以更大程度的达到节流的目的,(而这里的节流带来的好处是更快的编解码速度,数据包交互完成速度(因为要传输的数据量少了),节省带宽),我们通过判断指针值是否为nil,为nil时则直接跳过,不为nil时说明有数据才将其序列化。
网络I/O超时处理
由于复杂的网络环境,我们必须考虑对来自客户端的每个请求的收包和回包以及系统内部调用链之间的网络请求设置超时处理,否则系统可能会存在大量无法释放的连接。
1 http server与client之间的超时设置
对于http服务器一般都是使用go标准库中的http包,只需要简单几行代码即可创建一个http server,如:
http.HandleFunc("/hello",func(w http.ResponseWriter,r *http.Request){
io.WriteString(w,"hello world")
});
http.ListenAndServe()
这里其实是使用了内部的变量DefaultServeMux,但是它默认并不支持I/O超时,因此我们需要自己创建http.Server来提供服务:
svr := &http.Server{
Addr: "0.0.0.0:8080",
ReadTimeout: 4 * time.Second,
WriteTimeout: 4 * time.Second,
}
svr.ListenAndServe()
其中ReadTimeOut是从Accept请求后到RequestBody完全读取的时间,WriteTimeOut是从RequstBody开始读取到完整回包的时间。
2 系统内部调用链的超时设置
系统内部的服务之间经常需要协同服务才能完成对外的请求,因此通信无法避免。通过给建立好的连接对象net.Connd调用SetDeadline,SetReadDeadline,SetWriteDeadline三个方法设置Deadline可以达到对每次I/O进行超时控制,一旦超时后对该连接对象进行Close操作。顾名思义,就是分别对连接对象设置读写超时,读超时和写超时的函数,他们的设置是永久生效而不是只作用于一次I/O,一旦超时后将返回超时错误,因此每次I/O操作前都需要调用它们,这里贴一个简单的例子:
func SendOnePacket(conn net.Conn, packet []byte, timeout int64) error {
conn.SetWriteDeadline(time.Now().Add(time.Duration(int64(time.Millisecond) * timeout)))
for {
n, err := conn.Write(packet)
if err != nil {
return errors.New("Write error:" + err.Error())
}
if n == len(packet) {
return nil
}
packet = packet[n:]
}
return nil
}
func RecvOnePacket(conn net.Conn, timeout int64) ([]byte, error) {
conn.SetReadDeadline(time.Now().Add(time.Duration(int64(time.Millisecond)*timeout)))
recvBuf := bytes.NewBuffer(nil)
var msgLen int = 0
var buf [4096]byte
for {
n, err := conn.Read(buf[0:])
recvBuf.Write(buf[0:n])
if err != nil && err != io.EOF {
str := string(recvBuf.Bytes())
return nil, errors.New("Read Error:" + err.Error()+ " data:"+str)
}
msgLen = CheckPacket(recvBuf)
if msgLen > 0 {
break
}
if msgLen < 0 {
return nil,errors.New("CheckPacket error,ret:" + strconv.FormatInt(int64(msgLen),10))
}
}
packet := recvBuf.Bytes()
return packet[:msgLen], nil
}
CheckPacket函数根据自己的通信协议来实现,用于检查包是否完整,返回负数代表出错,为0表示不完整,大于0表示是接收完成,且该值为该数据包的完整长度。其中RecvOnePacket中的buf数组其实非常讲究,它会决定每一次的系统调用---Read函数,最多读取多少个字节(如果传入Read函数的切片长度为0的话直接就返回了),这个示例并为对数据缓存,因此出现粘包的话就雪崩了。
总结
目前小组内部也是刚引入go语言,对go的特性,深层次的一些实现理解还很浅。