2019-01-05 16:06:47 +08:00
package main
import (
2019-01-09 15:27:34 +08:00
"bytes"
2019-01-05 16:06:47 +08:00
"context"
"flag"
"net/http"
"os"
"os/signal"
2019-01-08 17:40:47 +08:00
"strconv"
2019-01-07 14:29:04 +08:00
"strings"
2019-01-05 16:06:47 +08:00
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
uuid "github.com/satori/go.uuid"
2019-01-07 11:45:13 +08:00
kafka "github.com/segmentio/kafka-go"
2019-01-05 16:06:47 +08:00
log "github.com/sirupsen/logrus"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time . Second
// Time allowed to read the next pong message from the peer.
2019-01-09 15:27:34 +08:00
pongWait = 30 * time . Second
2019-01-05 16:06:47 +08:00
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = ( pongWait * 9 ) / 10
// Maximum message size allowed from peer.
2019-01-09 15:27:34 +08:00
maxMessageSize = 512
2019-01-05 16:06:47 +08:00
)
var (
newline = [] byte { '\n' }
2019-01-09 15:27:34 +08:00
space = [] byte { ' ' }
2019-01-05 16:06:47 +08:00
addr = flag . String ( "addr" , ":9000" , "http service address" )
bootstrapServers = flag . String ( "bootstrap-servers" , "localhost:9092" , "kafka bootstrap servers" )
2019-01-09 11:13:47 +08:00
// partition = flag.Int("partition", 0, "kafka tipic partion")
wait = flag . Duration ( "graceful-timeout" , time . Second * 15 , "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m" )
upgrader = websocket . Upgrader {
2019-01-09 15:27:34 +08:00
ReadBufferSize : 2048 ,
WriteBufferSize : 2048 ,
2019-01-05 16:06:47 +08:00
}
2019-01-08 16:14:11 +08:00
hubMap = make ( map [ string ] Hub )
2019-01-09 11:13:47 +08:00
router = mux . NewRouter ()
2019-01-05 16:06:47 +08:00
)
2019-01-09 15:27:34 +08:00
// Hub maintains the kafka reader and set of active clients
// broadcasts messages to the clients.
type Hub struct {
// hun running indicator
running bool
// Kafka topic
topic string
// Kakfa reader partition
partition int
// Kafka start offset, -1 = latest, -2 = ealist
offset int64
// Kafka Reader
reader * kafka . Reader
// Registered clients.
clients map [ * Client ] bool
// Inbound messages from the clients.
broadcast chan [] byte
// Register requests from the clients.
register chan * Client
// Unregister requests from clients.
unregister chan * Client
}
2019-01-05 16:06:47 +08:00
// Client is a middleman between the websocket connection and the hub.
type Client struct {
hub * Hub
// The websocket connection.
conn * websocket . Conn
// Buffered channel of outbound messages.
send chan [] byte
}
2019-01-09 15:27:34 +08:00
func createKafkaReader ( topic string , offset int64 , partition int ) * kafka . Reader {
// Use uuid for kafka group id
2019-01-09 11:13:47 +08:00
group := uuid . NewV4 ()
2019-01-08 10:50:41 +08:00
2019-01-05 16:06:47 +08:00
log . WithFields ( log . Fields {
"topic" : topic ,
"bootstrap servers" : * bootstrapServers ,
2019-01-09 11:13:47 +08:00
"partition" : partition ,
2019-01-08 17:40:47 +08:00
"group" : group ,
"offset" : offset }). Info ( "creating kafka client ... " )
2019-01-05 16:06:47 +08:00
2019-01-07 11:45:13 +08:00
reader := kafka . NewReader ( kafka . ReaderConfig {
2019-01-09 15:27:34 +08:00
Brokers : strings . Split ( * bootstrapServers , "," ), // kafka brokers, split by comma
Topic : topic , // topic to subscribe
Partition : partition , // partition from url
MinBytes : 10e2 , // 1k
MaxBytes : 10e6 , // 10MB
2019-01-07 11:45:13 +08:00
})
2019-01-09 15:27:34 +08:00
// offset from url
// -1 = latest
// -2 = ealiest
2019-01-08 17:40:47 +08:00
reader . SetOffset ( offset )
2019-01-05 16:06:47 +08:00
2019-01-08 16:14:11 +08:00
return reader
}
2019-01-05 16:06:47 +08:00
2019-01-09 15:27:34 +08:00
// startClient keep the client connection running
// response the pong message
func ( c * Client ) startClient () {
log . WithField ( "remote" , c . conn . RemoteAddr ()). Info ( "client started" )
2019-01-08 16:14:11 +08:00
defer func () {
c . hub . unregister <- c
c . conn . Close ()
log . WithField ( "client" , c ). Warn ( "exit" )
}()
2019-01-09 15:27:34 +08:00
// response to pong message
c . conn . SetReadLimit ( maxMessageSize )
2019-01-08 16:14:11 +08:00
c . conn . SetReadDeadline ( time . Now (). Add ( pongWait ))
c . conn . SetPongHandler ( func ( string ) error { c . conn . SetReadDeadline ( time . Now (). Add ( pongWait )); return nil })
2019-01-09 15:27:34 +08:00
for {
_ , message , err := c . conn . ReadMessage ()
if err != nil {
if websocket . IsUnexpectedCloseError ( err , websocket . CloseGoingAway , websocket . CloseAbnormalClosure ) {
log . WithError ( err ). Error ( "read message error" )
2019-01-08 17:40:47 +08:00
}
2019-01-09 15:27:34 +08:00
break
}
message = bytes . TrimSpace ( bytes . Replace ( message , newline , space , - 1 ))
// if client send ping, response pong
if string ( message ) == "ping" {
c . send <- [] byte ( "pong" )
2019-01-05 16:06:47 +08:00
}
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func ( c * Client ) writePump () {
2019-01-09 15:27:34 +08:00
// ping message
2019-01-05 16:06:47 +08:00
ticker := time . NewTicker ( pingPeriod )
defer func () {
ticker . Stop ()
c . conn . Close ()
2019-01-08 17:40:47 +08:00
c . hub . unregister <- c
log . WithField ( "client" , c ). Warn ( "exit" )
2019-01-05 16:06:47 +08:00
}()
2019-01-09 15:27:34 +08:00
2019-01-05 16:06:47 +08:00
for {
select {
case message , ok := <- c . send :
c . conn . SetWriteDeadline ( time . Now (). Add ( writeWait ))
if ! ok {
2019-01-09 15:27:34 +08:00
log . WithField ( "client" , c ). Warn ( "sending close message" )
2019-01-05 16:06:47 +08:00
// The hub closed the channel.
c . conn . WriteMessage ( websocket . CloseMessage , [] byte {})
return
}
w , err := c . conn . NextWriter ( websocket . TextMessage )
if err != nil {
2019-01-09 15:27:34 +08:00
log . WithError ( err ). Error ( "can't get next wirter" )
2019-01-05 16:06:47 +08:00
return
}
w . Write ( message )
// Add queued chat messages to the current websocket message.
n := len ( c . send )
for i := 0 ; i < n ; i ++ {
w . Write ( newline )
w . Write ( <- c . send )
}
if err := w . Close (); err != nil {
2019-01-09 15:27:34 +08:00
log . WithError ( err ). Error ( "error in close writer" )
2019-01-05 16:06:47 +08:00
return
}
case <- ticker . C :
c . conn . SetWriteDeadline ( time . Now (). Add ( writeWait ))
if err := c . conn . WriteMessage ( websocket . PingMessage , nil ); err != nil {
2019-01-09 15:27:34 +08:00
log . WithError ( err ). Error ( "can't write ping message" )
2019-01-05 16:06:47 +08:00
return
}
}
}
}
// serveWs handles websocket requests from the peer.
2019-01-09 11:13:47 +08:00
func websocketHandler ( w http . ResponseWriter , r * http . Request ) {
2019-01-05 16:06:47 +08:00
vars := mux . Vars ( r )
topic := vars [ "topic" ]
2019-01-09 11:13:47 +08:00
partitionString := vars [ "partition" ]
partition , _ := strconv . Atoi ( partitionString )
offsetString := r . FormValue ( "offset" )
2019-01-08 17:40:47 +08:00
offset , _ := strconv . ParseInt ( offsetString , 10 , 64 )
2019-01-05 16:06:47 +08:00
conn , err := upgrader . Upgrade ( w , r , nil )
if err != nil {
log . Error ( "upgrade connection failed " , err )
return
}
2019-01-09 11:13:47 +08:00
key := topic + "|" + partitionString + "|" + offsetString
2019-01-08 17:40:47 +08:00
h , ok := hubMap [ key ]
2019-01-08 16:14:11 +08:00
if ! ok {
2019-01-09 11:13:47 +08:00
log . WithFields (
log . Fields { "topic" : topic ,
"offset" : offset ,
"partition" : partition }). Info ( "create new hub " )
h = * newHub ( topic , offset , partition )
2019-01-08 16:14:11 +08:00
go h . run ()
2019-01-08 17:40:47 +08:00
hubMap [ key ] = h
2019-01-08 16:14:11 +08:00
} else {
log . Info ( "join hub" )
}
client := & Client { hub : & h , conn : conn , send : make ( chan [] byte , 256 )}
2019-01-05 16:06:47 +08:00
client . hub . register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client . writePump ()
2019-01-09 15:27:34 +08:00
go client . startClient ()
2019-01-05 16:06:47 +08:00
}
2019-01-09 11:13:47 +08:00
func newHub ( topic string , offset int64 , partition int ) * Hub {
2019-01-05 16:06:47 +08:00
return & Hub {
2019-01-08 17:40:47 +08:00
running : true ,
topic : topic ,
2019-01-09 11:13:47 +08:00
partition : partition ,
2019-01-08 17:40:47 +08:00
offset : offset ,
2019-01-09 15:27:34 +08:00
reader : createKafkaReader ( topic , offset , partition ),
2019-01-05 16:06:47 +08:00
broadcast : make ( chan [] byte ),
register : make ( chan * Client ),
unregister : make ( chan * Client ),
clients : make ( map [ * Client ] bool ),
}
}
2019-01-09 15:27:34 +08:00
// Read message from kafka broker and broadcast to the clients
func ( h * Hub ) readKafka () {
2019-01-08 17:40:47 +08:00
log . WithField ( "hub" , * h ). Info ( "start to read" )
for h . running == true {
if h . reader == nil {
2019-01-08 17:56:05 +08:00
log . Info ( "waiting ... " )
time . Sleep ( writeWait )
2019-01-09 15:27:34 +08:00
h . reader = createKafkaReader ( h . topic , h . offset , h . partition )
2019-01-08 17:40:47 +08:00
}
2019-01-08 16:14:11 +08:00
m , err := h . reader . ReadMessage ( context . Background ())
if err != nil {
2019-01-09 15:27:34 +08:00
log . WithFields ( log . Fields { "error" : err }). Error ( "websocket read" )
2019-01-08 17:40:47 +08:00
h . reader . Close ()
h . reader = nil
} else {
log . WithFields ( log . Fields { "offset" : m . Offset , "message" : string ( m . Value )}). Info ( "got message" )
h . broadcast <- m . Value
2019-01-08 16:14:11 +08:00
}
2019-01-08 17:40:47 +08:00
2019-01-08 16:14:11 +08:00
}
}
2019-01-05 16:06:47 +08:00
2019-01-08 17:40:47 +08:00
func ( h * Hub ) getKey () string {
2019-01-09 11:13:47 +08:00
return h . topic + "|" + strconv . Itoa ( h . partition ) + "|" + strconv . FormatInt ( h . offset , 10 )
2019-01-08 17:40:47 +08:00
}
2019-01-05 16:06:47 +08:00
func ( h * Hub ) run () {
2019-01-09 15:27:34 +08:00
go h . readKafka ()
2019-01-08 17:40:47 +08:00
for h . running == true {
2019-01-05 16:06:47 +08:00
select {
case client := <- h . register :
h . clients [ client ] = true
case client := <- h . unregister :
if _ , ok := h . clients [ client ]; ok {
delete ( h . clients , client )
close ( client . send )
2019-01-08 17:40:47 +08:00
log . WithField ( "client" , client ). Warn ( "delete client" )
2019-01-09 15:27:34 +08:00
// Check if
2019-01-08 17:40:47 +08:00
if len ( h . clients ) < 1 {
log . Warn ( "all clients left" )
h . running = false
2019-01-09 15:27:34 +08:00
// h.reader.Close()
2019-01-08 17:40:47 +08:00
delete ( hubMap , h . getKey ())
2019-01-09 15:27:34 +08:00
log . WithField ( "hub" , h ). Info ( "remove hub" )
2019-01-08 17:40:47 +08:00
}
2019-01-05 16:06:47 +08:00
}
case message := <- h . broadcast :
for client := range h . clients {
select {
case client . send <- message :
default :
close ( client . send )
delete ( h . clients , client )
}
}
}
}
2019-01-09 15:27:34 +08:00
log . WithField ( "reader" , h . reader ). Warn ( "closing kafka reader " )
h . reader . Close ()
log . WithField ( "hub" , h ). Warn ( "closed" )
}
func destroy () {
log . Info ( " terminating " )
// close all hub
for key , hub := range hubMap {
log . WithField ( "key" , key ). Warn ( "closing hub" )
hub . running = false
// hub.reader.Close()
}
log . Fatal ( " terminated " )
2019-01-05 16:06:47 +08:00
}
func main () {
flag . Parse ()
2019-01-09 11:13:47 +08:00
loggedRouter := handlers . LoggingHandler ( os . Stdout , router )
2019-01-05 16:06:47 +08:00
srv := & http . Server {
Addr : * addr ,
// Good practice to set timeouts to avoid Slowloris attacks.
WriteTimeout : time . Second * 15 ,
ReadTimeout : time . Second * 15 ,
IdleTimeout : time . Second * 60 ,
Handler : loggedRouter , // Pass our instance of gorilla/mux in.
}
2019-01-09 11:13:47 +08:00
router . Path ( "/ws/{topic}/{partition}" ). Queries ( "offset" , "{offset}" ). HandlerFunc ( websocketHandler ). Name ( "web-socket" )
2019-01-05 16:06:47 +08:00
// Run our server in a goroutine so that it doesn't block.
go func () {
2019-01-05 16:13:16 +08:00
log . WithFields ( log . Fields { "address" : * addr }). Info ( "starting server" )
2019-01-05 16:06:47 +08:00
if err := srv . ListenAndServe (); err != nil {
2019-01-08 16:14:11 +08:00
log . WithFields ( log . Fields { "error" : err }). Error ( "error" )
2019-01-05 16:06:47 +08:00
}
}()
c := make ( chan os . Signal , 1 )
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal . Notify ( c , os . Interrupt )
// Block until we receive our signal.
<- c
// Create a deadline to wait for.
2019-01-07 11:54:11 +08:00
ctx , cancel := context . WithTimeout ( context . Background (), * wait )
2019-01-05 16:06:47 +08:00
defer cancel ()
// Doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
2019-01-09 15:27:34 +08:00
destroy ()
2019-01-05 16:06:47 +08:00
srv . Shutdown ( ctx )
// Optionally, you could run srv.Shutdown in a goroutine and block on
// <-ctx.Done() if your application should wait for other services
// to finalize based on context cancellation.
log . Info ( "shutting down" )
os . Exit ( 0 )
}