Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a tcp chat server implementation example #1435

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions examples/tcp-chat-server/.gitignore
randilt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Ballerina generates this directory during the compilation of a package.
# It contains compiler-generated artifacts and the final executable if this is an application package.
target/

# Ballerina maintains the compiler-generated source code here.
# Remove this if you want to commit generated sources.
generated/

# Contains configuration values used during development time.
# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details.
Config.toml
2 changes: 2 additions & 0 deletions examples/tcp-chat-server/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build-options]
randilt marked this conversation as resolved.
Show resolved Hide resolved
observabilityIncluded = true
117 changes: 117 additions & 0 deletions examples/tcp-chat-server/Dependencies.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 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"}
]
modules = [
{org = "ballerina", packageName = "io", moduleName = "io"}
]

[[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 = "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 = "user"
name = "tcp_chat_server"
version = "0.1.0"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "lang.string"},
{org = "ballerina", name = "tcp"},
{org = "ballerinai", name = "observe"}
]
modules = [
{org = "user", 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 broadcast to all connected clients with a sequential message number.
randilt marked this conversation as resolved.
Show resolved Hide resolved

## Features

- Supports multiple concurrent client connections
- Broadcasts messages to all connected clients
- Sequential message numbering
- Handles client disconnections gracefully
randilt marked this conversation as resolved.
Show resolved Hide resolved
- Welcome message for new clients

## Run the Server

```sh
# Start the server
$ bal run
```

## Connect as Client

You can connect using either telnet or netcat:
randilt marked this conversation as resolved.
Show resolved Hide resolved

```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
77 changes: 77 additions & 0 deletions examples/tcp-chat-server/main.bal
randilt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import ballerina/io;
randilt marked this conversation as resolved.
Show resolved Hide resolved
import ballerina/tcp;
import ballerina/lang.'string;
Nuvindu marked this conversation as resolved.
Show resolved Hide resolved

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;
io:println("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) {
io:println("Error occurred: ", err.message());
}

remote function onClose() {
_ = self.clients.remove(self.callerId);
io:println("Client disconnected");
}
}
Loading