Go学习笔记三-网络通信
1. 引言
网络通信是IT系统的必备功能,如果是做一些简单的工具类或者小软件,我们可以直接采用原生的SOCKET进行编程开发;如果项目规模比较大,则是基于一些网络通信库,比如zeromq, libevent, netty等;进一步的,使用网络通信中间件,直接利用其提供的接口进行数据收发即可。在这篇笔记中,就是说明在Golang中,如何做网络通信。
2. 网络通信模式
在一般的client-server模式中,模式是这样的: 服务器端等待client连接,client发起连接,双方建立连接后进行数据收发。但是处理方法有进化;
在早期,每建立一个连接,创建一个新进程进行处理,比如cgi;
然后,发展到建立一个新连接,创建一个新线程来处理;
现在,为了提高性能,连接和处理部分已经不是1:1的关系,因为连接越多,创建的线程越多,很多计算资源浪费在线程的调度上,所以用少部分线程处理大量连接,将计算资源用在有价值的地方,才有意义;比如WINDOWS的IOCP, linux下的epoll等,都是如此思路。
在Go中,提供了一个非常好理解的套路给我们进行socket编程,就是每建立一个连接,创建一个协程去处理。这个协程非常轻量级,不会像上文中提到的因为线程调度切换等占用大量的系统资源;但是底层依旧可以发挥系统的高性能。
3. 代码演练
3.1. 服务器端创建
服务器端的思路其实很简单,bind, accept, handle的思路,上代码:
package main
import (
"fmt"
"net"
)
//将字符数组转化为字符串
func byteToString(p []byte) string {
for i := 0; i < len(p); i++ {
if p[i] == 0 {
return string(p[0:i])
}
}
return string(p)
}
//连接处理函数,每当有连接进来,该函数就要被调用一次
func handleConn(conn net.Conn) {
connFrom := conn.RemoteAddr().String()
fmt.Println("peer info", connFrom)
defer conn.Close()
//接收数据,放置recvBuf中
recvBuf := make([]byte, 128)
conn.Read(recvBuf)
fmt.Println(byteToString(recvBuf))
//数据回写,告知client数据已收到
conn.Write([]byte("I am already recv your data. I am server, and you are client."))
}
//服务端程序的骨架,绑定IP:PORT, 等待连接,当有连接进来
//开一个协程,进行处理
func createServer(service string) {
//bind 主机:端口号
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
ln, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
fmt.Println("create server failed.\n")
return
}
//等待连接
for {
conn, err := ln.Accept()
if err != nil {
fmt.Println(err)
continue
}
//处理连接
go handleConn(conn)
}
}
func main() {
createServer("127.0.0.1:8080")
}
3.2. 客户端创建
package main
import (
"flag"
"fmt"
"io/ioutil"
"net"
"os"
)
func checkError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
//创建一次连接
//在该函数中完成了发起连接、发送数据、接收数据、关闭连接的几个动作
func OnceConn(service string) {
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
fmt.Println(tcpAddr)
//向服务器端发起连接请求
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
defer conn.Close()
//发送数据
_, err = conn.Write([]byte("hello,world"))
checkError(err)
//接收数据
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
}
func main() {
//从终端读取host,port
service := flag.String("host", "127.0.0.1:8080", "server host and port")
flag.Parse()
//测试代码,发起10次连接
for i := 1; i < 10; i++ {
OnceConn(*service)
}
}
3.3. 测试结果
服务端输出: 服务端收到了10次客户端的连接
D:\GO-WORKSPACE\src>server.exe
peer info 127.0.0.1:57015
hello,world
peer info 127.0.0.1:57016
hello,world
peer info 127.0.0.1:57017
hello,world
peer info 127.0.0.1:57018
hello,world
peer info 127.0.0.1:57019
hello,world
peer info 127.0.0.1:57020
hello,world
peer info 127.0.0.1:57021
hello,world
peer info 127.0.0.1:57022
hello,world
peer info 127.0.0.1:57023
hello,world
客户端输出: 客户端收到了服务器端的10次应答
D:\GO-WORKSPACE\src>client.exe
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
127.0.0.1:8080
I am already recv your data. I am server, and you are client.
在Go语言中,使用普通人最能理解的编程模式,实现了高性能的网络通信程序。在C++,JAVA中,按照这种写法,做出来的是程序性能是不高的,因为用的是线程,而不是协程。Go通过在语言层面的支持,屏蔽了一些较难理解的编程技法,让普通人也能写出还不错的网络通信程序。
4. 数据序列化
在网络通信程序中,数据的序列化一般有这么几种方法
- 自定义二进制格式
在我曾经的项目经历中,有过这样的一个例子,定义好不同业务数据下的所有二进制数据格式,这种定义非常精确,精确到了字节,不同位置的字节段代表某个具体的业务数据。针对每一种格式编写编码解码函数,嵌入式项目中很常见这种方式我认为可以采用工具自动生成编码解码函数,可惜我没去做,这一点,matlab中的一些仿真工具,是有很好的支持的。
在Go中,提供了Gob的二进制序列化工具,这种方式,比我们之前使用的方式要高级多了。至少,我不需要自己写编码解码函数。 我基于上文中的示例,不再发送字符串。而是发送一个结构体数据,这个数据在发送时会经过编码,在接收时会经过解码。从而说明在网络通信中如何基于Gob进行序列化工作。
进行通信的数据结构定义
type P struct {
X, Y, Z int
Name string
}
client端数据发送编码
1.首先创建encoder,对P类型的数据结构进行编码,并将数据写入创建的缓冲区sendBuf中; 2. 利用conn write方法,将二进制字节流方法出去
func OnceConn(service string) {
......
sendBuf := new(bytes.Buffer)
//代替网络连接
enc := gob.NewEncoder(sendBuf)
err = enc.Encode(P{1, 2, 3, "gobtest"})
if err != nil {
checkError(err)
}
//发送数据
_, err = conn.Write(sendBuf.Bytes())
checkError(err)
.......
}
server端接收并解码
- 接收数据,并放置缓冲区中;
- 创建decoder,并对缓冲区数据进行解码,结果放置在对应数据结构的变量中;
func handleConn(conn net.Conn) {
......
recvBuf := make([]byte, 100)
conn.Read(recvBuf)
var p P
pCache := bytes.NewBuffer(recvBuf)
dec := gob.NewDecoder(pCache)
err := dec.Decode(&p)
if err != nil {
fmt.Println("error happen.", err)
}
fmt.Println(p)
}
结果显示,服务器端收到了客户端的数据,并正确解码。
D:\GO-WORKSPACE\src>server.exe
peer info 127.0.0.1:61746
{1 2 3 gobtest}
peer info 127.0.0.1:61747
{1 2 3 gobtest}
peer info 127.0.0.1:61748
{1 2 3 gobtest}
peer info 127.0.0.1:61749
peer info 127.0.0.1:61750
peer info 127.0.0.1:61751
{1 2 3 gobtest}
peer info 127.0.0.1:61753
{1 2 3 gobtest}
peer info 127.0.0.1:61754
{1 2 3 gobtest}
{1 2 3 gobtest}
{1 2 3 gobtest}
peer info 127.0.0.1:61755
{1 2 3 gobtest}
- 自定义文本格式
文本格式的好处是机器和人都能阅读,尤其便于人阅读理解,方便调试。这种一般情况下主要采用XML或者json进行数据通信,这种方式,效率比二进制格式稍低。
此处以JSON处理为例进行说明
server端代码
使用json.Unmarshal进行解码
func handleConn(conn net.Conn) {
connFrom := conn.RemoteAddr().String()
fmt.Println("peer info", connFrom)
defer conn.Close()
//接收数据
recvBuf := make([]byte, 100)
conn.Read(recvBuf)
//对数据解码,并直接转为对应类型对象
var p P
content := byteToString(recvBuf)
err := json.Unmarshal([]byte(content), &p)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(p)
}
client端代码
使用json.Marshal对数据结构进行编码
func OnceConn(service string) {
//将字符信息转为tcpAddr
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
fmt.Println(tcpAddr)
//向服务器端发起连接请求
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
defer conn.Close()
p := P{1, 2, 3, "gobtest"}
b, err := json.Marshal(p)
content := byteToString(b)
fmt.Println(content)
//发送数据
_, err = conn.Write(b)
checkError(err)
}
服务器端数据
D:\GO-WORKSPACE\src>serverjson.exe
peer info 127.0.0.1:51809
{1 2 3 gobtest}
peer info 127.0.0.1:51810
{1 2 3 gobtest}
peer info 127.0.0.1:51811
{1 2 3 gobtest}
peer info 127.0.0.1:51812
{1 2 3 gobtest}
peer info 127.0.0.1:51813
{1 2 3 gobtest}
peer info 127.0.0.1:51814
{1 2 3 gobtest}
peer info 127.0.0.1:51815
{1 2 3 gobtest}
peer info 127.0.0.1:51816
{1 2 3 gobtest}
peer info 127.0.0.1:51817
{1 2 3 gobtest}
D:\GO-WORKSPACE\src>clientjson.exe
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
127.0.0.1:8080
{"X":1,"Y":2,"Z":3,"Name":"gobtest"}
5. 总结
在本章,我学习了基于Go语言的网络通信编程,并在此基础上对数据进行序列化,在网络进行传输,并在另外一段解码。