diff --git a/examples/tcp-chat-server/Ballerina.toml b/examples/tcp-chat-server/Ballerina.toml new file mode 100644 index 00000000..f68ada37 --- /dev/null +++ b/examples/tcp-chat-server/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "tcp_chat_server" +version = "0.1.0" +distribution = "2201.10.3" + +[build-options] +observabilityIncluded = true diff --git a/examples/tcp-chat-server/Dependencies.toml b/examples/tcp-chat-server/Dependencies.toml new file mode 100644 index 00000000..6418ca20 --- /dev/null +++ b/examples/tcp-chat-server/Dependencies.toml @@ -0,0 +1,128 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.10.3" + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] +modules = [ + {org = "ballerina", packageName = "lang.string", moduleName = "lang.string"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.3.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "tcp" +version = "1.11.2" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "tcp", moduleName = "tcp"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "wso2" +name = "tcp_chat_server" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "tcp"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "wso2", packageName = "tcp_chat_server", moduleName = "tcp_chat_server"} +] + diff --git a/examples/tcp-chat-server/README.md b/examples/tcp-chat-server/README.md new file mode 100644 index 00000000..636b1759 --- /dev/null +++ b/examples/tcp-chat-server/README.md @@ -0,0 +1,54 @@ +# TCP Chat Server + +[![Star on Github](https://img.shields.io/badge/-Star%20on%20Github-blue?style=social&logo=github)](https://github.com/ballerina-platform/module-ballerina-tcp) + +## Overview + +A simple TCP chat server implementation in Ballerina that allows multiple clients to connect and exchange messages. Each message is broadcasted to all the connected clients with a sequential message number. + +## Features + +- Supports multiple concurrent client connections +- Broadcasts messages to all connected clients +- Sequential message numbering +- Gracefully handles the client closures +- Welcome message for new clients + +## Run the Server + +```sh +# Start the server +$ bal run +``` + +## Connect as Client + +You can connect using either [`telnet`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/telnet) or [`netcat`](https://netcat.sourceforge.net/): + +```sh +# Using telnet +$ telnet localhost 3000 + +# Using netcat +$ nc localhost 3000 +``` + +## Testing + +1. Open multiple terminal windows +2. Start the server in one terminal +3. Connect multiple clients using telnet/netcat in other terminals +4. Type messages in any client terminal and press Enter +5. Observe the broadcast messages in all client terminals + +Each message will be prefixed with a sequential number and broadcast to all connected clients. + +## Implementation Details + +The server uses Ballerina's TCP module to: + +- Listen for incoming connections on port 3000 +- Maintain a map of connected clients +- Buffer incoming messages until newline +- Broadcast messages to all connected clients +- Handle client disconnections diff --git a/examples/tcp-chat-server/chat_service.bal b/examples/tcp-chat-server/chat_service.bal new file mode 100644 index 00000000..143c1bcb --- /dev/null +++ b/examples/tcp-chat-server/chat_service.bal @@ -0,0 +1,93 @@ +// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/lang.'string; +import ballerina/log; +import ballerina/tcp; + +type ChatServer service object { + map clients; + public int messageCount; + remote function onConnect(tcp:Caller caller) returns tcp:ConnectionService|tcp:Error; +}; + +service class ChatServerImpl { + *ChatServer; + map clients = {}; + public int messageCount = 0; + + remote function onConnect(tcp:Caller caller) returns tcp:ConnectionService|tcp:Error { + self.clients[caller.id] = caller; + log:printInfo("New client connected"); + string welcomeMsg = "Welcome!,\r\nSend your first message: \r\n"; + check caller->writeBytes(welcomeMsg.toBytes()); + return new ChatConnectionService(caller.id, self.clients, self); + } +} + +service on new tcp:Listener(3000) { + private final ChatServerImpl chatServer = new; + + remote function onConnect(tcp:Caller caller) returns tcp:ConnectionService|tcp:Error { + return self.chatServer->onConnect(caller); + } +} + +service class ChatConnectionService { + *tcp:ConnectionService; + private final string callerId; + private final map clients; + private final ChatServerImpl parent; + private string messageBuffer = ""; + + public function init(string callerId, map clients, ChatServerImpl parent) { + self.callerId = callerId; + self.clients = clients; + self.parent = parent; + } + + remote function onBytes(readonly & byte[] data) returns tcp:Error? { + string|error message = 'string:fromBytes(data); + if message is error { + return; + } + + self.messageBuffer += message; + if self.messageBuffer.includes("\n") { + string[] messages = re `\r?\n`.split(self.messageBuffer); + self.messageBuffer = messages[messages.length() - 1]; + + foreach string msg in messages.slice(0, messages.length() - 1) { + if msg.trim() != "" { + self.parent.messageCount += 1; + string broadcastMsg = string `Message #${self.parent.messageCount}: ${msg}` + "\r\nNew message:\r\n"; + foreach tcp:Caller caller in self.clients { + check caller->writeBytes(broadcastMsg.toBytes()); + } + } + } + } + } + + remote function onError(tcp:Error err) { + log:printError("Error occurred", err); + } + + remote function onClose() { + _ = self.clients.remove(self.callerId); + log:printInfo("Client disconnected"); + } +}