-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1435 from randilt/tcp-chat-server-example
Add a tcp chat server implementation example
- Loading branch information
Showing
4 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"} | ||
] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |