├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── benchmark │ └── main.go ├── chatroom │ └── main.go ├── tcp │ ├── client.go │ └── server.go └── websocket │ ├── client.go │ ├── server.go │ └── server_gorilla.go ├── config └── chatroom.yaml ├── global ├── config.go └── init.go ├── go.mod ├── go.sum ├── logic ├── broadcast.go ├── message.go ├── offline.go ├── sensitive.go └── user.go ├── polarisxu-qrcode-m.jpg ├── server ├── handle.go ├── home.go └── websocket.go └── template └── home.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chatroom 2 | 3 | 《Go 语言编程之旅:一起用 Go 做项目》 第四章聊天室源码。 4 | 5 | ## 关于本书 6 | 7 | 本书涵盖 Go 语言的各大经典实战,不介绍 Go 语言的语法基础,内容面向项目实践,同时会针对核心细节进行分析。而在实际项目迭代中,常常会出现或多或少的事故,因此本书也针对 Go 语言的大杀器(分析工具)以及常见问题进行了全面讲解。 8 | 9 | 本书适合已经大致学习了 Go 语言的基础语法后,想要跨越到下一个阶段的开发人员,可以填补该阶段的空白和进一步拓展你的思维方向。 10 | 11 | - 作者:陈剑煜(煎鱼),GitHub:[@eddycjy](https://github.com/eddycjy),微信公众号:脑子进煎鱼了。 12 | - 作者:徐新华(polaris),GitHub:[@polaris](https://github.com/polaris1119),微信公众号:Go语言中文网。 13 | 14 | ## 购买链接 15 | 16 | - 京东:https://item.jd.com/12685249.html 17 | - 当当:http://product.dangdang.com/28982027.html 18 | - 天猫:https://detail.tmall.com/item.htm?id=622185710833 19 | 20 | ## 关注我 21 | 22 | polarisxu 个人公众号 23 | 24 |  25 | -------------------------------------------------------------------------------- /cmd/benchmark/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/polaris1119/chatroom/logic" 12 | "nhooyr.io/websocket" 13 | "nhooyr.io/websocket/wsjson" 14 | ) 15 | 16 | var ( 17 | userNum int // 用户数 18 | loginInterval time.Duration // 用户登录时间间隔 19 | msgInterval time.Duration // 同一个用户发送消息间隔 20 | ) 21 | 22 | func init() { 23 | flag.IntVar(&userNum, "u", 500, "登录用户数") 24 | flag.DurationVar(&loginInterval, "l", 5e9, "用户陆续登录时间间隔") 25 | flag.DurationVar(&msgInterval, "m", 1*time.Minute, "用户发送消息时间间隔") 26 | } 27 | 28 | func main() { 29 | flag.Parse() 30 | 31 | for i := 0; i < userNum; i++ { 32 | go UserConnect("user" + strconv.Itoa(i)) 33 | time.Sleep(loginInterval) 34 | } 35 | 36 | select {} 37 | } 38 | 39 | func UserConnect(nickname string) { 40 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 41 | defer cancel() 42 | 43 | conn, _, err := websocket.Dial(ctx, "ws://127.0.0.1:2022/ws?nickname="+nickname, nil) 44 | if err != nil { 45 | log.Println("Dial error:", err) 46 | return 47 | } 48 | defer conn.Close(websocket.StatusInternalError, "内部错误!") 49 | 50 | go sendMessage(conn, nickname) 51 | 52 | ctx = context.Background() 53 | 54 | for { 55 | var message logic.Message 56 | err = wsjson.Read(ctx, conn, &message) 57 | if err != nil { 58 | log.Println("receive msg error:", err) 59 | continue 60 | } 61 | 62 | if message.ClientSendTime.IsZero() { 63 | continue 64 | } 65 | if d := time.Now().Sub(message.ClientSendTime); d > 1*time.Second { 66 | fmt.Printf("接收到服务端响应(%d):%#v\n", d.Milliseconds(), message) 67 | } 68 | } 69 | 70 | conn.Close(websocket.StatusNormalClosure, "") 71 | } 72 | 73 | func sendMessage(conn *websocket.Conn, nickname string) { 74 | ctx := context.Background() 75 | i := 1 76 | for { 77 | msg := map[string]string{ 78 | "content": "来自" + nickname + "的消息:" + strconv.Itoa(i), 79 | "send_time": strconv.FormatInt(time.Now().UnixNano(), 10), 80 | } 81 | err := wsjson.Write(ctx, conn, msg) 82 | if err != nil { 83 | log.Println("send msg error:", err, "nickname:", nickname, "no:", i) 84 | } 85 | i++ 86 | 87 | time.Sleep(msgInterval) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cmd/chatroom/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | _ "net/http/pprof" 9 | 10 | "github.com/polaris1119/chatroom/global" 11 | "github.com/polaris1119/chatroom/server" 12 | ) 13 | 14 | var ( 15 | addr = ":2022" 16 | banner = ` 17 | ____ _____ 18 | | | | /\ | 19 | | |____| / \ | 20 | | | | /----\ | 21 | |____| |/ \ | 22 | 23 | Go语言编程之旅 —— 一起用Go做项目:ChatRoom,start on:%s 24 | 25 | ` 26 | ) 27 | 28 | func init() { 29 | global.Init() 30 | } 31 | 32 | func main() { 33 | fmt.Printf(banner, addr) 34 | 35 | server.RegisterHandle() 36 | 37 | log.Fatal(http.ListenAndServe(addr, nil)) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/tcp/client.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | package main 5 | 6 | import ( 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | conn, err := net.Dial("tcp", ":2020") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | done := make(chan struct{}) 20 | go func() { 21 | io.Copy(os.Stdout, conn) // NOTE: ignoring errors 22 | log.Println("done") 23 | done <- struct{}{} // signal the main goroutine 24 | }() 25 | 26 | mustCopy(conn, os.Stdin) 27 | conn.Close() 28 | <-done 29 | } 30 | 31 | func mustCopy(dst io.Writer, src io.Reader) { 32 | if _, err := io.Copy(dst, src); err != nil { 33 | log.Fatal(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/tcp/server.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 polaris@studygolang.com. 2 | // License: Apache Licence 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "log" 10 | "net" 11 | "strconv" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | listener, err := net.Listen("tcp", ":2020") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | go broadcaster() 23 | 24 | for { 25 | conn, err := listener.Accept() 26 | if err != nil { 27 | log.Println(err) 28 | continue 29 | } 30 | 31 | go handleConn(conn) 32 | } 33 | } 34 | 35 | type User struct { 36 | ID int 37 | Addr string 38 | EnterAt time.Time 39 | MessageChannel chan string 40 | } 41 | 42 | func (u *User) String() string { 43 | return u.Addr + ", UID:" + strconv.Itoa(u.ID) + ", Enter At:" + 44 | u.EnterAt.Format("2006-01-02 15:04:05+8000") 45 | } 46 | 47 | // 给用户发送的消息 48 | type Message struct { 49 | OwnerID int 50 | Content string 51 | } 52 | 53 | var ( 54 | // 新用户到来,通过该 channel 进行登记 55 | enteringChannel = make(chan *User) 56 | // 用户离开,通过该 channel 进行登记 57 | leavingChannel = make(chan *User) 58 | // 广播专用的用户普通消息 channel,缓冲是尽可能避免出现异常情况堵塞 59 | messageChannel = make(chan Message, 8) 60 | ) 61 | 62 | // broadcaster 用于记录聊天室用户,并进行消息广播: 63 | // 1. 新用户进来;2. 用户普通消息;3. 用户离开 64 | func broadcaster() { 65 | users := make(map[*User]struct{}) 66 | 67 | for { 68 | select { 69 | case user := <-enteringChannel: 70 | // 新用户进入 71 | users[user] = struct{}{} 72 | case user := <-leavingChannel: 73 | // 用户离开 74 | delete(users, user) 75 | // 避免 goroutine 泄露 76 | close(user.MessageChannel) 77 | case msg := <-messageChannel: 78 | // 给所有在线用户发送消息 79 | for user := range users { 80 | if user.ID == msg.OwnerID { 81 | continue 82 | } 83 | user.MessageChannel <- msg.Content 84 | } 85 | } 86 | } 87 | } 88 | 89 | func handleConn(conn net.Conn) { 90 | defer conn.Close() 91 | 92 | // 1. 新用户进来,构建该用户的实例 93 | user := &User{ 94 | ID: GenUserID(), 95 | Addr: conn.RemoteAddr().String(), 96 | EnterAt: time.Now(), 97 | MessageChannel: make(chan string, 8), 98 | } 99 | 100 | // 2. 当前在一个新的 goroutine 中,用来进行读操作,因此需要开一个 goroutine 用于写操作 101 | // 读写 goroutine 之间可以通过 channel 进行通信 102 | go sendMessage(conn, user.MessageChannel) 103 | 104 | // 3. 给当前用户发送欢迎信息;给所有用户告知新用户到来 105 | user.MessageChannel <- "Welcome, " + user.String() 106 | msg := Message{ 107 | OwnerID: user.ID, 108 | Content: "user:`" + strconv.Itoa(user.ID) + "` has enter", 109 | } 110 | messageChannel <- msg 111 | 112 | // 4. 将该记录到全局的用户列表中,避免用锁 113 | enteringChannel <- user 114 | 115 | // 控制超时用户踢出 116 | var userActive = make(chan struct{}) 117 | go func() { 118 | d := 1 * time.Minute 119 | timer := time.NewTimer(d) 120 | for { 121 | select { 122 | case <-timer.C: 123 | conn.Close() 124 | case <-userActive: 125 | timer.Reset(d) 126 | } 127 | } 128 | }() 129 | 130 | // 5. 循环读取用户的输入 131 | input := bufio.NewScanner(conn) 132 | for input.Scan() { 133 | msg.Content = strconv.Itoa(user.ID) + ":" + input.Text() 134 | messageChannel <- msg 135 | 136 | // 用户活跃 137 | userActive <- struct{}{} 138 | } 139 | 140 | if err := input.Err(); err != nil { 141 | log.Println("读取错误:", err) 142 | } 143 | 144 | // 6. 用户离开 145 | leavingChannel <- user 146 | msg.Content = "user:`" + strconv.Itoa(user.ID) + "` has left" 147 | messageChannel <- msg 148 | } 149 | 150 | func sendMessage(conn net.Conn, ch <-chan string) { 151 | for msg := range ch { 152 | fmt.Fprintln(conn, msg) 153 | } 154 | } 155 | 156 | // 生成用户 ID 157 | var ( 158 | globalID int 159 | idLocker sync.Mutex 160 | ) 161 | 162 | func GenUserID() int { 163 | idLocker.Lock() 164 | defer idLocker.Unlock() 165 | 166 | globalID++ 167 | return globalID 168 | } 169 | -------------------------------------------------------------------------------- /cmd/websocket/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "nhooyr.io/websocket" 9 | "nhooyr.io/websocket/wsjson" 10 | ) 11 | 12 | func main() { 13 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 14 | defer cancel() 15 | 16 | c, _, err := websocket.Dial(ctx, "ws://localhost:2021/ws", nil) 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer c.Close(websocket.StatusInternalError, "内部错误!") 21 | 22 | err = wsjson.Write(ctx, c, "Hello WebSocket Server") 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | var v interface{} 28 | err = wsjson.Read(ctx, c, &v) 29 | if err != nil { 30 | panic(err) 31 | } 32 | fmt.Printf("接收到服务端响应:%v\n", v) 33 | 34 | c.Close(websocket.StatusNormalClosure, "") 35 | } 36 | -------------------------------------------------------------------------------- /cmd/websocket/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "nhooyr.io/websocket" 11 | "nhooyr.io/websocket/wsjson" 12 | ) 13 | 14 | func main() { 15 | http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 16 | fmt.Fprintln(w, "HTTP, Hello") 17 | }) 18 | 19 | http.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) { 20 | conn, err := websocket.Accept(w, req, nil) 21 | if err != nil { 22 | log.Println(err) 23 | return 24 | } 25 | defer conn.Close(websocket.StatusInternalError, "内部出错了!") 26 | 27 | ctx, cancel := context.WithTimeout(req.Context(), time.Second*10) 28 | defer cancel() 29 | 30 | var v interface{} 31 | err = wsjson.Read(ctx, conn, &v) 32 | if err != nil { 33 | log.Println(err) 34 | return 35 | } 36 | log.Printf("接收到客户端:%v\n", v) 37 | 38 | err = wsjson.Write(ctx, conn, "Hello WebSocket Client") 39 | if err != nil { 40 | log.Println(err) 41 | return 42 | } 43 | 44 | conn.Close(websocket.StatusNormalClosure, "") 45 | }) 46 | 47 | log.Fatal(http.ListenAndServe(":2021", nil)) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/websocket/server_gorilla.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 13 | fmt.Fprintln(w, "HTTP, Hello") 14 | }) 15 | 16 | upgrader := websocket.Upgrader{ 17 | ReadBufferSize: 1024, 18 | WriteBufferSize: 1024, 19 | } 20 | 21 | http.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) { 22 | conn, err := upgrader.Upgrade(w, req, nil) 23 | if err != nil { 24 | log.Println(err) 25 | return 26 | } 27 | defer conn.Close() 28 | 29 | // 就做一次读写 30 | var v interface{} 31 | err = conn.ReadJSON(&v) 32 | if err != nil { 33 | log.Println(err) 34 | return 35 | } 36 | 37 | log.Printf("接收到客户端:%s\n", v) 38 | 39 | if err := conn.WriteJSON("Hello WebSocket Client"); err != nil { 40 | log.Println(err) 41 | return 42 | } 43 | }) 44 | 45 | log.Fatal(http.ListenAndServe(":2021", nil)) 46 | } 47 | -------------------------------------------------------------------------------- /config/chatroom.yaml: -------------------------------------------------------------------------------- 1 | sensitive: 2 | - 坏蛋 3 | - 坏人 4 | - 发票 5 | - 傻子 6 | - 傻大个 7 | - 傻人 8 | - 傻逼 9 | 10 | offline-num: 10 11 | 12 | token-secret: 1zrvOpl5u14Y 13 | 14 | message-queue: 1024 15 | -------------------------------------------------------------------------------- /global/config.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "github.com/fsnotify/fsnotify" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var ( 9 | SensitiveWords []string 10 | 11 | MessageQueueLen = 1024 12 | ) 13 | 14 | func initConfig() { 15 | viper.SetConfigName("chatroom") 16 | viper.AddConfigPath(RootDir + "/config") 17 | 18 | if err := viper.ReadInConfig(); err != nil { 19 | panic(err) 20 | } 21 | 22 | SensitiveWords = viper.GetStringSlice("sensitive") 23 | MessageQueueLen = viper.GetInt("message-queue") 24 | 25 | viper.WatchConfig() 26 | viper.OnConfigChange(func(e fsnotify.Event) { 27 | viper.ReadInConfig() 28 | 29 | SensitiveWords = viper.GetStringSlice("sensitive") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /global/init.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sync" 7 | ) 8 | 9 | func init() { 10 | Init() 11 | } 12 | 13 | var RootDir string 14 | 15 | var once = new(sync.Once) 16 | 17 | func Init() { 18 | once.Do(func() { 19 | inferRootDir() 20 | initConfig() 21 | }) 22 | } 23 | 24 | // inferRootDir 推断出项目根目录 25 | func inferRootDir() { 26 | cwd, err := os.Getwd() 27 | if err != nil { 28 | panic(err) 29 | } 30 | var infer func(d string) string 31 | infer = func(d string) string { 32 | // 这里要确保项目根目录下存在 template 目录 33 | if exists(d + "/template") { 34 | return d 35 | } 36 | 37 | return infer(filepath.Dir(d)) 38 | } 39 | 40 | RootDir = infer(cwd) 41 | } 42 | 43 | func exists(filename string) bool { 44 | _, err := os.Stat(filename) 45 | return err == nil || os.IsExist(err) 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/polaris1119/chatroom 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | github.com/gorilla/websocket v1.4.1 8 | github.com/spf13/cast v1.3.0 9 | github.com/spf13/viper v1.6.2 10 | golang.org/x/sys v0.0.0-20200217220822-9197077df867 // indirect 11 | nhooyr.io/websocket v1.7.4 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 12 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 13 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 14 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 15 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 20 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 21 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 22 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 23 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 24 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 25 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 26 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 27 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 28 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 29 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 30 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 31 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 32 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 33 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 34 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 35 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 36 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 37 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 38 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 39 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 42 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 44 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 45 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 46 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 48 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 49 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 50 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 51 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 52 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 53 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 54 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 55 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 56 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 57 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 58 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 59 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 60 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 61 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 62 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 63 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 66 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 69 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 70 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 71 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 72 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 73 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 74 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 75 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 76 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 77 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 81 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 82 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 83 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 84 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 85 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 86 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 87 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 88 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 89 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 90 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 91 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 92 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 93 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 94 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 95 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 96 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 97 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 98 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 99 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 100 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 101 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 102 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 103 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 104 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 105 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 106 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 108 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 109 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 110 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 111 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 112 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 113 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 114 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 115 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 116 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 117 | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= 118 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 119 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 120 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 121 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 122 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 123 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 124 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 125 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 126 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 129 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 130 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 131 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 132 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 133 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw= 142 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 144 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 145 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 146 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 147 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 148 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 149 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 150 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 151 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 152 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 153 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 154 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 155 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 156 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 158 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 159 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 160 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 161 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 162 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 163 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 164 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 165 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 166 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 167 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 168 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 169 | nhooyr.io/websocket v1.7.4 h1:w/LGB2sZT0RV8lZYR7nfyaYz4PUbYZ5oF7NBon2M0NY= 170 | nhooyr.io/websocket v1.7.4/go.mod h1:PxYxCwFdFYQ0yRvtQz3s/dC+VEm7CSuC/4b9t8MQQxw= 171 | -------------------------------------------------------------------------------- /logic/broadcast.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "expvar" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/polaris1119/chatroom/global" 9 | ) 10 | 11 | func init() { 12 | expvar.Publish("message_queue", expvar.Func(calcMessageQueueLen)) 13 | } 14 | 15 | func calcMessageQueueLen() interface{} { 16 | fmt.Println("===len=:", len(Broadcaster.messageChannel)) 17 | return len(Broadcaster.messageChannel) 18 | } 19 | 20 | // broadcaster 广播器 21 | type broadcaster struct { 22 | // 所有聊天室用户 23 | users map[string]*User 24 | 25 | // 所有 channel 统一管理,可以避免外部乱用 26 | 27 | enteringChannel chan *User 28 | leavingChannel chan *User 29 | messageChannel chan *Message 30 | 31 | // 判断该昵称用户是否可进入聊天室(重复与否):true 能,false 不能 32 | checkUserChannel chan string 33 | checkUserCanInChannel chan bool 34 | 35 | // 获取用户列表 36 | requestUsersChannel chan struct{} 37 | usersChannel chan []*User 38 | } 39 | 40 | var Broadcaster = &broadcaster{ 41 | users: make(map[string]*User), 42 | 43 | enteringChannel: make(chan *User), 44 | leavingChannel: make(chan *User), 45 | messageChannel: make(chan *Message, global.MessageQueueLen), 46 | 47 | checkUserChannel: make(chan string), 48 | checkUserCanInChannel: make(chan bool), 49 | 50 | requestUsersChannel: make(chan struct{}), 51 | usersChannel: make(chan []*User), 52 | } 53 | 54 | // Start 启动广播器 55 | // 需要在一个新 goroutine 中运行,因为它不会返回 56 | func (b *broadcaster) Start() { 57 | for { 58 | select { 59 | case user := <-b.enteringChannel: 60 | // 新用户进入 61 | b.users[user.NickName] = user 62 | 63 | OfflineProcessor.Send(user) 64 | case user := <-b.leavingChannel: 65 | // 用户离开 66 | delete(b.users, user.NickName) 67 | // 避免 goroutine 泄露 68 | user.CloseMessageChannel() 69 | case msg := <-b.messageChannel: 70 | // 给所有在线用户发送消息 71 | for _, user := range b.users { 72 | if user.UID == msg.User.UID { 73 | continue 74 | } 75 | user.MessageChannel <- msg 76 | } 77 | OfflineProcessor.Save(msg) 78 | case nickname := <-b.checkUserChannel: 79 | if _, ok := b.users[nickname]; ok { 80 | b.checkUserCanInChannel <- false 81 | } else { 82 | b.checkUserCanInChannel <- true 83 | } 84 | case <-b.requestUsersChannel: 85 | userList := make([]*User, 0, len(b.users)) 86 | for _, user := range b.users { 87 | userList = append(userList, user) 88 | } 89 | 90 | b.usersChannel <- userList 91 | } 92 | } 93 | } 94 | 95 | func (b *broadcaster) UserEntering(u *User) { 96 | b.enteringChannel <- u 97 | } 98 | 99 | func (b *broadcaster) UserLeaving(u *User) { 100 | b.leavingChannel <- u 101 | } 102 | 103 | func (b *broadcaster) Broadcast(msg *Message) { 104 | if len(b.messageChannel) >= global.MessageQueueLen { 105 | log.Println("broadcast queue 满了") 106 | } 107 | b.messageChannel <- msg 108 | } 109 | 110 | func (b *broadcaster) CanEnterRoom(nickname string) bool { 111 | b.checkUserChannel <- nickname 112 | 113 | return <-b.checkUserCanInChannel 114 | } 115 | 116 | func (b *broadcaster) GetUserList() []*User { 117 | b.requestUsersChannel <- struct{}{} 118 | return <-b.usersChannel 119 | } 120 | -------------------------------------------------------------------------------- /logic/message.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | const ( 10 | MsgTypeNormal = iota // 普通 用户消息 11 | MsgTypeWelcome // 当前用户欢迎消息 12 | MsgTypeUserEnter // 用户进入 13 | MsgTypeUserLeave // 用户退出 14 | MsgTypeError // 错误消息 15 | ) 16 | 17 | // 给用户发送的消息 18 | type Message struct { 19 | // 哪个用户发送的消息 20 | User *User `json:"user"` 21 | Type int `json:"type"` 22 | Content string `json:"content"` 23 | MsgTime time.Time `json:"msg_time"` 24 | 25 | ClientSendTime time.Time `json:"client_send_time"` 26 | 27 | // 消息 @ 了谁 28 | Ats []string `json:"ats"` 29 | 30 | // 用户列表不通过 WebSocket 下发 31 | // Users []*User `json:"users"` 32 | } 33 | 34 | func NewMessage(user *User, content, clientTime string) *Message { 35 | message := &Message{ 36 | User: user, 37 | Type: MsgTypeNormal, 38 | Content: content, 39 | MsgTime: time.Now(), 40 | } 41 | if clientTime != "" { 42 | message.ClientSendTime = time.Unix(0, cast.ToInt64(clientTime)) 43 | } 44 | return message 45 | } 46 | 47 | func NewWelcomeMessage(user *User) *Message { 48 | return &Message{ 49 | User: user, 50 | Type: MsgTypeWelcome, 51 | Content: user.NickName + " 您好,欢迎加入聊天室!", 52 | MsgTime: time.Now(), 53 | } 54 | } 55 | 56 | func NewUserEnterMessage(user *User) *Message { 57 | return &Message{ 58 | User: user, 59 | Type: MsgTypeUserEnter, 60 | Content: user.NickName + " 加入了聊天室", 61 | MsgTime: time.Now(), 62 | } 63 | } 64 | 65 | func NewUserLeaveMessage(user *User) *Message { 66 | return &Message{ 67 | User: user, 68 | Type: MsgTypeUserLeave, 69 | Content: user.NickName + " 离开了聊天室", 70 | MsgTime: time.Now(), 71 | } 72 | } 73 | 74 | func NewErrorMessage(content string) *Message { 75 | return &Message{ 76 | User: System, 77 | Type: MsgTypeError, 78 | Content: content, 79 | MsgTime: time.Now(), 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /logic/offline.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "container/ring" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type offlineProcessor struct { 10 | n int 11 | 12 | // 保存所有用户最近的 n 条消息 13 | recentRing *ring.Ring 14 | 15 | // 保存某个用户离线消息(一样 n 条) 16 | userRing map[string]*ring.Ring 17 | } 18 | 19 | var OfflineProcessor = newOfflineProcessor() 20 | 21 | func newOfflineProcessor() *offlineProcessor { 22 | n := viper.GetInt("offline-num") 23 | 24 | return &offlineProcessor{ 25 | n: n, 26 | recentRing: ring.New(n), 27 | userRing: make(map[string]*ring.Ring), 28 | } 29 | } 30 | 31 | func (o *offlineProcessor) Save(msg *Message) { 32 | if msg.Type != MsgTypeNormal { 33 | return 34 | } 35 | o.recentRing.Value = msg 36 | o.recentRing = o.recentRing.Next() 37 | 38 | for _, nickname := range msg.Ats { 39 | nickname = nickname[1:] 40 | var ( 41 | r *ring.Ring 42 | ok bool 43 | ) 44 | if r, ok = o.userRing[nickname]; !ok { 45 | r = ring.New(o.n) 46 | } 47 | r.Value = msg 48 | o.userRing[nickname] = r.Next() 49 | } 50 | } 51 | 52 | func (o *offlineProcessor) Send(user *User) { 53 | o.recentRing.Do(func(value interface{}) { 54 | if value != nil { 55 | user.MessageChannel <- value.(*Message) 56 | } 57 | }) 58 | 59 | if user.isNew { 60 | return 61 | } 62 | 63 | if r, ok := o.userRing[user.NickName]; ok { 64 | r.Do(func(value interface{}) { 65 | if value != nil { 66 | user.MessageChannel <- value.(*Message) 67 | } 68 | }) 69 | 70 | delete(o.userRing, user.NickName) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /logic/sensitive.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/polaris1119/chatroom/global" 7 | ) 8 | 9 | func FilterSensitive(content string) string { 10 | for _, word := range global.SensitiveWords { 11 | content = strings.ReplaceAll(content, word, "**") 12 | } 13 | 14 | return content 15 | } 16 | -------------------------------------------------------------------------------- /logic/user.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "regexp" 12 | "strings" 13 | "sync/atomic" 14 | "time" 15 | 16 | "github.com/spf13/cast" 17 | "github.com/spf13/viper" 18 | "nhooyr.io/websocket" 19 | "nhooyr.io/websocket/wsjson" 20 | ) 21 | 22 | var globalUID uint32 = 0 23 | 24 | type User struct { 25 | UID int `json:"uid"` 26 | NickName string `json:"nickname"` 27 | EnterAt time.Time `json:"enter_at"` 28 | Addr string `json:"addr"` 29 | MessageChannel chan *Message `json:"-"` 30 | Token string `json:"token"` 31 | 32 | conn *websocket.Conn 33 | 34 | isNew bool 35 | } 36 | 37 | // 系统用户,代表是系统主动发送的消息 38 | var System = &User{} 39 | 40 | func NewUser(conn *websocket.Conn, token, nickname, addr string) *User { 41 | user := &User{ 42 | NickName: nickname, 43 | Addr: addr, 44 | EnterAt: time.Now(), 45 | MessageChannel: make(chan *Message, 32), 46 | Token: token, 47 | 48 | conn: conn, 49 | } 50 | 51 | if user.Token != "" { 52 | uid, err := parseTokenAndValidate(token, nickname) 53 | if err == nil { 54 | user.UID = uid 55 | } 56 | } 57 | 58 | if user.UID == 0 { 59 | user.UID = int(atomic.AddUint32(&globalUID, 1)) 60 | user.Token = genToken(user.UID, user.NickName) 61 | user.isNew = true 62 | } 63 | 64 | return user 65 | } 66 | 67 | func (u *User) SendMessage(ctx context.Context) { 68 | for msg := range u.MessageChannel { 69 | wsjson.Write(ctx, u.conn, msg) 70 | } 71 | } 72 | 73 | // CloseMessageChannel 避免 goroutine 泄露 74 | func (u *User) CloseMessageChannel() { 75 | close(u.MessageChannel) 76 | } 77 | 78 | func (u *User) ReceiveMessage(ctx context.Context) error { 79 | var ( 80 | receiveMsg map[string]string 81 | err error 82 | ) 83 | for { 84 | err = wsjson.Read(ctx, u.conn, &receiveMsg) 85 | if err != nil { 86 | // 判定连接是否关闭了,正常关闭,不认为是错误 87 | var closeErr websocket.CloseError 88 | if errors.As(err, &closeErr) { 89 | return nil 90 | } else if errors.Is(err, io.EOF) { 91 | return nil 92 | } 93 | 94 | return err 95 | } 96 | 97 | // 内容发送到聊天室 98 | sendMsg := NewMessage(u, receiveMsg["content"], receiveMsg["send_time"]) 99 | sendMsg.Content = FilterSensitive(sendMsg.Content) 100 | 101 | // 解析 content,看看 @ 谁了 102 | reg := regexp.MustCompile(`@[^\s@]{2,20}`) 103 | sendMsg.Ats = reg.FindAllString(sendMsg.Content, -1) 104 | 105 | Broadcaster.Broadcast(sendMsg) 106 | } 107 | } 108 | 109 | func genToken(uid int, nickname string) string { 110 | secret := viper.GetString("token-secret") 111 | message := fmt.Sprintf("%s%s%d", nickname, secret, uid) 112 | 113 | messageMAC := macSha256([]byte(message), []byte(secret)) 114 | 115 | return fmt.Sprintf("%suid%d", base64.StdEncoding.EncodeToString(messageMAC), uid) 116 | } 117 | 118 | func parseTokenAndValidate(token, nickname string) (int, error) { 119 | pos := strings.LastIndex(token, "uid") 120 | messageMAC, err := base64.StdEncoding.DecodeString(token[:pos]) 121 | if err != nil { 122 | return 0, err 123 | } 124 | uid := cast.ToInt(token[pos+3:]) 125 | 126 | secret := viper.GetString("token-secret") 127 | message := fmt.Sprintf("%s%s%d", nickname, secret, uid) 128 | 129 | ok := validateMAC([]byte(message), messageMAC, []byte(secret)) 130 | if ok { 131 | return uid, nil 132 | } 133 | 134 | return 0, errors.New("token is illegal") 135 | } 136 | 137 | func macSha256(message, secret []byte) []byte { 138 | mac := hmac.New(sha256.New, secret) 139 | mac.Write(message) 140 | return mac.Sum(nil) 141 | } 142 | 143 | func validateMAC(message, messageMAC, secret []byte) bool { 144 | mac := hmac.New(sha256.New, secret) 145 | mac.Write(message) 146 | expectedMAC := mac.Sum(nil) 147 | return hmac.Equal(messageMAC, expectedMAC) 148 | } 149 | -------------------------------------------------------------------------------- /polarisxu-qrcode-m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-programming-tour-book/chatroom/297866eda4fa20a5b6551c08c3cf88393211fb0a/polarisxu-qrcode-m.jpg -------------------------------------------------------------------------------- /server/handle.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/polaris1119/chatroom/logic" 7 | ) 8 | 9 | func RegisterHandle() { 10 | // 广播消息处理 11 | go logic.Broadcaster.Start() 12 | 13 | http.HandleFunc("/", homeHandleFunc) 14 | http.HandleFunc("/user_list", userListHandleFunc) 15 | http.HandleFunc("/ws", WebSocketHandleFunc) 16 | } 17 | -------------------------------------------------------------------------------- /server/home.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | 9 | "github.com/polaris1119/chatroom/global" 10 | "github.com/polaris1119/chatroom/logic" 11 | ) 12 | 13 | func homeHandleFunc(w http.ResponseWriter, req *http.Request) { 14 | tpl, err := template.ParseFiles(global.RootDir + "/template/home.html") 15 | if err != nil { 16 | fmt.Fprint(w, "模板解析错误!") 17 | return 18 | } 19 | 20 | err = tpl.Execute(w, nil) 21 | if err != nil { 22 | fmt.Fprint(w, "模板执行错误!") 23 | return 24 | } 25 | } 26 | 27 | func userListHandleFunc(w http.ResponseWriter, req *http.Request) { 28 | w.Header().Add("Content-Type", "application/json") 29 | w.WriteHeader(http.StatusOK) 30 | 31 | userList := logic.Broadcaster.GetUserList() 32 | b, err := json.Marshal(userList) 33 | 34 | if err != nil { 35 | fmt.Fprint(w, `[]`) 36 | } else { 37 | fmt.Fprint(w, string(b)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/websocket.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "nhooyr.io/websocket" 8 | "nhooyr.io/websocket/wsjson" 9 | 10 | "github.com/polaris1119/chatroom/logic" 11 | ) 12 | 13 | func WebSocketHandleFunc(w http.ResponseWriter, req *http.Request) { 14 | // Accept 从客户端接受 WebSocket 握手,并将连接升级到 WebSocket。 15 | // 如果 Origin 域与主机不同,Accept 将拒绝握手,除非设置了 InsecureSkipVerify 选项(通过第三个参数 AcceptOptions 设置)。 16 | // 换句话说,默认情况下,它不允许跨源请求。如果发生错误,Accept 将始终写入适当的响应 17 | conn, err := websocket.Accept(w, req, &websocket.AcceptOptions{InsecureSkipVerify: true}) 18 | if err != nil { 19 | log.Println("websocket accept error:", err) 20 | return 21 | } 22 | 23 | // 1. 新用户进来,构建该用户的实例 24 | token := req.FormValue("token") 25 | nickname := req.FormValue("nickname") 26 | if l := len(nickname); l < 2 || l > 20 { 27 | log.Println("nickname illegal: ", nickname) 28 | wsjson.Write(req.Context(), conn, logic.NewErrorMessage("非法昵称,昵称长度:2-20")) 29 | conn.Close(websocket.StatusUnsupportedData, "nickname illegal!") 30 | return 31 | } 32 | if !logic.Broadcaster.CanEnterRoom(nickname) { 33 | log.Println("昵称已经存在:", nickname) 34 | wsjson.Write(req.Context(), conn, logic.NewErrorMessage("该昵称已经已存在!")) 35 | conn.Close(websocket.StatusUnsupportedData, "nickname exists!") 36 | return 37 | } 38 | 39 | userHasToken := logic.NewUser(conn, token, nickname, req.RemoteAddr) 40 | 41 | // 2. 开启给用户发送消息的 goroutine 42 | go userHasToken.SendMessage(req.Context()) 43 | 44 | // 3. 给当前用户发送欢迎信息 45 | userHasToken.MessageChannel <- logic.NewWelcomeMessage(userHasToken) 46 | 47 | // 避免 token 泄露 48 | tmpUser := *userHasToken 49 | user := &tmpUser 50 | user.Token = "" 51 | 52 | // 给所有用户告知新用户到来 53 | msg := logic.NewUserEnterMessage(user) 54 | logic.Broadcaster.Broadcast(msg) 55 | 56 | // 4. 将该用户加入广播器的用列表中 57 | logic.Broadcaster.UserEntering(user) 58 | log.Println("user:", nickname, "joins chat") 59 | 60 | // 5. 接收用户消息 61 | err = user.ReceiveMessage(req.Context()) 62 | 63 | // 6. 用户离开 64 | logic.Broadcaster.UserLeaving(user) 65 | msg = logic.NewUserLeaveMessage(user) 66 | logic.Broadcaster.Broadcast(msg) 67 | log.Println("user:", nickname, "leaves chat") 68 | 69 | // 根据读取时的错误执行不同的 Close 70 | if err == nil { 71 | conn.Close(websocket.StatusNormalClosure, "") 72 | } else { 73 | log.Println("read from client error:", err) 74 | conn.Close(websocket.StatusInternalError, "Read from client error") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /template/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |