一直对Redis里面的pub/sub很好奇,于是用golang做下简单的实现,做个原型,效果如图:
对pub/sub 模型来说,会有这么两条通路:
channel.go
package pubsub
import (
"sync"
"fmt"
"sync/atomic"
"net"
"bufio"
)
type Channel struct {
Name string
clients map[int]*Client
sync.RWMutex
waitGroup sync.WaitGroup
messageCount uint64
exitFlag int32
}
func NewChannel(channelName string) *Channel {
return &Channel{
Name:channelName,
clients:make(map[int]*Client),
}
}
func (this *Channel) AddClient(client *Client) bool {
this.RLock()
_, found := this.clients[client.Id]
this.RUnlock()
this.Lock()
if !found {
this.clients[client.Id] = client
}
this.Unlock()
return found
}
func (this *Channel) DeleteClient(client *Client) int{
var remain int
// 整理输出信息
this.Lock()
delete(this.clients, client.Id)
this.Unlock()
this.RLock()
remain = len(this.clients)
this.RUnlock()
return remain
}
func (this *Channel) Notify(message string) bool {
this.RLock()
defer this.RUnlock()
for clientid, client := range this.clients {
this.PutMessage(clientid, message)
go handleResponse(client.conn, message)
}
this.waitGroup.Done()
return true
}
func handleResponse(conn net.Conn, message string) {
// todo alive check
fmt.Println("handleResponse: ", message)
writter := bufio.NewWriter(conn)
writter.WriteString("[CONSUME MESSAGE] " + message)
writter.Flush()
}
func (this *Channel) Wait() {
//this.waitGroup.Wait()
}
func (this *Channel) Exit() {
if !atomic.CompareAndSwapInt32(&this.exitFlag, 0, 1) {
return
}
this.Wait()
}
func (this *Channel) Exiting() bool {
return atomic.LoadInt32(&this.exitFlag) == 1
}
func (this *Channel) PutMessage(clientid int, message string) {
this.RLock()
defer this.RUnlock()
if this.Exiting() {
return
}
fmt.Println("{", this.Name, "}[", clientid, "] ", message)
atomic.AddUint64(&this.messageCount, 1)
return
}
** server.go**
package pubsub
import (
"sync"
"net"
"strconv"
"bufio"
"fmt"
"strings"
)
type Client struct {
Id int
Ip string
conn net.Conn
}
type Server struct {
// map[Channel.Name]*Channel 一个channel会被很多个client给subscribe了
Bucket map[string]*Channel
sync.RWMutex
}
func NewServer() *Server {
return &Server{
Bucket:make(map[string]*Channel),
}
}
func (this *Server) Run(host string, port int) error {
address := host + ":" + strconv.Itoa(port)
listener, err := net.Listen("tcp", address)
if err != nil {
return err
}
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go this.HandleRequest(conn)
}
}
func (this *Server) HandleRequest(conn net.Conn) {
//defer conn.Close()
for {
bytes, _, _ := bufio.NewReader(conn).ReadLine()
content := strings.Trim(strings.Trim(string(bytes), " "), "\n")
fmt.Println(fmt.Sprintf("request string: [%s]", content))
writter := bufio.NewWriter(conn)
if content == "subscribe" {
address := conn.RemoteAddr().String()
splits := strings.Split(address, ":")
clientid, _ := strconv.Atoi(splits[1])
client := &Client{
Id: clientid,
Ip: splits[0],
conn: conn,
}
topic := "hello"
this.Subscribe(client, topic)
writter.WriteString(address)
} else if content == "publish" {
message := "PUBLISH MESSAGE"
topic := "hello"
this.PublishMessahe(topic, message)
}else if content == "quit" {
content = "client quited"
break
}else {
fmt.Println("common chat " + content)
writter.WriteString(content + "\n")
writter.Flush()
}
writter.WriteString(content + "\n")
}
}
func (this *Server)Subscribe(client *Client, channelName string) {
this.RLock()
channel, found := this.Bucket[channelName]
this.RUnlock()
if found {
channel.AddClient(client)
}else{
// create a new channel, add this client
newchannel := NewChannel(channelName)
newchannel.AddClient(client)
this.Lock()
this.Bucket[channelName] = newchannel
this.Unlock()
}
}
func (this *Server) Ubsubscribe(client *Client, channelName string) {
this.RLock()
channel, found := this.Bucket[channelName]
this.RUnlock()
if found {
remain := channel.DeleteClient(client)
if remain == 0 {
channel.Exit()
this.Lock()
delete(this.Bucket, channelName)
this.Unlock()
}
}
}
func (this *Server) PublishMessahe(channelName, message string) (bool, string) {
this.RLock()
channel, found := this.Bucket[channelName]
defer this.RUnlock()
if !found {
return false, "channelName 不存在"
}
channel.waitGroup.Add(1)
go channel.Notify(message)
channel.waitGroup.Wait()
return true, ""
}
main.go
package main
import (
"github.com/guoruibiao/pubsub"
"log"
"fmt"
)
func main() {
server := pubsub.NewServer()
fmt.Println("server running...")
err := server.Run("localhost", 8080)
if err != nil {
log.Fatal(err)
}
}
首先肯定是先把服务跑起来啦,如下:
$ go run main.go
server running...
然后是客户端链接测试,因为底层实现是用的tcp链接,所以可以借助netcat,这样就不用单独再写golang的客户端连接了。
打开第一个终端, 输入subscribe
nc localhost 8080
subscribe
打开第二个终端,输入subscribe
nc localhost 8080
subscribe
打开第三个终端,输入publish
nc localhost 8080
publish
就可以看到,第一、第二个终端有来自服务器的publish数据推送了,具体可以查看上面的GIF图。
简单的订阅发布机制实现(Golang)