go tcp、udp服务
golang标准库中的网络库非常之强大,那些在其他语言处理起来非常繁琐的socket代码,在golang中变得有点呆萌,而且却非常的高效。这是因为如此目前大行其道的云服务首先golang,很多区块链都是使用golang,分布式运用和容器编排软件通用使用golang。
Tcp服务
我们先写一个tcp C/S通讯模型的demo:
package main
import (
"fmt"
"log"
"net"
"sync"
"time"
)
func main() {
addr :=":8088"
wg := sync.WaitGroup{}
server := func (){
wg.Add(1)
defer wg.Done()
l,err := net.Listen("tcp",addr)
if err !=nil {
log.Fatal(err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
handle := func(conn net.Conn) {
buff := make([]byte,1024)
n,_:=conn.Read(buff)
fmt.Println(string(buff[:n]))
conn.Write([]byte("nice to meet you too"))
}
go handle(conn)
}
}
client := func(id string) {
wg.Add(1)
defer wg.Done()
conn ,err:= net.Dial("tcp",addr)
if err !=nil {
log.Fatal(err)
}
conn.Write([]byte("nice to meet you"))
buff := make([]byte,1024)
n,_:=conn.Read(buff)
fmt.Println(id + " recv:" + string(buff[:n]))
}
go server() //启动服务端
time.Sleep(1e9) //这边停1s等服务端启动
go client("client1") //启动客服端1
go client("client2") //启动客服端2
wg.Wait()
}
$ go run main.go
nice to meet you
nice to meet you
client2 recv:nice to meet you too
client1 recv:nice to meet you to
我们用了51行代码,没有使用第三方库实现了tcp server和2个client的通讯。
Udp服务
package main
import (
"fmt"
"log"
"net"
"sync"
"time"
)
func main() {
addr :=":8088"
wg := sync.WaitGroup{}
server := func (){
wg.Add(1)
defer wg.Done()
uAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
log.Fatal(err)
}
l,err := net.ListenUDP("udp",uAddr)
if err !=nil {
log.Fatal(err)
}
for {
data := make([]byte,1024)
n,rAddr,err :=l.ReadFrom(data)
if err !=nil {
log.Fatal(err)
}
fmt.Println(string(data[:n]))
l.WriteTo([]byte("nice to meet you too"),rAddr)
}
}
client := func(id string) {
wg.Add(1)
defer wg.Done()
conn ,err:= net.Dial("udp",addr) //和tcp client代码相比,这边仅仅改了network类型
if err !=nil {
log.Fatal(err)
}
conn.Write([]byte("nice to meet you"))
buff := make([]byte,1024)
n,_:=conn.Read(buff)
fmt.Println(id + " recv:" + string(buff[:n]))
}
go server() //启动服务端
time.Sleep(1e9) //这边停1s等服务端启动
go client("client1") //启动客服端1
go client("client2") //启动客服端2
wg.Wait()
}
$ go run main.go
nice to meet you
nice to meet you
client2 recv:nice to meet you too
client1 recv:nice to meet you to
在Server端代码稍微和tcp的方式有点不一样,client端仅仅把net.Dial("tcp",addr)
改成net.Dial("udp",addr)
同样非常简单。
运用层协议
网络协议是为计算机网络中进行数据交换而建立的规则、标准或约定的集合。通俗的说就是,双方约定的一种都能懂的数据格式。
应用层协议(application layer protocol)定义了运行在不同端系统上的应用程序进程如何相互传递报文。很多运用层协议是基于tcp的,因为可靠性对运用来说非常重要。我们常见的http,smtp(简单邮件传送协议),ftp都是基于tcp。比较少数的像dns(Domain Name System)是基于udp。
根据协议的序列化方式还可以分成 二进制协议和文本协议。
文本协议:一般是由一串ACSII字符组成,常见的文本协议有http1.0,redis通讯协议(RESP)。
二进制协议:有字节流数据组成,通常包括消息头(header)和消息体(body),消息头的长度固定,并且定义了消息体的长度。
解析文本协议解析
我们写一个解析redis 协议的demo
package main
import (
"bytes"
"errors"
"io"
"log"
"net"
"strconv"
"strings"
)
var kvMap = make(map[string]string,10)
func parseCmd(buf []byte)([]string, error){ //解析redis command 协议
var cmd []string
if buf[0] == '*' {
for i:=1;i<len(buf);i++ {
if buf[i] == '\n' && buf[i-1] =='\r'{
count,_ := strconv.Atoi(string(buf[1:i-1]))
for j:=0;j<count;j++ {
i++
if buf[i] != '$' {
return nil,errors.New("error")
}
i++
si:=i
for ;i<len(buf);i++ {
if buf[i] == '\n' && buf[i-1] =='\r' {
size,_ := strconv.Atoi(string(buf[si:i-1]))
cmd = append(cmd,string(buf[i+1:i+size+1]))
i = i+size+2
break
}
}
}
}
}
}
return cmd,nil
}
func respString(msg string) []byte { //返回redis string
b := bytes.Buffer{}
b.Write( []byte{'+'})
b.Write( []byte(msg))
b.Write([]byte{'\r','\n'})
return b.Bytes()
}
func respError(msg string) []byte{ //返回redis 错误信息
b := bytes.Buffer{}
b.Write( []byte{'-'})
b.Write( []byte(msg))
b.Write([]byte{'\r', '\n'})
return b.Bytes()
}
func respNull() []byte{ //返回redis Null
return []byte{'$', '-', '1', '\r', '\n'}
}
func setKv(writer io.Writer,key,val string) {
kvMap[key] = val
resp := respString("OK")
writer.Write(resp)
}
func getV(writer io.Writer,key string) {
if v ,found := kvMap[key];found {
resp := respString(v)
writer.Write(resp)
}else {
writer.Write(respNull())
}
}
func main() {
addr := ":8088"
l, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal(err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
handle := func(conn net.Conn) {
defer conn.Close()
buf := make([]byte,1024)
n,_:=conn.Read(buf)
cmd,err := parseCmd(buf[:n])
if err !=nil {
conn.Write(respError("COMMAND not supported"))
return
}
switch strings.ToUpper(cmd[0]) {
case "SET":
setKv(conn,cmd[1],cmd[2])
case "GET":
getV(conn,cmd[1])
default:
conn.Write(respError("COMMAND not supported"))
}
}
go handle(conn)
}
}
$ go run main.go &
$ redis-cli -p 8088 ## 我们直接使用redis的cli
127.0.0.1:8088> set wida abx
OK
127.0.0.1:8088> get wida
abx
127.0.0.1:8088> llen wida
(error) COMMAND not supported
127.0.0.1:8088> get amy
(nil)
127.0.0.1:8088>
这边只是简单实现了redis get和set的功能,有兴趣的同学可以拓展下实现redis的其他功能,这样子对你了解redis的底层原理有更深的认识。
解析二进制协议
我们先定义一下消息格式
/**
请求参数:
+-----+-------+------+----------+----------+----------+
| CMD | ARGS | L1 | STR1 | Ln | STRn |
+-----+-------+------+----------+----------+----------+
| 1 | 1 | 1 | Variable | 2 | Variable |
+-----+-------+------+----------+----------+----------+
CMD 命令类型
ARG 参数个数
L1 参数一长度
STR1 参数与值
Ln 第n个参数长度
STRN 第n个参数值
返回格式
+-----+-------+----------+
| SUC | LEN | BODY |
+-----+-------+----------+
| 1 | 4 | Variable |
+-----+-------+----------+
SUC 是否成功
LEN BODY长度
BODY 消息体
*/
package main
import (
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
"sync"
"time"
)
type Command struct {
Cmd uint8
Args []string
}
func readCommand(r io.Reader) (*Command, error) {
cmd := Command{}
head := []byte{0, 0} //读取 CMD和ARGS
_, err := io.ReadFull(r, head)
if err != nil {
return nil, err
}
cmd.Cmd = head[0]
args := int(head[1])
for i := 0; i < args; i++ { //循环读取 Ln STRn
var length = []byte{0}
_, err = r.Read(length)
if err != nil {
return nil, err
}
str := make([]byte, int(length[0]))
_, err := io.ReadFull(r, str)
if err != nil {
return nil, err
}
cmd.Args = append(cmd.Args, string(str))
}
return &cmd, nil
}
func readResp(r io.Reader) (string, error) {
suc := []byte{0} //读取SUC 如果不成功就返回
_, err := r.Read(suc)
if err != nil {
return "", err
}
if int(suc[0]) != 0 {
return "", errors.New(fmt.Sprintf("resp errcode %v", suc[0]))
}
var bodyLen int32
err = binary.Read(r, binary.BigEndian, &bodyLen) //大端读去长度 4字节
if err != nil {
return "", err
}
body := make([]byte, bodyLen)
_, err = io.ReadFull(r, body) //读取body
if err != nil {
return "", err
}
return string(body), nil
}
func main() {
addr := ":8088"
wg := sync.WaitGroup{}
server := func() {
wg.Add(1)
defer wg.Done()
l, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal(err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
handle := func(conn net.Conn) {
defer conn.Close()
for {
cmd, err := readCommand(conn)
if err != nil {
break
}
fmt.Printf("server recv :%v\n",cmd)
switch cmd.Cmd {
case 1:
//拼resp 字节
conn.Write([]byte{uint8(0)})
binary.Write(conn, binary.BigEndian, int32(10)) //大端写长长度 注意要和client约定好,当然可以用小端
conn.Write([]byte("9876543210"))
case 2:
conn.Write([]byte{uint8(0)})
binary.Write(conn, binary.BigEndian, int32(16))
conn.Write([]byte("0000009876543210"))
}
}
}
go handle(conn)
}
}
client := func() {
wg.Add(1)
defer wg.Done()
conn, _ := net.Dial("tcp", addr)
//拼CMD字节
conn.Write([]byte{uint8(1), uint8(1), uint8(10)})
conn.Write([]byte("0123456789"))
ret, _ := readResp(conn)
fmt.Println("client recv:",ret)
//拼CMD字节
conn.Write([]byte{uint8(2), uint8(1), uint8(16)})
conn.Write([]byte("0123456789000000"))
ret, _ = readResp(conn)
fmt.Println("client recv:",ret)
}
go server()
time.Sleep(1e9)
go client()
wg.Wait()
}
$ go run main.go
server recv :&{1 [0123456789]}
client recv: 9876543210
server recv :&{2 [0123456789000000]}
client recv: 0000009876543210
这边我们实现了二进制协议的解析,二进制协议的解析效率会比文本方式快,而且通常情况下会省带宽。
总结
本小节介绍了golang tcp和udp server端和client端的编写,我们还介绍了运用层协议文本协议和二进制协议的编写。本小节的demo会比较有启发性,大家可以发散思维做代码的拓展。