├── Overall_architecture.png ├── README.md ├── client ├── client.go ├── main.go ├── server ├── server.go └── user.go /Overall_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HOU-SZ/IM_System_Go/414ab07eadb9ce2f64710c0ba2a0f868b2a745f6/Overall_architecture.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IM_System_Go 2 | An instant messaging system using go language 3 | 4 | ## Overall architecture 5 | ![Overall architecture](Overall_architecture.png) 6 | 7 | ## Versions and functions 8 | - Version 1: Basic server 9 | - Version 2: User online and broadcast 10 | - Version 3: User message broadcast 11 | - Version 4: Business decoupling and encapsulation: move user online, offline and send message logic to user.go 12 | - Version 5: Add function to query online users 13 | - Version 6: Add function to change user name 14 | - Version 7: Add function to force user offline after timeout 15 | - Version 8: Support private masseges 16 | - Version 9: Add client module to replace the nc method 17 | 18 | ## To run 19 | 20 | - Build server: 21 | 22 | ```bash 23 | go build -o server server.go main.go user.go 24 | ``` 25 | 26 | - Build client: 27 | 28 | ```bash 29 | go build -o client client.go 30 | ``` 31 | 32 | - Run server: 33 | 34 | ```bash 35 | ./server 36 | ``` 37 | 38 | - Run client: 39 | 40 | ```bash 41 | ./client 42 | ``` -------------------------------------------------------------------------------- /client: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HOU-SZ/IM_System_Go/414ab07eadb9ce2f64710c0ba2a0f868b2a745f6/client -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | ) 10 | 11 | type Client struct { 12 | ServerIp string 13 | ServerPort int 14 | Name string 15 | conn net.Conn 16 | flag int 17 | } 18 | 19 | var serverIp string 20 | var serverPort int 21 | 22 | func NewClient(serverIp string, serverPort int) *Client { 23 | // Creat a new client 24 | client := &Client{ 25 | ServerIp: serverIp, 26 | ServerPort: serverPort, 27 | flag: 999, 28 | } 29 | 30 | // Connect to the server 31 | conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort)) 32 | if err != nil { 33 | fmt.Println("net.Dial error: ", err) 34 | return nil 35 | } 36 | 37 | client.conn = conn 38 | return client 39 | } 40 | 41 | func (client *Client) UpdateName() bool { 42 | fmt.Println(">>>>>Please input your name:") 43 | fmt.Scanln(&client.Name) 44 | 45 | sendMsg := "rename|" + client.Name + "\n" 46 | _, err := client.conn.Write([]byte(sendMsg)) 47 | if err != nil { 48 | fmt.Println("conn.Write error: ", err) 49 | return false 50 | } 51 | 52 | return true 53 | } 54 | 55 | func (client *Client) PublicMessage() { 56 | var chatMsg string 57 | 58 | fmt.Println(">>>>Please input message, or exit by input \"exit\".") 59 | fmt.Scanln(&chatMsg) 60 | 61 | for chatMsg != "exit" { 62 | if len(chatMsg) != 0 { 63 | _, err := client.conn.Write([]byte(chatMsg + "\n")) 64 | if err != nil { 65 | fmt.Println("conn.Write error: ", err) 66 | break 67 | } 68 | } 69 | 70 | chatMsg = "" 71 | fmt.Println(">>>>Please input message, or exit by input \"exit\".") 72 | fmt.Scanln(&chatMsg) 73 | } 74 | } 75 | 76 | func (client *Client) QueryOnlineUsers() { 77 | sendMsg := "who\n" 78 | _, err := client.conn.Write([]byte(sendMsg)) 79 | if err != nil { 80 | fmt.Println("conn.Write error: ", err) 81 | return 82 | } 83 | } 84 | 85 | func (client *Client) PrivateMessage() { 86 | var remoteName string 87 | var chatMsg string 88 | 89 | client.QueryOnlineUsers() 90 | fmt.Println(">>>>Please input user name, or exit by input \"exit\".") 91 | fmt.Scanln(&remoteName) 92 | 93 | for remoteName != "exit" { 94 | fmt.Println(">>>>Please input message, or exit by input \"exit\".") 95 | fmt.Scanln(&chatMsg) 96 | 97 | for chatMsg != "exit" { 98 | if len(chatMsg) != 0 { 99 | sendMsg := "to|" + remoteName + "|" + chatMsg + "\n\n" 100 | _, err := client.conn.Write([]byte(sendMsg)) 101 | if err != nil { 102 | fmt.Println("conn.Write error: ", err) 103 | break 104 | } 105 | } 106 | 107 | chatMsg = "" 108 | fmt.Println(">>>>Please input message, or exit by input \"exit\".") 109 | fmt.Scanln(&chatMsg) 110 | } 111 | 112 | client.QueryOnlineUsers() 113 | fmt.Println(">>>>Please input user name, or exit by input \"exit\".") 114 | fmt.Scanln(&remoteName) 115 | } 116 | } 117 | 118 | func (client *Client) DealResponse() { 119 | // Once the are some message in client.conn, copy it to stdout, block and listen forever 120 | io.Copy(os.Stdout, client.conn) 121 | 122 | // Equivalent to the following code 123 | // buffer := make([]byte, 4096) 124 | // client.conn.Read(buffer) 125 | // fmt.Println(buffer) 126 | } 127 | 128 | func (client *Client) menu() bool { 129 | var flag int 130 | 131 | fmt.Println("1. Send public message") 132 | fmt.Println("2. Send private message") 133 | fmt.Println("3. Update user name") 134 | fmt.Println("0. Exit") 135 | 136 | fmt.Scanln(&flag) 137 | 138 | if flag >= 0 && flag <= 3 { 139 | client.flag = flag 140 | return true 141 | } else { 142 | fmt.Println(">>>>>Please input a valid number<<<<<") 143 | return false 144 | } 145 | } 146 | 147 | func (client *Client) Run() { 148 | // If the flag number != 0, go into the loop, else exit 149 | for client.flag != 0 { 150 | // Loop until recieve a valid flag number 151 | for client.menu() != true { 152 | continue 153 | } 154 | 155 | switch client.flag { 156 | case 1: 157 | client.PublicMessage() 158 | break 159 | case 2: 160 | client.PrivateMessage() 161 | break 162 | case 3: 163 | client.UpdateName() 164 | break 165 | } 166 | } 167 | } 168 | 169 | func init() { 170 | flag.StringVar(&serverIp, "ip", "127.0.0.1", "Server ip address") 171 | flag.IntVar(&serverPort, "port", 8888, "Server port") 172 | } 173 | 174 | func main() { 175 | flag.Parse() 176 | client := NewClient(serverIp, serverPort) 177 | if client == nil { 178 | fmt.Println("Connect to server failed...") 179 | return 180 | } 181 | 182 | // Create a goroutine to process the message from server 183 | go client.DealResponse() 184 | 185 | fmt.Println("Connect to server success...") 186 | 187 | // todo 188 | client.Run() 189 | } 190 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | server := NewServer("127.0.0.1", 8888) 5 | server.Start() 6 | } 7 | -------------------------------------------------------------------------------- /server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HOU-SZ/IM_System_Go/414ab07eadb9ce2f64710c0ba2a0f868b2a745f6/server -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type Server struct { 12 | Ip string 13 | Port int 14 | 15 | // A map to store current online users 16 | OnlineMap map[string]*User 17 | mapLock sync.RWMutex 18 | 19 | // A channel to broadcast messages 20 | Message chan string 21 | } 22 | 23 | // Create a new server 24 | func NewServer(ip string, port int) *Server { 25 | server := &Server{ 26 | Ip: ip, 27 | Port: port, 28 | OnlineMap: make(map[string]*User), 29 | Message: make(chan string), 30 | } 31 | return server 32 | } 33 | 34 | func (this *Server) Start() { 35 | // Socket listen 36 | listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port)) 37 | if err != nil { 38 | fmt.Println("net.Listen error: ", err) 39 | return 40 | } else { 41 | fmt.Println("net.Listen success.") 42 | } 43 | 44 | // Finally close listener 45 | defer listener.Close() 46 | 47 | // When starting, start the goroutine to monitor Message channel 48 | go this.ListenMessage() 49 | 50 | // Monitor connections 51 | for { 52 | // Accept connection 53 | conn, err := listener.Accept() 54 | if err != nil { 55 | fmt.Println("Listener accpet error: ", err) 56 | continue 57 | } 58 | fmt.Println("Listener accept success.") 59 | go this.Handler(conn) 60 | 61 | } 62 | } 63 | 64 | // Func to monitor current Message channel, once there are any messages in the Channel, send it to all online users 65 | func (this *Server) ListenMessage() { 66 | for { 67 | msg := <-this.Message 68 | 69 | this.mapLock.Lock() 70 | for _, cli := range this.OnlineMap { 71 | cli.C <- msg 72 | } 73 | this.mapLock.Unlock() 74 | } 75 | } 76 | 77 | func (this *Server) Handler(conn net.Conn) { 78 | fmt.Println("Connection create success.") 79 | 80 | user := NewUser(conn, this) 81 | 82 | // Broadcast user online message 83 | user.Online() 84 | 85 | // A channel to monitor whether the user is alive 86 | isLive := make(chan bool) 87 | 88 | // Recieve the messages sent by user and broadcast to all users 89 | go func() { 90 | buffer := make([]byte, 4096) 91 | for { 92 | n, err := conn.Read(buffer) 93 | if n == 0 { 94 | user.Offline() 95 | return 96 | } 97 | if err != nil && err != io.EOF { 98 | fmt.Println("Conn read err: ", err) 99 | return 100 | } 101 | 102 | // Extract the message from user (remove "\n") 103 | msg := string(buffer[:n-1]) 104 | 105 | // Broadcast the message 106 | user.DoMessage(msg) 107 | 108 | // Any message sent by user means the user is alive 109 | isLive <- true 110 | } 111 | }() 112 | 113 | // Keep current goroutine alive 114 | for { 115 | select { 116 | case <-isLive: 117 | // The user is alive, should reset timer 118 | // Do nothing, to activate the select, update the timer 119 | case <-time.After(time.Second * 300): 120 | // Timeout, should force close the user 121 | user.sendMsg("You are timeout!") 122 | // isLive <- false 123 | close(user.C) 124 | // user.Offline() 125 | conn.Close() 126 | return 127 | } 128 | } 129 | 130 | } 131 | 132 | // Func to broadcast the user online message to Message Channel 133 | func (this *Server) Broadcast(user *User, msg string) { 134 | sendMessage := "[" + user.Addr + "]" + user.Name + ":" + msg 135 | 136 | this.Message <- sendMessage 137 | } 138 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | type User struct { 9 | Name string 10 | Addr string 11 | C chan string 12 | conn net.Conn 13 | 14 | server *Server 15 | } 16 | 17 | //Create a new user 18 | func NewUser(conn net.Conn, server *Server) *User { 19 | userAddr := conn.RemoteAddr().String() 20 | 21 | user := &User{ 22 | Name: userAddr, 23 | Addr: userAddr, 24 | C: make(chan string), 25 | conn: conn, 26 | server: server, 27 | } 28 | 29 | // start the goroutine to monitor current channel 30 | go user.ListenMessage() 31 | 32 | return user 33 | } 34 | 35 | // func to monitor current user channel, once there are any message in the channel, send the message to client 36 | func (this *User) ListenMessage() { 37 | for { 38 | msg := <-this.C 39 | this.conn.Write([]byte(msg + "\n")) 40 | } 41 | } 42 | 43 | func (this *User) Online() { 44 | // Add the user to server's OnlineMap 45 | this.server.mapLock.Lock() 46 | this.server.OnlineMap[this.Name] = this 47 | this.server.mapLock.Unlock() 48 | 49 | // Broadcast the user online message to server's Message Channel 50 | this.server.Broadcast(this, "online") 51 | } 52 | 53 | func (this *User) Offline() { 54 | // Delete the user from server's OnlineMap 55 | this.server.mapLock.Lock() 56 | delete(this.server.OnlineMap, this.Name) 57 | this.server.mapLock.Unlock() 58 | 59 | // Broadcast the user offline message to server's Message Channel 60 | this.server.Broadcast(this, "offline") 61 | } 62 | 63 | func (this *User) DoMessage(msg string) { 64 | if msg == "who" { 65 | // Query online users 66 | this.server.mapLock.Lock() 67 | for _, user := range this.server.OnlineMap { 68 | onlineMsg := "[" + user.Addr + "]" + user.Name + ": " + "is online\n" 69 | this.sendMsg(onlineMsg) 70 | } 71 | this.server.mapLock.Unlock() 72 | 73 | } else if len(msg) > 7 && msg[:7] == "rename|" { 74 | // Change user name 75 | newName := strings.Split(msg, "|")[1] 76 | // Check if the new name exist already 77 | _, ok := this.server.OnlineMap[newName] 78 | if ok { 79 | this.sendMsg("The new user name has already exist.\n") 80 | } else { 81 | this.server.mapLock.Lock() 82 | delete(this.server.OnlineMap, this.Name) 83 | this.Name = newName 84 | this.server.OnlineMap[this.Name] = this 85 | this.server.mapLock.Unlock() 86 | 87 | this.sendMsg("Your user name has updated as: " + this.Name + "\n") 88 | } 89 | 90 | } else if len(msg) > 4 && msg[:3] == "to|" { 91 | // Private Message 92 | if len(strings.Split(msg, "|")) != 3 { 93 | this.sendMsg("The message format is not correct, please use the foramt like: \"to|shizheng|I love you\" .\n") 94 | return 95 | } 96 | remoteName := strings.Split(msg, "|")[1] 97 | if remoteName == "" { 98 | this.sendMsg("The user name cannot be empty.\n") 99 | return 100 | } 101 | 102 | remoteUser, ok := this.server.OnlineMap[remoteName] 103 | if !ok { 104 | this.sendMsg("The user dosn't exist.\n") 105 | return 106 | } 107 | content := strings.Split(msg, "|")[2] 108 | if content == "" { 109 | this.sendMsg("The message cannot be empty.\n") 110 | return 111 | } 112 | 113 | remoteUser.sendMsg(this.Name + " to you: " + content + "\n") 114 | 115 | } else { 116 | this.server.Broadcast(this, msg) 117 | } 118 | } 119 | 120 | func (this *User) sendMsg(msg string) { 121 | this.conn.Write([]byte(msg)) 122 | } 123 | --------------------------------------------------------------------------------