本章从nats的协议出发, 来阐述如何遵循协议实现一个client
package main
import (
"bufio"
"fmt"
"log"
"net"
"sync"
)
type Client struct {
conn net.Conn
w *bufio.Writer
sync.Mutex
}
func NewClient() *Client {
return &Client{}
}
func (c *Client) Connect(uri string) error {
conn, err := net.Dial("tcp", uri)
if err != nil {
return err
}
c.conn = conn
c.w = bufio.NewWriter(conn) //
return nil
}
func (c *Client) Publish(subject string, payload []byte) error {
c.Lock()
defer c.Unlock()
pub := fmt.Sprintf("pub %s %d\r\n", subject, len(payload))
_, err := c.w.WriteString(pub)
if nil != err {
return err
}
_, err = c.w.Write(payload)
if nil != err {
return err
}
_, err = c.w.WriteString("\r\n")
if nil != err {
return err
}
return c.w.Flush()
}
func (c *Client) Close() {
c.Lock()
defer c.Unlock()
c.conn.Close() // along with the writer
}
func main() {
nc := NewClient()
err := nc.Connect(":4222")
if err != nil {
log.Fatalf("Error: %s", err)
}
defer nc.Close()
err = nc.Publish("hello", []byte("world"))
if err != nil {
log.Fatalf("Error: %v", err)
}
log.Println("done")
}
原文链接:
https://www.oreilly.com/ideas/building-messaging-in-go-network-clients
附上简单read/write实现.
read需要在单独的goroutine中完成.
package main
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"net"
"strings"
"sync"
)
type MsgHandler func(subject string, reply string, sid int)
type ClientImpl struct{}
func (c *ClientImpl) processMsg(subj string, reply string, sid int, payload []byte) {
panic(errors.New("you shall override this method"))
}
func (c *ClientImpl) processInfo(line string) {}
func (c *ClientImpl) processPing() {}
func (c *ClientImpl) processPong() {}
func (c *ClientImpl) processErr(msg string) {}
// true, we shall continue
// false: no. stop reading routine.
func (c *ClientImpl) processOpErr(err error) bool {
return false
}
type client interface {
processInfo(line string)
processMsg(subj string, reply string, sid int, payload []byte)
processPing()
processPong()
processErr(msg string)
processOpErr(err error) bool
}
func readLoop(c client, conn net.Conn, whenDone func()) {
defer whenDone()
r := bufio.NewReader(conn)
for {
line, err := r.ReadString('\n')
if err != nil {
if !c.processOpErr(err) {
return
}
}
line = strings.TrimSuffix(line, "\r\n") // if not ends with \r\n, it will remain unchanged.
args := strings.SplitN(line, " ", 2)
if len(args) < 1 {
c.processOpErr(errors.New("Error: malformed control line"))
return
}
op := strings.TrimSpace(args[0])
switch op {
case "MSG":
//fmt.Printf("Msg: <%v>\n", line)
var subject, reply string
var sid, size int
n := strings.Count(args[1], " ")
switch n {
case 2:
// No reply is expected in this case
// (message is just broadcast)
// MSG foo 1 3\r\n
// bar\r\n
_, err := fmt.Sscanf(args[1], "%s %d %d",
&subject, &sid, &size)
if err != nil {
c.processOpErr(err)
return
}
// ok, good to go
case 3:
// Reply is expected
// MSG foo 1 bar 4\r\n
// quux\r\n
_, err := fmt.Sscanf(args[1], "%s %d %s %d",
&subject, &sid, &reply, &size)
if err != nil {
c.processOpErr(err)
return
}
default:
c.processOpErr(errors.New("nats: bad control line"))
return
}
payload := make([]byte, size)
_, err = io.ReadFull(r, payload)
if err != nil {
c.processOpErr(err)
return
}
// In the two-argument case, the reply below is null.
c.processMsg(subject, reply, sid, payload)
case "INFO":
c.processInfo(line)
case "PING":
c.processPing()
case "PONG":
c.processPong()
case "+OK":
// Do nothing
case "-ERR":
c.processOpErr(errors.New(args[1]))
}
}
}
/
//
/
type Client struct {
ClientImpl // the default implementation
conn net.Conn
w *bufio.Writer
seqID int
sync.Mutex
cbs map[int]MsgHandler
waitGroup sync.WaitGroup
}
func NewClient() *Client {
return &Client{
seqID: 1000,
cbs: make(map[int]MsgHandler),
}
}
func (c *Client) Connect(uri string) error {
conn, err := net.Dial("tcp", uri)
if err != nil {
return err
}
c.conn = conn
c.w = bufio.NewWriter(conn) //
return nil
}
func (c *Client) PublishString(subject string, content string) error {
return c.Publish(subject, []byte(content))
}
func (c *Client) Publish(subject string, payload []byte) error {
c.Lock()
defer c.Unlock()
pub := fmt.Sprintf("pub %s %d\r\n", subject, len(payload))
_, err := c.w.WriteString(pub)
if nil != err {
return err
}
_, err = c.w.Write(payload)
if nil != err {
return err
}
_, err = c.w.WriteString("\r\n")
if nil != err {
return err
}
return c.w.Flush()
}
func (c *Client) Subscribe(subject string, cb MsgHandler) error {
c.Lock()
defer c.Unlock()
subjectID := c.seqID //use once
c.seqID++
sub := fmt.Sprintf("sub %s %d\r\n", subject, subjectID)
_, err := c.w.WriteString(sub)
if nil != err {
return err
}
c.cbs[subjectID] = cb
return c.w.Flush()
}
//override
func (c *Client) processMsg(subj string, reply string, sid int, payload []byte) {
cb, ok := c.cbs[sid]
if !ok {
log.Printf("missing callback for sid:<%v>", sid)
}
cb(subj, reply, sid)
}
func (c *Client) StartRead() {
c.waitGroup.Add(1)
go readLoop(c, c.conn, func() {
c.waitGroup.Done()
})
}
func (c *Client) Join() {
c.waitGroup.Wait()
// log.Printf("join done")
}
func (c *Client) Close() {
c.Lock()
defer c.Unlock()
c.conn.Close() // along with the writer
}
func main() {
nc := NewClient()
err := nc.Connect("myvnc:4222")
if err != nil {
log.Fatalf("Error: %s", err)
}
// err = nc.Publish("hello", []byte("world"))
// if err != nil {
// log.Fatalf("Error: %v", err)
// }
const sendCount = 10
left := sendCount
doneCh := make(chan struct{})
recvID := 0
nc.Subscribe("hello", func(subject string, reply string, sid int) {
fmt.Printf("recv %v: %v <%v>\n", recvID, subject, reply)
recvID++
left--
if left <= 0 {
close(doneCh)
}
})
nc.StartRead()
for i := sendCount; i > 0; i-- {
if err := nc.PublishString("hello", "world"); err != nil {
log.Fatalf("Error: %v", err)
}
}
<-doneCh
nc.Close()
nc.Join()
log.Println("done")
}