├── .gitignore ├── LICENSE ├── README.md ├── cleaner.go ├── client.go ├── config.go ├── config ├── config.ini └── log4go.xml ├── errors.go ├── example ├── client │ ├── chat.html │ ├── room.html │ ├── room_list.html │ └── static │ │ └── video │ │ └── iphonex.mp4 └── serv │ └── main.go ├── kafka.go ├── log.go ├── message_handler.go ├── proto.go ├── room.go ├── static_server.go ├── utils └── concurrent_list.go └── ws_server.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 JWL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # danmu 2 | 3 | 轻量弹幕(IM)系统,基于GO 4 | 5 | # Require 6 | 7 | `GO` >= `1.8` 8 | 9 | `KAFKA` >= `1.0` 10 | 11 | # Demo 12 | 13 | 本项目为你提供了一个在线demo:[demo](http://danmu.jwlchina.cn) 14 | 15 | # Getting Start 16 | 17 | 安装好 `GOLANG` 环境以及 `KAFKA` 18 | 19 | 获取项目源码 20 | ```bash 21 | go get github.com/kong36088/danmu 22 | ``` 23 | 24 | 到项目`config`目录下配置好你的kafka地址以及服务监听地址 25 | 26 | 为 `kafka` 创建一个topic `danmu` 27 | ```bash 28 | kafka-topics.sh --create --topic danmu --zookeeper localhost:2181 --replication-factor 1 --partitions 1 29 | ``` 30 | 31 | 本项目为提供了一个使用实例,在完成环境部署后,运行以下命令: 32 | ```bash 33 | $ cd /path/to/your/application 34 | 35 | $ cd example/serv/ 36 | 37 | $ go build # or go run main.go 38 | 39 | $ ./serv # run your application 40 | ``` 41 | 42 | 服务开启之后,用浏览器访问打开html文件 `example/client/room_list.html` 即可看到服务效果 -------------------------------------------------------------------------------- /cleaner.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | type Cleaner struct { 4 | } 5 | 6 | var ( 7 | cleaner *Cleaner 8 | ) 9 | 10 | func InitCleaner() error{ 11 | cleaner = new(Cleaner) 12 | 13 | return OK 14 | } 15 | 16 | /** 17 | 清除保存的client信息,关闭连接 18 | */ 19 | func (cleaner *Cleaner) CleanClient(client *Client) { 20 | client.Close() 21 | 22 | room, err := roomBucket.Get(client.RoomId) 23 | if err == nil { 24 | delete(room.clients, client.Conn) 25 | } 26 | 27 | clientBucket.Remove(client.Conn) 28 | 29 | client = nil //for gc 30 | } 31 | 32 | //TODO CleanRoom 33 | func (cleaner *Cleaner) CleanRoom(room *Room){ 34 | 35 | } 36 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "encoding/json" 5 | log "github.com/alecthomas/log4go" 6 | "github.com/gorilla/websocket" 7 | "strconv" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type ( 13 | cid int64 14 | ) 15 | 16 | var ( 17 | clientBucket *ClientBucket 18 | keepalive int 19 | ) 20 | 21 | type Client struct { 22 | ClientId cid //remain 23 | RoomId rid 24 | Conn *websocket.Conn 25 | } 26 | 27 | func NewClient(roomId rid, conn *websocket.Conn) *Client { 28 | c := new(Client) 29 | c.RoomId = roomId 30 | c.Conn = conn 31 | return c 32 | } 33 | 34 | func (client *Client) ReadJSON(v interface{}) error { 35 | err := client.Conn.ReadJSON(v) 36 | return err 37 | } 38 | 39 | func (client *Client) Read(v interface{}) (int, []byte, error) { 40 | msgType, msg, err := client.Conn.ReadMessage() 41 | return msgType, msg, err 42 | } 43 | 44 | func (client *Client) WriteErrorMsg(msg string) error { 45 | err := client.Conn.WriteJSON(Proto{ 46 | OP: OPErr, 47 | Message: msg, 48 | RoomId: -1, 49 | }) 50 | return err 51 | } 52 | 53 | func (client *Client) WriteMessage(msg string, roomId rid) error { 54 | err := client.Conn.WriteJSON(Proto{ 55 | OP: OPMsg, 56 | Message: msg, 57 | RoomId: roomId, 58 | }) 59 | return err 60 | } 61 | 62 | func (client *Client) WriteControl(messageType int, data []byte, deadline time.Time) error { 63 | return client.Conn.WriteControl(messageType, data, deadline) 64 | } 65 | 66 | func (client *Client) Write(proto *Proto) error { 67 | err := client.Conn.WriteJSON(proto) 68 | return err 69 | } 70 | 71 | func (client *Client) BatchWrite(proto []*Proto) error { 72 | jsonProtos, err := json.Marshal(proto) 73 | if err != nil { 74 | return err 75 | } 76 | err = client.Conn.WriteMessage(websocket.TextMessage, jsonProtos) 77 | return err 78 | } 79 | 80 | func (client *Client) Close() error { 81 | return client.Conn.Close() 82 | } 83 | 84 | func (client *Client) ErrorReport(err error, msg string) error { 85 | if msg != "" { 86 | return client.WriteErrorMsg(msg) 87 | } 88 | return OK 89 | } 90 | 91 | //keepAlive send ping message to client 92 | // should be called by goroutines 93 | func (client *Client) keepAlive() { 94 | if keepalive <= 0 { 95 | return 96 | } 97 | timeout := time.Duration(keepalive) * time.Second 98 | 99 | lastResponse := time.Now() 100 | client.Conn.SetPongHandler(func(msg string) error { 101 | lastResponse = time.Now() 102 | 103 | return nil 104 | }) 105 | go func() { 106 | for { 107 | err := client.WriteControl(websocket.PingMessage, []byte("keepalive"), time.Now().Add(writeWait)) 108 | if err != nil { 109 | return 110 | } 111 | time.Sleep(timeout / 2) 112 | if time.Now().Sub(lastResponse) > timeout { 113 | log.Info("Ping pong timeout, close client", client) 114 | cleaner.CleanClient(client) 115 | return 116 | } 117 | } 118 | }() 119 | } 120 | func (client *Client) CloseHandler() func(code int, text string) { 121 | return func(code int, text string) { 122 | message := []byte{} 123 | if code != websocket.CloseNoStatusReceived { 124 | message = websocket.FormatCloseMessage(code, "") 125 | } 126 | client.WriteControl(websocket.CloseMessage, message, time.Now().Add(writeWait)) 127 | cleaner.CleanClient(client) 128 | } 129 | } 130 | 131 | type ClientBucket struct { 132 | Clients map[*websocket.Conn]*Client 133 | lck sync.RWMutex 134 | } 135 | 136 | func InitClientBucket() error { 137 | var ( 138 | err error 139 | ) 140 | clientBucket = &ClientBucket{ 141 | Clients: make(map[*websocket.Conn]*Client), 142 | lck: sync.RWMutex{}, 143 | } 144 | 145 | keepalive, err = strconv.Atoi(Conf.GetConfig("sys", "keepalive_timeout")) 146 | if err != nil { 147 | return err 148 | } 149 | return OK 150 | } 151 | 152 | func (cb *ClientBucket) Get(conn *websocket.Conn) (*Client, error) { 153 | cb.lck.RLock() 154 | defer cb.lck.RUnlock() 155 | if v, ok := cb.Clients[conn]; ok { 156 | return v, OK 157 | } else { 158 | return nil, ErrRoomDoesNotExist 159 | } 160 | 161 | } 162 | 163 | func (cb *ClientBucket) Add(client *Client) error { 164 | cb.lck.Lock() 165 | cb.Clients[client.Conn] = client 166 | cb.lck.Unlock() 167 | return OK 168 | } 169 | 170 | func (cb *ClientBucket) Remove(conn *websocket.Conn) error { 171 | if _, ok := cb.Clients[conn]; !ok { 172 | return ErrRoomDoesNotExist 173 | } 174 | cb.lck.Lock() 175 | delete(cb.Clients, conn) 176 | cb.lck.Unlock() 177 | return OK 178 | } 179 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | log "github.com/alecthomas/log4go" 8 | "github.com/larspensjo/config" 9 | "os" 10 | "runtime" 11 | ) 12 | 13 | var ( 14 | Conf *Config 15 | appPath = os.Getenv("GOPATH") + "/src/github.com/kong36088/danmu/" 16 | configFile = flag.String("config", appPath+"config/config.ini", "General configuration file") 17 | ) 18 | 19 | type Config struct { 20 | values map[string]map[string]string 21 | } 22 | 23 | //topic list 24 | 25 | func NewConfig() *Config { 26 | return &Config{ 27 | values: make(map[string]map[string]string), 28 | } 29 | } 30 | 31 | func InitConfig() error { 32 | Conf = NewConfig() 33 | 34 | runtime.GOMAXPROCS(runtime.NumCPU()) 35 | flag.Parse() 36 | 37 | cfgSecs, err := config.ReadDefault(*configFile) 38 | if err != nil { 39 | return errors.New(fmt.Sprintf("Fail to find %s %s", *configFile, err)) 40 | } 41 | 42 | for _, section := range cfgSecs.Sections() { 43 | options, err := cfgSecs.SectionOptions(section) 44 | if err != nil { 45 | log.Error("Read options of file %s section %s failed, %s\n", *configFile, section, err) 46 | continue 47 | } 48 | Conf.values[section] = make(map[string]string) 49 | for _, v := range options { 50 | option, err := cfgSecs.String(section, v) 51 | if err != nil { 52 | log.Error("Read file %s option %s failed, %s\n", *configFile, v, err) 53 | continue 54 | } 55 | Conf.values[section][v] = option 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func (c *Config) GetConfig(section, option string) string { 62 | return c.values[section][option] 63 | } 64 | 65 | func (c *Config) GetSectionConfig(section string) map[string]string { 66 | return c.values[section] 67 | } 68 | 69 | func (c *Config) GetAllConfig() map[string]map[string]string { 70 | return c.values 71 | } 72 | -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [sys] 2 | port=9500 3 | keepalive_timeout=60 #心跳检测超时时间,设置为0则不进行检测 4 | log_conf=./config/log4go.xml 5 | push_freq=1 #弹幕推送时延,默认1秒推送一次 6 | 7 | [kafka] 8 | address=localhost:9092 9 | group=danmu_c 10 | topic=danmu -------------------------------------------------------------------------------- /config/log4go.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | stdout 4 | console 5 | 6 | DEBUG 7 | 8 | 9 | debug_file 10 | file 11 | DEBUG 12 | /var/log/danmu/danmu_debug.log 13 | [%D %T] [%L] [%S] %M 14 | true 15 | 0M 16 | 0K 17 | true 18 | 19 | 20 | info_file 21 | file 22 | INFO 23 | /var/log/danmu/danmu_info.log 24 | 35 | [%D %T] [%L] [%S] %M 36 | true 37 | 0M 38 | 0K 39 | true 40 | 41 | 42 | warn_file 43 | file 44 | WARNING 45 | /var/log/danmu/danmu_warn.log 46 | [%D %T] [%L] [%S] %M 47 | true 48 | 0M 49 | 0K 50 | true 51 | 52 | 53 | error_file 54 | file 55 | ERROR 56 | /var/log/danmu/danmu_error.log 57 | [%D %T] [%L] [%S] %M 58 | true 59 | 0M 60 | 0K 61 | true 62 | 63 | 64 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import "errors" 4 | 5 | var ( 6 | OK error = nil 7 | //sys 8 | ErrParamError = errors.New("incorrect param") 9 | 10 | //room 11 | ErrRoomDoesNotExist = errors.New("room does not exist") 12 | ) 13 | -------------------------------------------------------------------------------- /example/client/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat Example 5 | 55 | 92 | 93 | 94 |
95 |
96 | 97 | 98 |
99 | 100 | -------------------------------------------------------------------------------- /example/client/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Room 7 | 8 | 98 | 99 | 100 |
101 |
102 | 103 |
104 |
105 | 106 |
发送
107 |
108 |
109 | 110 | 111 | 269 | 270 | -------------------------------------------------------------------------------- /example/client/room_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Room list 6 | 7 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | 房间1 25 |
26 |
27 | 房间2 28 |
29 |
30 | 房间3 31 |
32 |
33 |
34 |
35 | 房间4 36 |
37 |
38 | 房间5 39 |
40 |
41 | 房间6 42 |
43 |
44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /example/client/static/video/iphonex.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kong36088/danmu/40ae9f888d9bfaa7a627b55739a16082934804c7/example/client/static/video/iphonex.mp4 -------------------------------------------------------------------------------- /example/serv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/kong36088/danmu" 5 | ) 6 | 7 | func main() { 8 | danmu.StartServer() 9 | } -------------------------------------------------------------------------------- /kafka.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "github.com/Shopify/sarama" 5 | log "github.com/alecthomas/log4go" 6 | "github.com/bsm/sarama-cluster" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var ( 12 | Topic string 13 | Topics []string 14 | Group string 15 | producer sarama.AsyncProducer 16 | consumer *cluster.Consumer 17 | ) 18 | 19 | func InitKafka(kafkaAddrs []string) error { 20 | var ( 21 | err error 22 | ) 23 | Topic = Conf.GetConfig("kafka", "topic") 24 | Topics = strings.Split(Topic, ",") 25 | Group = Conf.GetConfig("kafka", "group") 26 | 27 | err = InitKafkaProducer(kafkaAddrs) 28 | if err != nil { 29 | return err 30 | } 31 | err = InitKafkaConsumer(kafkaAddrs) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func CloseKafka() { 40 | producer.Close() 41 | consumer.Close() 42 | } 43 | 44 | // producer 45 | 46 | func InitKafkaProducer(kafkaAddrs []string) error { 47 | var ( 48 | err error 49 | ) 50 | config := sarama.NewConfig() 51 | //config.Producer.RequiredAcks = sarama.NoResponse 52 | config.Producer.RequiredAcks = sarama.WaitForLocal 53 | config.Producer.Partitioner = sarama.NewHashPartitioner 54 | config.Producer.Return.Successes = true 55 | config.Producer.Return.Errors = true 56 | config.Producer.Timeout = 5 * time.Second 57 | producer, err = sarama.NewAsyncProducer(kafkaAddrs, config) 58 | if err != nil{ 59 | return err 60 | } 61 | go handleProducerSuccess() 62 | go handleProducerError() 63 | return OK 64 | } 65 | 66 | func handleProducerSuccess() { 67 | var ( 68 | pm *sarama.ProducerMessage 69 | ) 70 | for { 71 | pm = <-producer.Successes() 72 | if pm != nil { 73 | log.Info("producer message success, partition:%d offset:%d key:%v valus:%s", pm.Partition, pm.Offset, pm.Key, pm.Value) 74 | } 75 | } 76 | } 77 | 78 | func handleProducerError() { 79 | var ( 80 | err *sarama.ProducerError 81 | ) 82 | for { 83 | err = <-producer.Errors() 84 | if err != nil { 85 | log.Error("producer message error, partition:%d offset:%d key:%v valus:%s error(%v)", err.Msg.Partition, err.Msg.Offset, err.Msg.Key, err.Msg.Value, err.Err) 86 | } 87 | } 88 | } 89 | 90 | //consumer 91 | 92 | func InitKafkaConsumer(kafkaAddrs []string) error { 93 | var ( 94 | err error 95 | ) 96 | config := cluster.NewConfig() 97 | config.Group.Return.Notifications = false 98 | config.Consumer.Return.Errors = true 99 | config.Consumer.Offsets.CommitInterval = 1 * time.Second 100 | config.Consumer.Offsets.Initial = sarama.OffsetNewest //初始从最新的offset开始 101 | 102 | consumer, err = cluster.NewConsumer(kafkaAddrs, Group, Topics, config) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // consume errors 108 | go handleConsumerError() 109 | 110 | return OK 111 | } 112 | 113 | func handleConsumerError() { 114 | for err := range consumer.Errors() { 115 | log.Error("Consumer Error: %s\n", err.Error()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | log "github.com/alecthomas/log4go" 5 | ) 6 | 7 | func InitLog() error { 8 | logPath := Conf.GetConfig("sys", "log_conf") 9 | log.LoadConfiguration(appPath + logPath) 10 | return OK 11 | } 12 | 13 | func CloseLog() { 14 | log.Close() 15 | } 16 | -------------------------------------------------------------------------------- /message_handler.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "encoding/json" 5 | log "github.com/alecthomas/log4go" 6 | "strconv" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var ( 12 | commandChans map[*Room]chan string 13 | lock *sync.RWMutex 14 | pushFreq int 15 | msgRoomObs RoomObserverInterface 16 | ) 17 | 18 | func InitMessageHandler() error { 19 | var err error 20 | 21 | commandChans = make(map[*Room]chan string) 22 | lock = &sync.RWMutex{} 23 | 24 | pushFreq, err = strconv.Atoi(Conf.GetConfig("sys", "push_freq")) 25 | msgRoomObs = new(MessageRoomObserver) 26 | 27 | roomBucket.AttachObserver(msgRoomObs) 28 | 29 | if err != nil { 30 | return err 31 | } 32 | return OK 33 | } 34 | 35 | type MessageRoomObserver struct{} 36 | 37 | func (mro *MessageRoomObserver) Update(action int, room *Room) { 38 | lock.Lock() 39 | defer lock.Unlock() 40 | 41 | if action == RoomActionAdd { 42 | commandChans[room] = make(chan string) 43 | go messagePusher(room, commandChans[room]) 44 | } else if action == RoomActionDelete { 45 | commandChans[room] <- "stop" 46 | delete(commandChans, room) 47 | } 48 | 49 | } 50 | 51 | func messageHandler() { 52 | var ( 53 | proto *Proto 54 | ) 55 | proto = NewProto() 56 | for { 57 | select { 58 | case msg, ok := <-consumer.Messages(): 59 | if ok { 60 | //fmt.Printf("%s/%d/%d\t%s\t%s\n", msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value) 61 | consumer.MarkOffset(msg, "") // mark message as processed 62 | 63 | if err := json.Unmarshal(msg.Value, proto); err != nil { 64 | log.Error(err) 65 | continue 66 | } 67 | 68 | roomId := proto.RoomId 69 | room, err := roomBucket.Get(rid(roomId)) 70 | if err != nil { 71 | log.Error(err) 72 | continue 73 | } 74 | room.protoList.PushBack(proto) 75 | log.Debug(proto) 76 | } 77 | } 78 | } 79 | } 80 | 81 | func messagePusher(room *Room, commandChan chan string) { 82 | ticker := time.NewTicker(time.Duration(pushFreq) * time.Second) // 推送定时器 83 | for { 84 | select { 85 | case command := <-commandChan: 86 | if command == "stop" { 87 | return 88 | } 89 | case <- ticker.C: 90 | datas := room.protoList.PopAll() 91 | protoLen := len(datas) 92 | if protoLen > 0 { 93 | protos := make([]*Proto, 0, protoLen) 94 | for i := 0; i < protoLen; i++ { 95 | proto, err := datas[i].(*Proto) 96 | if err == false { 97 | log.Error("*Proto type assertion failed") 98 | continue 99 | } 100 | protos = append(protos, proto) 101 | } 102 | for _, client := range room.GetClients() { 103 | client.BatchWrite(protos) 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "encoding/json" 5 | log "github.com/alecthomas/log4go" 6 | ) 7 | 8 | const ( 9 | OPErr = "error" 10 | OPMsg = "message" 11 | ) 12 | 13 | type Proto struct { 14 | OP string `json:"op"` 15 | Message string `json:"message"` 16 | RoomId rid `json:"room_id"` 17 | } 18 | 19 | func NewProto() *Proto{ 20 | return &Proto{} 21 | } 22 | 23 | func (p Proto) String() string { 24 | return p.JsonEncode() 25 | } 26 | 27 | func (p *Proto) JsonEncode() string { 28 | j, err := json.Marshal(p) 29 | if err != nil { 30 | log.Error(err) 31 | return "" 32 | } 33 | return string(j) 34 | } 35 | -------------------------------------------------------------------------------- /room.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/alecthomas/log4go" 6 | "github.com/gorilla/websocket" 7 | "github.com/kong36088/danmu/utils" 8 | "strconv" 9 | "sync" 10 | ) 11 | 12 | const ( 13 | roomMapCup = 100 14 | ) 15 | 16 | type ( 17 | rid int64 18 | ) 19 | 20 | //TODO room auto create 21 | func (r rid) String() string { 22 | return strconv.Itoa(int(r)) 23 | } 24 | 25 | var ( 26 | roomList = []rid{1, 2, 3, 4, 5, 6, 7, 8} 27 | roomBucket *RoomBucket 28 | ) 29 | 30 | type Room struct { 31 | RoomId rid 32 | clients map[*websocket.Conn]*Client 33 | lck sync.RWMutex 34 | protoList *utils.ConcurrentList //Batch push 35 | } 36 | 37 | func NewRoom(roomId rid) *Room { 38 | r := new(Room) 39 | r.RoomId = roomId 40 | r.clients = make(map[*websocket.Conn]*Client) 41 | r.protoList = utils.NewConcurrentList() 42 | return r 43 | } 44 | 45 | func (room *Room) AddClient(client *Client) error { 46 | room.lck.Lock() 47 | room.clients[client.Conn] = client 48 | room.lck.Unlock() 49 | 50 | return nil 51 | } 52 | 53 | func (room *Room) GetClients() map[*websocket.Conn]*Client { 54 | room.lck.RLock() 55 | m := room.clients 56 | room.lck.RUnlock() 57 | 58 | return m 59 | } 60 | 61 | func (room Room) String() string { 62 | return fmt.Sprintf("RoomId:%s", room.RoomId) 63 | } 64 | 65 | type RoomBucket struct { 66 | Rooms map[rid]*Room 67 | lck sync.RWMutex 68 | observers map[interface{}]interface{} 69 | } 70 | 71 | func InitRoomBucket() error { 72 | roomBucket = &RoomBucket{ 73 | Rooms: make(map[rid]*Room, roomMapCup), 74 | lck: sync.RWMutex{}, 75 | observers: make(map[interface{}]interface{}), 76 | } 77 | for _, v := range roomList { 78 | r := NewRoom(v) 79 | err := roomBucket.Add(r) 80 | if err != nil { 81 | log.Exit(err) 82 | } 83 | } 84 | return OK 85 | } 86 | 87 | func (rb *RoomBucket) Get(id rid) (*Room, error) { 88 | rb.lck.RLock() 89 | defer rb.lck.RUnlock() 90 | if v, ok := rb.Rooms[id]; ok { 91 | return v, nil 92 | } else { 93 | return nil, ErrRoomDoesNotExist 94 | } 95 | 96 | } 97 | 98 | func (rb *RoomBucket) Add(room *Room) error { 99 | rb.lck.Lock() 100 | rb.Rooms[room.RoomId] = room 101 | rb.lck.Unlock() 102 | 103 | rb.notify(RoomActionAdd, room) 104 | 105 | return OK 106 | } 107 | 108 | func (rb *RoomBucket) Remove(room *Room) error { 109 | if _, ok := rb.Rooms[room.RoomId]; !ok { 110 | return ErrRoomDoesNotExist 111 | } 112 | rb.lck.Lock() 113 | delete(rb.Rooms, room.RoomId) 114 | rb.lck.Unlock() 115 | 116 | rb.notify(RoomActionDelete, room) 117 | 118 | return OK 119 | } 120 | 121 | func (rb *RoomBucket) AttachObserver(observer RoomObserverInterface) { 122 | rb.lck.Lock() 123 | rb.observers[observer] = observer 124 | 125 | for _, room := range rb.Rooms { 126 | observer.Update(RoomActionAdd, room) 127 | } 128 | 129 | rb.lck.Unlock() 130 | } 131 | 132 | func (rb *RoomBucket) DetachObserver(observer RoomObserverInterface) { 133 | rb.lck.Lock() 134 | delete(rb.observers, observer) 135 | 136 | for _, room := range rb.Rooms { 137 | observer.Update(RoomActionDelete, room) 138 | } 139 | 140 | rb.lck.Unlock() 141 | } 142 | 143 | func (rb *RoomBucket) notify(action int, room *Room) { 144 | rb.lck.RLock() 145 | defer rb.lck.RUnlock() 146 | 147 | for _, ob := range rb.observers { 148 | ob.(RoomObserverInterface).Update(action, room) 149 | } 150 | } 151 | 152 | const ( 153 | RoomActionAdd = 1 154 | RoomActionDelete = 2 155 | ) 156 | 157 | type RoomObserverInterface interface { 158 | Update(int, *Room) 159 | } 160 | -------------------------------------------------------------------------------- /static_server.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "html/template" 9 | "net/http" 10 | ) 11 | 12 | 13 | func StaticHandler(res http.ResponseWriter, req *http.Request) { 14 | t, err := template.ParseFiles("I:\\ubuntu14.04\\share\\docker\\go\\www\\src\\github.com\\kong36088\\danmu\\client\\index.html") 15 | if err != nil { 16 | fmt.Println(err) 17 | return 18 | } 19 | err = WriteTemplateToHttpResponse(res, t) 20 | if err != nil { 21 | fmt.Println(err) 22 | return 23 | } 24 | } 25 | 26 | func WriteTemplateToHttpResponse(res http.ResponseWriter, t *template.Template) error { 27 | if t == nil || res == nil { 28 | return errors.New("WriteTemplateToHttpResponse: t must not be nil.") 29 | } 30 | var buf bytes.Buffer 31 | err := t.Execute(&buf, nil) 32 | if err != nil { 33 | return err 34 | } 35 | res.Header().Set("Content-Type", "text/html; charset=utf-8") 36 | _, err = res.Write(buf.Bytes()) 37 | return err 38 | } -------------------------------------------------------------------------------- /utils/concurrent_list.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | ) 7 | 8 | type ConcurrentList struct { 9 | lock *sync.RWMutex 10 | list *list.List 11 | } 12 | 13 | func NewConcurrentList() *ConcurrentList { 14 | return &ConcurrentList{ 15 | lock: &sync.RWMutex{}, 16 | list: list.New(), 17 | } 18 | } 19 | 20 | func (cl *ConcurrentList) PushBack(v interface{}) *list.Element { 21 | cl.lock.Lock() 22 | 23 | ele := cl.list.PushBack(v) 24 | 25 | cl.lock.Unlock() 26 | 27 | return ele 28 | } 29 | 30 | func (cl *ConcurrentList) Pop() interface{} { 31 | cl.lock.Lock() 32 | 33 | value := cl.list.Remove(cl.list.Back()) 34 | 35 | cl.lock.Unlock() 36 | 37 | return value 38 | } 39 | 40 | func (cl *ConcurrentList) PopAll() []interface{} { 41 | datas := make([]interface{}, 0, cl.Len()) 42 | 43 | if cap(datas) > 0 { 44 | cl.lock.Lock() 45 | 46 | for i := 0; i < cap(datas); i++ { 47 | datas = append(datas, cl.list.Remove(cl.list.Front())) 48 | } 49 | cl.lock.Unlock() 50 | 51 | return datas 52 | } else { 53 | return nil 54 | } 55 | 56 | } 57 | 58 | func (cl *ConcurrentList) Back() *list.Element { 59 | cl.lock.RLock() 60 | 61 | ele := cl.list.Back() 62 | 63 | cl.lock.RUnlock() 64 | 65 | return ele 66 | } 67 | 68 | func (cl *ConcurrentList) Front() *list.Element { 69 | cl.lock.RLock() 70 | 71 | ele := cl.list.Front() 72 | 73 | cl.lock.RUnlock() 74 | 75 | return ele 76 | } 77 | 78 | func (cl *ConcurrentList) Len() int { 79 | cl.lock.RLock() 80 | 81 | length := cl.list.Len() 82 | 83 | cl.lock.RUnlock() 84 | 85 | return length 86 | } 87 | -------------------------------------------------------------------------------- /ws_server.go: -------------------------------------------------------------------------------- 1 | package danmu 2 | 3 | import ( 4 | "github.com/Shopify/sarama" 5 | log "github.com/alecthomas/log4go" 6 | "github.com/gorilla/websocket" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | writeWait = time.Second 15 | roomIdFiled = "room" 16 | ) 17 | 18 | //TODO 在线人数 19 | var 20 | ( 21 | upgrader = websocket.Upgrader{ 22 | ReadBufferSize: 1024, 23 | WriteBufferSize: 1024, 24 | CheckOrigin: func(r *http.Request) bool { 25 | return true 26 | }, // 解决域不一致的问题 27 | } // 将http升级为websocket 28 | ) 29 | 30 | func onConnect(w http.ResponseWriter, r *http.Request) { 31 | conn, err := upgrader.Upgrade(w, r, nil) 32 | if err != nil { 33 | log.Error(err) 34 | return 35 | } 36 | client := NewClient(0, conn) 37 | roomId := r.FormValue(roomIdFiled) 38 | roomIdi, err := strconv.Atoi(roomId) 39 | if err != nil { 40 | client.WriteErrorMsg("incorrect roomId.") 41 | cleaner.CleanClient(client) 42 | log.Error("Parse roomid failed, roomid: %s, err: %s\n", roomId, err) 43 | return 44 | } 45 | room, err := roomBucket.Get(rid(roomIdi)) 46 | if err != nil { 47 | client.WriteErrorMsg("Room does not exist.") 48 | cleaner.CleanClient(client) 49 | log.Error("Room does not exist, roomid: %s, err: %s\n", roomId, err) 50 | return 51 | } 52 | client.RoomId = rid(roomIdi) 53 | room.AddClient(client) 54 | 55 | clientBucket.Add(client) 56 | 57 | //send 58 | go listen(client) 59 | go client.keepAlive() 60 | 61 | } 62 | 63 | // listen message that receive from client 64 | // should be called by goroutines 65 | func listen(client *Client) { 66 | defer cleaner.CleanClient(client) 67 | for { 68 | proto := Proto{} 69 | err := client.ReadJSON(&proto) 70 | if err != nil { 71 | if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { 72 | return 73 | } else { 74 | log.Error(err) 75 | return 76 | } 77 | } 78 | log.Debug(proto) 79 | msg := &sarama.ProducerMessage{ 80 | Topic: Topic, 81 | Value: sarama.ByteEncoder(proto.JsonEncode()), 82 | } 83 | producer.Input() <- msg 84 | } 85 | } 86 | 87 | func StartServer() { 88 | var ( 89 | err error 90 | ) 91 | if err = InitConfig(); err != nil { 92 | panic(err) 93 | } 94 | 95 | if err = InitLog(); err != nil { 96 | panic(err) 97 | } 98 | defer CloseLog() 99 | log.Info("danmu server start") 100 | 101 | kafkaAddrs := Conf.GetConfig("kafka", "address") 102 | kafkaAddr := strings.Split(kafkaAddrs, ",") 103 | 104 | if err = InitKafka(kafkaAddr); err != nil { 105 | log.Exit(err) 106 | } 107 | defer CloseKafka() 108 | 109 | if err = InitRoomBucket(); err != nil { 110 | log.Exit(err) 111 | } 112 | 113 | if err = InitClientBucket(); err != nil { 114 | log.Exit(err) 115 | } 116 | 117 | if err = InitCleaner(); err != nil { 118 | log.Exit(err) 119 | } 120 | 121 | // http.HandleFunc("/", StaticHandler) 122 | http.HandleFunc("/ws", onConnect) 123 | 124 | if err = InitMessageHandler(); err != nil { 125 | log.Exit(err) 126 | } 127 | go messageHandler() 128 | 129 | addr := ":" + Conf.GetConfig("sys", "port") 130 | err = http.ListenAndServe(addr, nil) 131 | if err != nil { 132 | log.Exit(err) 133 | } 134 | } 135 | --------------------------------------------------------------------------------