golang使用channel实现broadcast

        最近的一个项目需要一个类似于广播的功能, 当客户端连接到服务端以后, 需要把相同的一份数据分别发送给每个客户端, 功能虽然简单, 但是还是有坑, 主要的坑就是channel阻塞的问题. 所本文模拟了这个服务, 加深自己的理解. 

        代码运行环境为: Linux 3.10.0-957.21.3.el7.x86_64

        代码结构如下:

        其中broad.go文件, 功能主要是监听client的注册(RegisterChan), 注销(UnregisterChan), 以及定时(time.NewTicker)这三个事件并维护一个map, 代码如下:

package broadcast

import (
	"time"
)

//Broad ..
type Broad struct {
	ClientsMap     map[*Client]bool // map
	RegisterChan   chan *Client     // 连接注册
	UnregisterChan chan *Client     // 关闭连接
}

// NewBroad ..
func NewBroad() *Broad {
	return &Broad{
		ClientsMap:     make(map[*Client]bool),
		RegisterChan:   make(chan *Client, 1),
		UnregisterChan: make(chan *Client, 1),
	}
}

// Run ..
func (broad *Broad) Run() {
	defer func() {
		close(broad.RegisterChan)
		close(broad.UnregisterChan)
	}()

	ticker := time.NewTicker(time.Microsecond * 1)
	defer ticker.Stop()

	msg := []byte("HI.")

	for {
		select {
		case client := <-broad.RegisterChan:
			broad.ClientsMap[client] = true

		case client := <-broad.UnregisterChan:
			client.Conn.Close()              //关闭连接
			delete(broad.ClientsMap, client) // 删除元素
			close(client.WriteChan)          // 关闭channel

		case <-ticker.C:
			for client := range broad.ClientsMap {
				client.WriteChan <- &msg
			}
		}
	}
}

        client.go文件, 主要是接收WriteChan的事件, 并往客户端写数据且设置了超时时间, 其中为了模拟我遇到的坑, 增加了一个userGo的选项, 可以自行模拟, 代码如下:

package broadcast

import (
	"log"
	"net"
	"time"
)

// Client ..
type Client struct {
	Broad     *Broad
	Conn      net.Conn
	WriteChan chan *[]byte
}

// WriteMsg ..
func (client *Client) WriteMsg() {

	defer func() {
		// 代码运行到这里的时候, 还没有向Broad协程发送注销channel,
		// ClientsMap里还是有该client. 当写该WriteChan时, 会造成
		// Broad协程阻塞, 无法再监听新的连接. 此时需要开一个新的协
		// 程继续监听WriteChan事件, 直到Broad协程关闭了WriteChan.

		// 可以不使用这段代码, 并把Broad协程里的秒改成毫秒/微秒并断
		// 掉客户端的网络进行尝试.
		if *useGo {
			go func() {
				for {
					select {
					case msg, ok := <-client.WriteChan:
						if !ok || msg == nil {
							log.Println("Is not ok.")
							return
						}
					}
				}
			}()
		}

		// 通知Broad, 此client离线
		client.Broad.UnregisterChan <- client
	}()

	for {
		select {
		case msg, ok := <-client.WriteChan:
			if !ok || msg == nil {
				log.Println("Is not ok or equal nil.")
				return
			}

			if err := client.Conn.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
				log.Println("SetWriteDeadline failed. ErrMsg: " + err.Error())
				return
			}
			if _, err := client.Conn.Write(*msg); err != nil {
				log.Println("Write failed. ErrMsg: " + err.Error())
				return
			}
		}
	}
}

        server.go文件, 功能主要是实现TCP服务, 代码如下:

package broadcast

import (
	"flag"
	"log"
	"net"
)

var useGo = flag.Bool("use_go", false, "是否使用新协程读取WriteChan, 防止阻塞")
var addr = flag.String("addr", "localhost:8080", "TCP addr")

func init() {
	flag.Parse()
}

// Start ..
func Start() {
	broad := NewBroad()
	go broad.Run()

	listener, err := net.Listen("tcp", *addr)
	if err != nil {
		log.Fatal("Listen fail.")
	}
	log.Println("Listen success.")
	defer listener.Close()

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println("Accept failed. ErrMsg: " + err.Error())
			continue
		}
		log.Println("Accept successfully. Remote addr: " + conn.RemoteAddr().String())

		client := &Client{Broad: broad, Conn: conn, WriteChan: make(chan *[]byte)}
		client.Broad.RegisterChan <- client

		go client.WriteMsg()
	}
}

        main.go文件主要是调用这个包, 这里不再展示.

        模拟遇到的坑结果如下:

图片左边的客户端的网络一直都是好的, 而在我断掉了第二个客户端的网络后, Broad协程阻塞在client.WriteChan <- &msg这, 导致第一个客户端的也无法收到数据. 解决办法就是重新开启一个协程接收WriteChan, 直到Broad协程接收到UnregisterChan事件并关闭客户端.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值