Created
February 1, 2023 15:57
-
-
Save SubCoder1/3a700149b2e7bb179a9123c6283030ff to your computer and use it in GitHub Desktop.
Server Side Event in Go using Gin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Server Sent Event</title> | |
</head> | |
<body> | |
<div class="event-data" id="data"></div> | |
</body> | |
<script> | |
// EventSource object of javascript listens the streaming events from our go server and prints the message. | |
var stream = new EventSource("/stream"); | |
stream.onopen = function (event) { | |
console.log(event); | |
}; | |
stream.onmessage = function (event) { | |
console.log(JSON.parse(event.data)); | |
document.getElementById("data").innerText = event.data; | |
}; | |
stream.onerror = function (event) { | |
console.log(event); | |
stream.close(); | |
}; | |
</script> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a Server Side Event (SSE) Example using Gin Framework. | |
// In this example, data is sent to the first client on subsequent successful connections. | |
// After running this program, open up 2 or more tabs of localhost:8085, | |
// Watch a message getting sent to the first tab (first client). | |
package main | |
import ( | |
"io" | |
"log" | |
"net/http" | |
"github.com/gin-gonic/gin" | |
) | |
// Data to be broadcasted to a client. | |
type Data struct { | |
Message string `json:"message"` | |
From int `json:"sender"` | |
To int `json:"receiver"` | |
} | |
// Uniquely defines an incoming client. | |
type Client struct { | |
// Unique Client ID | |
ID int | |
// Client channel | |
Channel chan Data | |
} | |
// Global ID variable. | |
// Increments or decrements on every incoming or outgoing client connections. | |
var ID int = 0 | |
// Keeps track of every SSE events. | |
type Event struct { | |
// Data are pushed to this channel | |
Message chan Data | |
// New client connections | |
NewClients chan Client | |
// Closed client connections | |
ClosedClients chan Client | |
// Total client connections | |
TotalClients map[int]chan Data | |
} | |
// Initializes Event and starts the event listener | |
func NewEvent() (event *Event) { | |
event = &Event{ | |
Message: make(chan Data), | |
NewClients: make(chan Client), | |
ClosedClients: make(chan Client), | |
TotalClients: make(map[int]chan Data), | |
} | |
go event.listen() | |
return | |
} | |
// It Listens all incoming requests from clients. | |
// Handles addition and removal of clients and broadcast messages to clients. | |
func (stream *Event) listen() { | |
for { | |
select { | |
// Add new available client | |
case client := <-stream.NewClients: | |
stream.TotalClients[client.ID] = client.Channel | |
log.Printf("Added client. %d registered clients", len(stream.TotalClients)) | |
// Remove closed client | |
case client := <-stream.ClosedClients: | |
delete(stream.TotalClients, client.ID) | |
close(client.Channel) | |
ID -= 1 | |
log.Printf("Removed client. %d registered clients", len(stream.TotalClients)) | |
// Broadcast message to a specific client with client ID fetched from eventMsg.To | |
case eventMsg := <-stream.Message: | |
stream.TotalClients[eventMsg.To] <- eventMsg | |
} | |
} | |
} | |
// Mandatory Headers which should be set in the Response header for SSE to work. | |
func HeadersMiddleware() gin.HandlerFunc { | |
return func(c *gin.Context) { | |
c.Writer.Header().Set("Content-Type", "text/event-stream") | |
c.Writer.Header().Set("Cache-Control", "no-cache") | |
c.Writer.Header().Set("Connection", "keep-alive") | |
c.Writer.Header().Set("Transfer-Encoding", "chunked") | |
c.Next() | |
} | |
} | |
// This is a middleware which creates a Client struct variable with unique UUID & Channel, | |
// And sets it in the connection's context. | |
func (stream *Event) SSEConnMiddleware() gin.HandlerFunc { | |
return func(gctx *gin.Context) { | |
// Increment global variable ID | |
ID += 1 | |
// Initialize client | |
client := Client{ | |
ID: ID, | |
Channel: make(chan Data), | |
} | |
// Send new connection to event to store | |
stream.NewClients <- client | |
defer func() { | |
// Send closed connection to event server | |
log.Printf("Closing connection : %d", client.ID) | |
stream.ClosedClients <- client | |
}() | |
gctx.Set("client", client) | |
gctx.Next() | |
} | |
} | |
func main() { | |
router := gin.Default() | |
stream := NewEvent() | |
router.GET("/stream", HeadersMiddleware(), stream.SSEConnMiddleware(), func(gctx *gin.Context) { | |
v, ok := gctx.Get("client") | |
if !ok { | |
gctx.Status(http.StatusInternalServerError) | |
return | |
} | |
client, ok := v.(Client) | |
if !ok { | |
gctx.Status(http.StatusInternalServerError) | |
return | |
} | |
// Data to be sent to a specific client | |
// Currently this data would be sent to the first client on every new connection | |
data := Data{ | |
Message: "New Client in town", | |
From: client.ID, | |
To: 1, // To send this data to a specified client, you can change this to the specific client ID | |
} | |
// This goroutine will send the above data to Message channel | |
// Which will pass through listen(), where it will get sent to the specified client (To) | |
go func() { | |
if stream.TotalClients[data.To] == nil { | |
// Client doesn't exist or disconnected | |
log.Printf("Receiver - %d doesn't exist or disconnected.", data.To) | |
} else { | |
stream.Message <- data | |
} | |
}() | |
gctx.Stream(func(w io.Writer) bool { | |
// Stream data to client | |
for { | |
select { | |
// Send msg to the client | |
case msg, ok := <-client.Channel: | |
if !ok { | |
return false | |
} | |
gctx.SSEvent("message", msg) | |
return true | |
// Client exit | |
case <-gctx.Request.Context().Done(): | |
return false | |
} | |
} | |
}) | |
}) | |
// Parse Static files | |
router.StaticFile("/", "./index.html") | |
router.Run(":8085") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment