├── .gitignore ├── README.md ├── rooms.go ├── go.mod ├── LICENSE ├── main.go ├── stats.go ├── routes.go ├── resources ├── static │ ├── prismjs.min.css │ ├── realtime.js │ ├── prismjs.min.js │ ├── epoch.min.js │ └── epoch.min.css └── room_login.templ.html └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Gin Web Server 2 | 3 | This repo can be used as a starting point to deploy [Go](https://golang.org/) web applications on Render. 4 | 5 | It is based on the [realtime chat](https://github.com/gin-gonic/examples/tree/master/realtime-advanced) example powered by the [Gin](https://github.com/gin-gonic/gin) web framework. 6 | 7 | The sample app is up at https://go-gin.onrender.com. 8 | 9 | ## Deployment 10 | 11 | See the guide at https://render.com/docs/deploy-go-gin. 12 | -------------------------------------------------------------------------------- /rooms.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/dustin/go-broadcast" 4 | 5 | var roomChannels = make(map[string]broadcast.Broadcaster) 6 | 7 | func openListener(roomid string) chan interface{} { 8 | listener := make(chan interface{}) 9 | room(roomid).Register(listener) 10 | return listener 11 | } 12 | 13 | func closeListener(roomid string, listener chan interface{}) { 14 | room(roomid).Unregister(listener) 15 | close(listener) 16 | } 17 | 18 | func room(roomid string) broadcast.Broadcaster { 19 | b, ok := roomChannels[roomid] 20 | if !ok { 21 | b = broadcast.NewBroadcaster(10) 22 | roomChannels[roomid] = b 23 | } 24 | return b 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/render-examples/go-gin-web-server 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 7 | github.com/gin-gonic/gin v1.7.4 8 | github.com/go-playground/universal-translator v0.17.0 // indirect 9 | github.com/golang/protobuf v1.3.4 // indirect 10 | github.com/json-iterator/go v1.1.9 // indirect 11 | github.com/leodido/go-urn v1.2.0 // indirect 12 | github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 13 | github.com/mattn/go-isatty v0.0.12 // indirect 14 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 15 | github.com/modern-go/reflect2 v1.0.1 // indirect 16 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect 17 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 18 | gopkg.in/go-playground/validator.v9 v9.31.0 // indirect 19 | gopkg.in/yaml.v2 v2.2.8 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019, Render Developers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func main() { 13 | ConfigRuntime() 14 | StartWorkers() 15 | StartGin() 16 | } 17 | 18 | // ConfigRuntime sets the number of operating system threads. 19 | func ConfigRuntime() { 20 | nuCPU := runtime.NumCPU() 21 | runtime.GOMAXPROCS(nuCPU) 22 | fmt.Printf("Running with %d CPUs\n", nuCPU) 23 | } 24 | 25 | // StartWorkers start starsWorker by goroutine. 26 | func StartWorkers() { 27 | go statsWorker() 28 | } 29 | 30 | // StartGin starts gin web server with setting router. 31 | func StartGin() { 32 | gin.SetMode(gin.ReleaseMode) 33 | 34 | router := gin.New() 35 | router.Use(rateLimit, gin.Recovery()) 36 | router.LoadHTMLGlob("resources/*.templ.html") 37 | router.Static("/static", "resources/static") 38 | router.GET("/", index) 39 | router.GET("/room/:roomid", roomGET) 40 | router.POST("/room-post/:roomid", roomPOST) 41 | router.GET("/stream/:roomid", streamRoom) 42 | 43 | port := os.Getenv("PORT") 44 | if port == "" { 45 | port = "8080" 46 | } 47 | if err := router.Run(":" + port); err != nil { 48 | log.Panicf("error: %s", err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "time" 7 | 8 | "github.com/manucorporat/stats" 9 | ) 10 | 11 | var ( 12 | ips = stats.New() 13 | messages = stats.New() 14 | users = stats.New() 15 | mutexStats sync.RWMutex 16 | savedStats map[string]uint64 17 | ) 18 | 19 | func statsWorker() { 20 | c := time.Tick(1 * time.Second) 21 | var lastMallocs uint64 22 | var lastFrees uint64 23 | for range c { 24 | var stats runtime.MemStats 25 | runtime.ReadMemStats(&stats) 26 | 27 | mutexStats.Lock() 28 | savedStats = map[string]uint64{ 29 | "timestamp": uint64(time.Now().Unix()), 30 | "HeapInuse": stats.HeapInuse, 31 | "StackInuse": stats.StackInuse, 32 | "Mallocs": stats.Mallocs - lastMallocs, 33 | "Frees": stats.Frees - lastFrees, 34 | "Inbound": uint64(messages.Get("inbound")), 35 | "Outbound": uint64(messages.Get("outbound")), 36 | "Connected": connectedUsers(), 37 | } 38 | lastMallocs = stats.Mallocs 39 | lastFrees = stats.Frees 40 | messages.Reset() 41 | mutexStats.Unlock() 42 | } 43 | } 44 | 45 | func connectedUsers() uint64 { 46 | connected := users.Get("connected") - users.Get("disconnected") 47 | if connected < 0 { 48 | return 0 49 | } 50 | return uint64(connected) 51 | } 52 | 53 | // Stats returns savedStats data. 54 | func Stats() map[string]uint64 { 55 | mutexStats.RLock() 56 | defer mutexStats.RUnlock() 57 | 58 | return savedStats 59 | } 60 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func rateLimit(c *gin.Context) { 15 | ip := c.ClientIP() 16 | value := int(ips.Add(ip, 1)) 17 | if value%50 == 0 { 18 | fmt.Printf("ip: %s, count: %d\n", ip, value) 19 | } 20 | if value >= 200 { 21 | if value%200 == 0 { 22 | fmt.Println("ip blocked") 23 | } 24 | c.Abort() 25 | c.String(http.StatusServiceUnavailable, "you were automatically banned :)") 26 | } 27 | } 28 | 29 | func index(c *gin.Context) { 30 | c.Redirect(http.StatusMovedPermanently, "/room/hn") 31 | } 32 | 33 | func roomGET(c *gin.Context) { 34 | roomid := c.Param("roomid") 35 | nick := c.Query("nick") 36 | if len(nick) < 2 { 37 | nick = "" 38 | } 39 | if len(nick) > 13 { 40 | nick = nick[0:12] + "..." 41 | } 42 | c.HTML(http.StatusOK, "room_login.templ.html", gin.H{ 43 | "roomid": roomid, 44 | "nick": nick, 45 | "timestamp": time.Now().Unix(), 46 | }) 47 | 48 | } 49 | 50 | func roomPOST(c *gin.Context) { 51 | roomid := c.Param("roomid") 52 | nick := c.Query("nick") 53 | message := c.PostForm("message") 54 | message = strings.TrimSpace(message) 55 | 56 | validMessage := len(message) > 1 && len(message) < 200 57 | validNick := len(nick) > 1 && len(nick) < 14 58 | if !validMessage || !validNick { 59 | c.JSON(http.StatusBadRequest, gin.H{ 60 | "status": "failed", 61 | "error": "the message or nickname is too long", 62 | }) 63 | return 64 | } 65 | 66 | post := gin.H{ 67 | "nick": html.EscapeString(nick), 68 | "message": html.EscapeString(message), 69 | } 70 | messages.Add("inbound", 1) 71 | room(roomid).Submit(post) 72 | c.JSON(http.StatusOK, post) 73 | } 74 | 75 | func streamRoom(c *gin.Context) { 76 | roomid := c.Param("roomid") 77 | listener := openListener(roomid) 78 | ticker := time.NewTicker(1 * time.Second) 79 | users.Add("connected", 1) 80 | defer func() { 81 | closeListener(roomid, listener) 82 | ticker.Stop() 83 | users.Add("disconnected", 1) 84 | }() 85 | 86 | c.Stream(func(w io.Writer) bool { 87 | select { 88 | case msg := <-listener: 89 | messages.Add("outbound", 1) 90 | c.SSEvent("message", msg) 91 | case <-ticker.C: 92 | c.SSEvent("stats", Stats()) 93 | } 94 | return true 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /resources/static/prismjs.min.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 13 | direction: ltr; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | line-height: 1.5; 19 | 20 | -moz-tab-size: 4; 21 | -o-tab-size: 4; 22 | tab-size: 4; 23 | 24 | -webkit-hyphens: none; 25 | -moz-hyphens: none; 26 | -ms-hyphens: none; 27 | hyphens: none; 28 | } 29 | 30 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 31 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 32 | text-shadow: none; 33 | background: #b3d4fc; 34 | } 35 | 36 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 37 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 38 | text-shadow: none; 39 | background: #b3d4fc; 40 | } 41 | 42 | @media print { 43 | code[class*="language-"], 44 | pre[class*="language-"] { 45 | text-shadow: none; 46 | } 47 | } 48 | 49 | /* Code blocks */ 50 | pre[class*="language-"] { 51 | padding: 1em; 52 | margin: .5em 0; 53 | overflow: auto; 54 | } 55 | 56 | :not(pre) > code[class*="language-"], 57 | pre[class*="language-"] { 58 | background: #f5f2f0; 59 | } 60 | 61 | /* Inline code */ 62 | :not(pre) > code[class*="language-"] { 63 | padding: .1em; 64 | border-radius: .3em; 65 | } 66 | 67 | .token.comment, 68 | .token.prolog, 69 | .token.doctype, 70 | .token.cdata { 71 | color: slategray; 72 | } 73 | 74 | .token.punctuation { 75 | color: #999; 76 | } 77 | 78 | .namespace { 79 | opacity: .7; 80 | } 81 | 82 | .token.property, 83 | .token.tag, 84 | .token.boolean, 85 | .token.number, 86 | .token.constant, 87 | .token.symbol, 88 | .token.deleted { 89 | color: #905; 90 | } 91 | 92 | .token.selector, 93 | .token.attr-name, 94 | .token.string, 95 | .token.char, 96 | .token.builtin, 97 | .token.inserted { 98 | color: #690; 99 | } 100 | 101 | .token.operator, 102 | .token.entity, 103 | .token.url, 104 | .language-css .token.string, 105 | .style .token.string { 106 | color: #a67f59; 107 | background: hsla(0, 0%, 100%, .5); 108 | } 109 | 110 | .token.atrule, 111 | .token.attr-value, 112 | .token.keyword { 113 | color: #07a; 114 | } 115 | 116 | .token.function { 117 | color: #DD4A68; 118 | } 119 | 120 | .token.regex, 121 | .token.important, 122 | .token.variable { 123 | color: #e90; 124 | } 125 | 126 | .token.important, 127 | .token.bold { 128 | font-weight: bold; 129 | } 130 | .token.italic { 131 | font-style: italic; 132 | } 133 | 134 | .token.entity { 135 | cursor: help; 136 | } 137 | 138 | -------------------------------------------------------------------------------- /resources/static/realtime.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function StartRealtime(roomid, timestamp) { 4 | StartEpoch(timestamp); 5 | StartSSE(roomid); 6 | StartForm(); 7 | } 8 | 9 | function StartForm() { 10 | $('#chat-message').focus(); 11 | $('#chat-form').ajaxForm(function() { 12 | $('#chat-message').val(''); 13 | $('#chat-message').focus(); 14 | }); 15 | } 16 | 17 | function StartEpoch(timestamp) { 18 | var windowSize = 60; 19 | var height = 200; 20 | var defaultData = histogram(windowSize, timestamp); 21 | 22 | window.heapChart = $('#heapChart').epoch({ 23 | type: 'time.area', 24 | axes: ['bottom', 'left'], 25 | height: height, 26 | historySize: 10, 27 | data: [ 28 | {values: defaultData}, 29 | {values: defaultData} 30 | ] 31 | }); 32 | 33 | window.mallocsChart = $('#mallocsChart').epoch({ 34 | type: 'time.area', 35 | axes: ['bottom', 'left'], 36 | height: height, 37 | historySize: 10, 38 | data: [ 39 | {values: defaultData}, 40 | {values: defaultData} 41 | ] 42 | }); 43 | 44 | window.messagesChart = $('#messagesChart').epoch({ 45 | type: 'time.line', 46 | axes: ['bottom', 'left'], 47 | height: 240, 48 | historySize: 10, 49 | data: [ 50 | {values: defaultData}, 51 | {values: defaultData}, 52 | {values: defaultData} 53 | ] 54 | }); 55 | } 56 | 57 | function StartSSE(roomid) { 58 | if (!window.EventSource) { 59 | alert("EventSource is not enabled in this browser"); 60 | return; 61 | } 62 | var source = new EventSource('/stream/'+roomid); 63 | source.addEventListener('message', newChatMessage, false); 64 | source.addEventListener('stats', stats, false); 65 | } 66 | 67 | function stats(e) { 68 | var data = parseJSONStats(e.data); 69 | heapChart.push(data.heap); 70 | mallocsChart.push(data.mallocs); 71 | messagesChart.push(data.messages); 72 | } 73 | 74 | function parseJSONStats(e) { 75 | var data = jQuery.parseJSON(e); 76 | var timestamp = data.timestamp; 77 | 78 | var heap = [ 79 | {time: timestamp, y: data.HeapInuse}, 80 | {time: timestamp, y: data.StackInuse} 81 | ]; 82 | 83 | var mallocs = [ 84 | {time: timestamp, y: data.Mallocs}, 85 | {time: timestamp, y: data.Frees} 86 | ]; 87 | var messages = [ 88 | {time: timestamp, y: data.Connected}, 89 | {time: timestamp, y: data.Inbound}, 90 | {time: timestamp, y: data.Outbound} 91 | ]; 92 | 93 | return { 94 | heap: heap, 95 | mallocs: mallocs, 96 | messages: messages 97 | } 98 | } 99 | 100 | function newChatMessage(e) { 101 | var data = jQuery.parseJSON(e.data); 102 | var nick = data.nick; 103 | var message = data.message; 104 | var style = rowStyle(nick); 105 | var html = "
Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. Learn more.
62 |The chat and the charts data is provided in realtime using the SSE implementation of Gin Framework.
63 || Nick | 70 |Message | 71 |
|---|
102 | ◼︎ Users
103 | ◼︎ Inbound messages / sec
104 | ◼︎ Outbound messages / sec
105 |
116 |
117 | 118 |
119 | ◼︎ Heap bytes
120 | ◼︎ Stack bytes
121 |
126 |
127 | 128 |
129 | ◼︎ Mallocs / sec
130 | ◼︎ Frees / sec
131 |
func streamRoom(c *gin.Context) {
145 | roomid := c.ParamValue("roomid")
146 | listener := openListener(roomid)
147 | statsTicker := time.NewTicker(1 * time.Second)
148 | defer closeListener(roomid, listener)
149 | defer statsTicker.Stop()
150 |
151 | c.Stream(func(w io.Writer) bool {
152 | select {
153 | case msg := <-listener:
154 | c.SSEvent("message", msg)
155 | case <-statsTicker.C:
156 | c.SSEvent("stats", Stats())
157 | }
158 | return true
159 | })
160 | }
161 | function StartSSE(roomid) {
165 | var source = new EventSource('/stream/'+roomid);
166 | source.addEventListener('message', newChatMessage, false);
167 | source.addEventListener('stats', stats, false);
168 | }
169 | import "github.com/manucorporat/sse"
175 |
176 | func httpHandler(w http.ResponseWriter, req *http.Request) {
177 | // data can be a primitive like a string, an integer or a float
178 | sse.Encode(w, sse.Event{
179 | Event: "message",
180 | Data: "some data\nmore data",
181 | })
182 |
183 | // also a complex type, like a map, a struct or a slice
184 | sse.Encode(w, sse.Event{
185 | Id: "124",
186 | Event: "message",
187 | Data: map[string]interface{}{
188 | "user": "manu",
189 | "date": time.Now().Unix(),
190 | "content": "hi!",
191 | },
192 | })
193 | }
194 | event: message
195 | data: some data\\nmore data
196 |
197 | id: 124
198 | event: message
199 | data: {"content":"hi!","date":1431540810,"user":"manu"}
200 |