Skip to content

Commit

Permalink
Merge pull request #1435 from randilt/tcp-chat-server-example
Browse files Browse the repository at this point in the history
Add a tcp chat server implementation example
  • Loading branch information
DimuthuMadushan authored Jan 10, 2025
2 parents 191424e + c1457f4 commit fde3fe6
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 0 deletions.
8 changes: 8 additions & 0 deletions examples/tcp-chat-server/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
org = "wso2"
name = "tcp_chat_server"
version = "0.1.0"
distribution = "2201.10.3"

[build-options]
observabilityIncluded = true
128 changes: 128 additions & 0 deletions examples/tcp-chat-server/Dependencies.toml
Original file line number Diff line number Diff line change
@@ -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"}
]

54 changes: 54 additions & 0 deletions examples/tcp-chat-server/README.md
Original file line number Diff line number Diff line change
@@ -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
93 changes: 93 additions & 0 deletions examples/tcp-chat-server/chat_service.bal
Original file line number Diff line number Diff line change
@@ -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<tcp:Caller> clients;
public int messageCount;
remote function onConnect(tcp:Caller caller) returns tcp:ConnectionService|tcp:Error;
};

service class ChatServerImpl {
*ChatServer;
map<tcp:Caller> 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<tcp:Caller> clients;
private final ChatServerImpl parent;
private string messageBuffer = "";

public function init(string callerId, map<tcp:Caller> 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");
}
}

0 comments on commit fde3fe6

Please sign in to comment.