├── go.mod ├── chat.png ├── scc.png ├── .gitignore ├── README.md ├── LICENSE ├── client └── smallchat-client.go └── server └── smallchat-server.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smallnest/smallchat 2 | 3 | go 1.23.1 4 | -------------------------------------------------------------------------------- /chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallnest/smallchat/HEAD/chat.png -------------------------------------------------------------------------------- /scc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallnest/smallchat/HEAD/scc.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smallchat 2 | A minimal programming example for a chat server in Go. 3 | 4 | Redis作者 Salvatore Sanfilippo 最近创建一个新的演示项目:[smallchat](https://github.com/antirez/smallchat),用了200行代码实现了一个聊天室。这个项目的目的是展示如何用Go语言实现一个简单的聊天服务器。 5 | 6 | 他写这个项目的动机如下: 7 | 8 | > 昨天我正在与几个前端开发者朋友闲聊,他们距离系统编程有些远。我们回忆起了过去的IRC时光。不可避免地,我说:编写一个非常简单的IRC服务器每个人都应该做一次。这样程序中有非常有趣的部分。一个进程进行多路复用,维护客户端状态,可以用不同的方式实现等等。 9 | > 10 | > 然后讨论继续,我想,我会给你们展示一个极简的C语言例子。但是你能编写出啥样的最小聊天服务器呢?要真正做到极简,我们不应该需要任何特殊的客户端,即使不是很完美,它应该可以用`telnet`或`nc`(netcat)作为客户端连接。服务器的主要功能只是接收一些聊天信息并发送给所有其他客户端,这有时称为**扇出**操作。这还需要一个合适的`readline()`函数,然后是缓冲等等。我们想要更简单的:利用内核缓冲区,假装我们每次都从客户端收到一个完整的行(这个假设在实际中通常是正确的,所以这个假设没啥问题)。 11 | > 12 | > 好吧,有了这些技巧,我们可以用只有200行代码实现一个聊天室,用户甚至可以设置昵称(当然,不计空格和注释)。由于我将这个小程序作为示例编写给我的朋友,我决定也把它推到Github上。 13 | 14 | 15 | 嗯,很有意思,这让我想起我最早在学校了玩BBS的时光,看到大家写的BBS服务端和客户端就觉得很神奇。最近几年我也一直在做网络编程方面的工作,所以我想使用Go语言实现antirez的这个小项目,看看Go语言在网络编程方面的表现如何。 16 | 17 | 最后以不到100行的代码实现了一个聊天室。 18 | 19 | ![](chat.png) 20 | 21 | 22 | 除去注释,不到一百行: 23 | ![](scc.png) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 smallnest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/smallchat-client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | serverHost = flag.String("h", "localhost", "server host") 14 | serverPort = flag.Int("p", 8972, "server port") 15 | ) 16 | 17 | func main() { 18 | flag.Parse() 19 | 20 | // 连接服务器 21 | conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", *serverHost, *serverPort)) 22 | if err != nil { 23 | fmt.Println("failed to connect to server:", err) 24 | os.Exit(1) 25 | } 26 | defer conn.Close() 27 | 28 | fmt.Println("failed to connect to chat server") 29 | 30 | // 启动goroutine接收服务器消息 31 | go func() { 32 | buffer := make([]byte, 1024) 33 | for { 34 | n, err := conn.Read(buffer) 35 | if err != nil { 36 | fmt.Println("\ndisconnected from server") 37 | os.Exit(0) 38 | } 39 | fmt.Print(string(buffer[:n])) 40 | } 41 | }() 42 | 43 | // 从标准输入读取消息并发送 44 | scanner := bufio.NewScanner(os.Stdin) 45 | for scanner.Scan() { 46 | msg := scanner.Text() 47 | msg = strings.TrimSpace(msg) 48 | 49 | if msg == "" { 50 | continue 51 | } 52 | 53 | // 发送消息到服务器 54 | _, err := conn.Write([]byte(msg + "\n")) 55 | if err != nil { 56 | fmt.Println("failed to send message:", err) 57 | break 58 | } 59 | 60 | // 如果输入 /quit 则退出 61 | if msg == "/quit" { 62 | fmt.Println("goodbye!") 63 | return 64 | } 65 | } 66 | 67 | if err := scanner.Err(); err != nil { 68 | fmt.Println("failed to read input:", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/smallchat-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | const ( 13 | maxClients = 1000 14 | maxNickLen = 32 15 | ) 16 | 17 | var ( 18 | serverPort = flag.Int("p", 8972, "server port") 19 | ) 20 | 21 | type Client struct { 22 | conn net.Conn 23 | nick string 24 | readChan chan string 25 | } 26 | 27 | func (c Client) startRecv() { 28 | for msg := range c.readChan { 29 | c.conn.Write([]byte(msg)) 30 | } 31 | } 32 | 33 | type ChatState struct { 34 | listener net.Listener 35 | 36 | clientsLock sync.RWMutex 37 | clients map[net.Conn]*Client 38 | numClients int 39 | } 40 | 41 | var chatState = &ChatState{ 42 | clients: make(map[net.Conn]*Client), 43 | } 44 | 45 | func initChat() { 46 | var err error 47 | chatState.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", *serverPort)) 48 | if err != nil { 49 | fmt.Println("listen error:", err) 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | func handleClient(client *Client) { 55 | // 发送欢迎信息 56 | welcomeMsg := "Welcome Simple Chat! Use /nick to change nick name.\n" 57 | client.conn.Write([]byte(welcomeMsg)) 58 | 59 | buf := make([]byte, 256) 60 | for { 61 | n, err := client.conn.Read(buf) 62 | if err != nil { 63 | fmt.Printf("client left: %s\n", client.conn.RemoteAddr()) 64 | closeClient(client) 65 | return 66 | } 67 | 68 | msg := string(buf[:n]) 69 | msg = strings.TrimSpace(msg) 70 | if len(msg) > 0 && msg[0] == '/' { 71 | // 处理命令 72 | parts := strings.SplitN(msg, " ", 2) 73 | cmd := parts[0] 74 | if cmd == "/nick" && len(parts) > 1 { 75 | if len(parts[1]) > maxNickLen { 76 | client.conn.Write([]byte("nick name too long\n")) 77 | continue 78 | } 79 | client.nick = parts[1] 80 | } 81 | continue 82 | } 83 | 84 | if len(msg) == 0 { 85 | continue 86 | } 87 | if buf[0] == 255 || strings.ToLower(msg) == "quit" { 88 | closeClient(client) 89 | return 90 | } 91 | fmt.Printf("%s: %s\n", client.nick, msg) 92 | 93 | // 将消息转发给其他客户端 94 | chatState.clientsLock.RLock() 95 | for _, cl := range chatState.clients { 96 | if cl != client { 97 | cl.readChan <- ">> " + client.nick + ": " + msg + "\n" 98 | } 99 | } 100 | chatState.clientsLock.RUnlock() 101 | } 102 | } 103 | 104 | func closeClient(client *Client) { 105 | chatState.clientsLock.Lock() 106 | close(client.readChan) 107 | client.conn.Close() 108 | delete(chatState.clients, client.conn) 109 | chatState.numClients-- 110 | chatState.clientsLock.Unlock() 111 | } 112 | 113 | func main() { 114 | flag.Parse() 115 | 116 | initChat() 117 | 118 | for { 119 | conn, err := chatState.listener.Accept() 120 | if err != nil { 121 | fmt.Println("accept error:", err) 122 | continue 123 | } 124 | 125 | client := &Client{conn: conn} 126 | client.nick = fmt.Sprintf("user%d", conn.RemoteAddr().(*net.TCPAddr).Port) 127 | client.readChan = make(chan string, 5) 128 | 129 | chatState.clientsLock.Lock() 130 | if chatState.numClients >= maxClients { 131 | fmt.Printf("too many clients, reject %s\n", conn.RemoteAddr()) 132 | conn.Close() 133 | chatState.clientsLock.Unlock() 134 | continue 135 | } 136 | chatState.clients[conn] = client 137 | chatState.numClients++ 138 | chatState.clientsLock.Unlock() 139 | 140 | go handleClient(client) 141 | go client.startRecv() 142 | fmt.Printf("new client: %s\n", conn.RemoteAddr()) 143 | } 144 | } 145 | --------------------------------------------------------------------------------