commit 7b1c126acd47955ef5a952c6bf898d0be163bf4a Author: kenneth Date: Sun Dec 24 00:10:41 2023 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd2d928 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# TCP CHAT + ++ `/nick ` 设置自己的昵称 ++ `/join ` 加入聊天房间 ++ `/rooms` 获取聊天房间列表 ++ `/msg ` 发送消息 ++ `/quit` 退出房间 \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..9db2879 --- /dev/null +++ b/client.go @@ -0,0 +1,71 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "strings" +) + +type client struct { + conn net.Conn + nick string + room *room + commands chan<- command +} + +func (c *client) readInput() { + for { + msg, err := bufio.NewReader(c.conn).ReadString('\n') + if err != nil { + return + } + + msg = strings.Trim(msg, "\r\n") + args := strings.Split(msg, " ") + cmd := strings.TrimSpace(args[0]) + + switch cmd { + case "/nick": + c.commands <- command{ + id: CMD_NICK, + client: c, + args: args, + } + case "/join": + c.commands <- command{ + id: CMD_JOIN, + client: c, + args: args, + } + case "/rooms": + c.commands <- command{ + id: CMD_ROOMS, + client: c, + args: args, + } + case "/msg": + c.commands <- command{ + id: CMD_MSG, + client: c, + args: args, + } + case "/quit": + c.commands <- command{ + id: CMD_QUIT, + client: c, + args: args, + } + default: + c.err(fmt.Errorf("unknown command %s", cmd)) + } + } +} + +func (c *client) err(err error) { + c.conn.Write([]byte("ERR: " + err.Error() + "\n")) +} + +func (c *client) msg(msg string) { + c.conn.Write([]byte("> " + msg + "\n")) +} diff --git a/command.go b/command.go new file mode 100644 index 0000000..0ffcdff --- /dev/null +++ b/command.go @@ -0,0 +1,17 @@ +package main + +type commandId int + +const ( + CMD_NICK = iota + CMD_JOIN + CMD_ROOMS + CMD_MSG + CMD_QUIT +) + +type command struct { + id commandId + client *client + args []string +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..92197e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/zhang2092/tcp-chat + +go 1.21.4 diff --git a/main.go b/main.go new file mode 100644 index 0000000..6847b05 --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "log" + "net" +) + +func main() { + s := newServer() + go s.run() + + linstener, err := net.Listen("tcp", ":8888") + if err != nil { + log.Fatalf("failed to start server: %v", err) + } + defer linstener.Close() + log.Println("started server on :8888") + + for { + conn, err := linstener.Accept() + if err != nil { + log.Printf("failed to accept connection: %v", err) + continue + } + + go s.newClient(conn) + } +} diff --git a/room.go b/room.go new file mode 100644 index 0000000..13e444a --- /dev/null +++ b/room.go @@ -0,0 +1,16 @@ +package main + +import "net" + +type room struct { + name string + members map[net.Addr]*client +} + +func (r *room) broadcast(c *client, msg string) { + for addr, m := range r.members { + if addr != c.conn.RemoteAddr() { + m.msg(msg) + } + } +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..b132940 --- /dev/null +++ b/server.go @@ -0,0 +1,109 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net" + "strings" +) + +type server struct { + rooms map[string]*room + commands chan command +} + +func newServer() *server { + return &server{ + rooms: make(map[string]*room), + commands: make(chan command), + } +} + +func (s *server) run() { + for cmd := range s.commands { + switch cmd.id { + case CMD_NICK: + s.nick(cmd.client, cmd.args) + case CMD_JOIN: + s.join(cmd.client, cmd.args) + case CMD_ROOMS: + s.listRooms(cmd.client, cmd.args) + case CMD_MSG: + s.msg(cmd.client, cmd.args) + case CMD_QUIT: + s.quit(cmd.client, cmd.args) + } + } +} + +func (s *server) newClient(conn net.Conn) { + log.Printf("new client has connected: %s", conn.RemoteAddr().String()) + + c := &client{ + conn: conn, + nick: "anonymous", + commands: s.commands, + } + + c.readInput() +} + +func (s *server) nick(c *client, args []string) { + c.nick = args[1] + c.msg(fmt.Sprintf("all right, I will call you %s", c.nick)) +} + +func (s *server) join(c *client, args []string) { + roomName := args[1] + + r, ok := s.rooms[roomName] + if !ok { + r = &room{ + name: roomName, + members: make(map[net.Addr]*client), + } + s.rooms[roomName] = r + } + + r.members[c.conn.RemoteAddr()] = c + + s.quitCurrentRoom(c) + c.room = r + + r.broadcast(c, fmt.Sprintf("%s has joined the room", c.nick)) + c.msg(fmt.Sprintf("Wecome to %s", r.name)) +} + +func (s *server) listRooms(c *client, args []string) { + var rooms []string + for name := range s.rooms { + rooms = append(rooms, name) + } + + c.msg(fmt.Sprintf("available rooms are: %s", strings.Join(rooms, ", "))) +} + +func (s *server) msg(c *client, args []string) { + if c.room == nil { + c.err(errors.New("you must join the room first")) + return + } + + c.room.broadcast(c, strings.Join(args[1:], " ")) +} + +func (s *server) quit(c *client, args []string) { + log.Printf("client has disconnection: %s", c.conn.RemoteAddr().String()) + + s.quitCurrentRoom(c) + c.msg("sad to see you go :(") + c.conn.Close() +} + +func (s *server) quitCurrentRoom(c *client) { + if c.room != nil { + delete(c.room.members, c.conn.RemoteAddr()) + c.room.broadcast(c, fmt.Sprintf("%s has left the room", c.nick)) + } +}