Skip to content

Commit ec8c867

Browse files
committed
Add solution for Challenge 8 by odelbos
1 parent 4befc9a commit ec8c867

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package challenge8
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"sync"
7+
)
8+
9+
// Common errors that can be returned by the Chat Server
10+
var (
11+
ErrUsernameAlreadyTaken = errors.New("username already taken")
12+
ErrRecipientNotFound = errors.New("recipient not found")
13+
ErrClientDisconnected = errors.New("client disconnected")
14+
)
15+
16+
// Client represents a connected chat client
17+
type Client struct {
18+
username string
19+
incoming chan string
20+
outgoing chan string
21+
disconnect chan struct{}
22+
disconnected bool
23+
mu sync.RWMutex
24+
}
25+
26+
// Send sends a message to the client (non-blocking)
27+
func (c *Client) Send(message string) {
28+
if c.disconnected {
29+
return
30+
}
31+
32+
c.mu.RLock()
33+
defer c.mu.RUnlock()
34+
35+
select {
36+
case c.incoming <- message:
37+
default:
38+
// Do not block
39+
}
40+
}
41+
42+
// Receive returns the next message for the client (blocking)
43+
func (c *Client) Receive() string {
44+
if msg, ok := <-c.incoming; ok {
45+
return msg
46+
}
47+
return ""
48+
}
49+
50+
func (c *Client) do_disconnect() {
51+
if c.disconnected {
52+
return
53+
}
54+
55+
c.mu.Lock()
56+
defer c.mu.Unlock()
57+
58+
close(c.incoming)
59+
close(c.disconnect)
60+
c.disconnected = true
61+
}
62+
63+
// ChatServer manages client connections and message routing
64+
type ChatServer struct {
65+
clients map[string]*Client
66+
mu sync.RWMutex
67+
}
68+
69+
// NewChatServer creates a new chat server instance
70+
func NewChatServer() *ChatServer {
71+
return &ChatServer{clients: make(map[string]*Client)}
72+
}
73+
74+
// Connect adds a new client to the chat server
75+
func (s *ChatServer) Connect(username string) (*Client, error) {
76+
s.mu.Lock()
77+
defer s.mu.Unlock()
78+
79+
if _, ok := s.clients[username]; ok {
80+
return nil, ErrUsernameAlreadyTaken
81+
}
82+
83+
client := &Client{
84+
username: username,
85+
incoming: make(chan string, 100),
86+
outgoing: make(chan string, 100),
87+
disconnect: make(chan struct{}),
88+
}
89+
s.clients[username] = client
90+
91+
go s.handleClient(client)
92+
93+
return client, nil
94+
}
95+
96+
// Disconnect removes a client from the chat server
97+
func (s *ChatServer) Disconnect(client *Client) {
98+
s.mu.Lock()
99+
defer s.mu.Unlock()
100+
101+
if _, ok := s.clients[client.username]; ! ok {
102+
return
103+
}
104+
105+
client.do_disconnect()
106+
delete(s.clients, client.username)
107+
}
108+
109+
// Broadcast sends a message to all connected clients
110+
func (s *ChatServer) Broadcast(sender *Client, message string) {
111+
s.mu.RLock()
112+
defer s.mu.RUnlock()
113+
114+
msg := fmt.Sprintf("%s: %s", sender.username, message)
115+
for _, client := range(s.clients) {
116+
if client.username != sender.username {
117+
client.Send(msg)
118+
}
119+
}
120+
}
121+
122+
// PrivateMessage sends a message to a specific client
123+
func (s *ChatServer) PrivateMessage(sender *Client, recipient string, message string) error {
124+
if sender.disconnected {
125+
return ErrClientDisconnected
126+
}
127+
128+
s.mu.RLock()
129+
defer s.mu.RUnlock()
130+
131+
target, ok := s.clients[recipient]
132+
if ! ok {
133+
return ErrRecipientNotFound
134+
}
135+
if target.disconnected {
136+
return ErrClientDisconnected
137+
}
138+
139+
msg := fmt.Sprintf("(pm) %s: %s", sender.username, message)
140+
target.Send(msg)
141+
return nil
142+
}
143+
144+
// handleClient processes outgoing messages and disconnection for a client
145+
func (s *ChatServer) handleClient(client *Client) {
146+
for {
147+
select {
148+
case msg := <-client.outgoing:
149+
s.Broadcast(client, msg)
150+
case <-client.disconnect:
151+
s.Disconnect(client)
152+
return
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)