Skip to main content

Custom Driver Example: Generic TCP Server Driver

Overview

The Generic TCP Server driver is a simple example driver designed to demonstrate basic TCP server functionality within the HSYCO platform. This driver serves as a starting point for developers who want to create custom TCP-based communication drivers for HSYCO.

Features

  • TCP Server Implementation: Creates a multi-threaded TCP server that listens for incoming connections
  • Configurable Port: Allows configuration of the listening port (default: 25)
  • Thread Pool Management: Supports configurable number of worker threads for handling concurrent connections
  • Connection Handling: Manages multiple simultaneous client connections with automatic cleanup
  • I/O Integration: Integrates with HSYCO's I/O system to expose received data

Configuration

The driver accepts the following configuration parameters in the hsyco.ini file:

Parameters

ParameterTypeDefaultDescription
portinteger25The TCP port number on which the server will listen (1-65535)
threadsinteger4Number of worker threads for handling concurrent connections
note

Make sure the specified port is not already in use by other applications on your system.

Driver Behavior

Server Operations

  1. Initialization: The driver creates a ServerSocket on the specified port and starts the configured number of worker threads
  2. Connection Acceptance: The server continuously listens for incoming TCP connections
  3. Request Processing: Each connection is handled by an available worker thread from the thread pool
  4. Data Processing: When a client connects, the server:
    • Sends an SMTP-style welcome message: 220 [server-name] SMTP
    • Reads one line of data from the client
    • Writes the received line to the HSYCO I/O system as line
    • Closes the connection

Thread Pool Management

  • Worker threads are created during the first server startup
  • Maximum concurrent connections are limited by the number of configured threads
  • Excess connections are automatically rejected and logged
  • Connection overflow protection prevents server resource exhaustion

Datapoints

The driver exposes the following I/O datapoints:

I/O DatapointTypeRead/WriteDescription
linestringRContains the last line received from a TCP client

Logging

The driver provides informational and error logging:

  • Startup: Logs successful server start with port information
  • Connections: Can log connection details (currently commented out)
  • Errors: Logs initialization failures and connection overflow conditions
  • Verbose Mode: Additional diagnostic information when verbose logging is enabled

Java code

/*  HSYCO CONTROLLER, GENERIC SERVER I/O DRIVER CODE, version 3.8.0 Build 0134
* (c) 2015-2025 HSYCO S.r.l.
*
* For information, see the HSYCO web site:
* http://www.hsyco.com/
*
/*------------------------------------------------------------*/

package drivers.generictcpserver;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import com.hsyco.Configuration;
import com.hsyco.driverBase;
import com.hsyco.hsyco;

public class Driver extends driverBase {
public static final int DEFAULTSOCKETPORT = 25;
public static final boolean SHUTDOWNWHENSLAVE = false;
public static final int DEFAULTTHREADS = 4;
public static final int DEFAULTINBOXSIZE = 32;


// driver's private properties
private int port = DEFAULTSOCKETPORT;
private int threads = DEFAULTTHREADS;
private GENERICTcpServer genericserver = null;

// called once every time the driver is started.
// Returning false will fail the initialization procedure, and the driver will not start
public boolean init(String name, HashMap<String, String> config) {

super.init(name); // do not remove

try {
port = Integer.parseInt(config.get("port"));
if (port < 1 || port > 65535) {
port = DEFAULTSOCKETPORT;
}
} catch (Exception e) {}

try {
threads = Integer.parseInt(config.get("threads"));
if (threads < 1) {
port = DEFAULTTHREADS;
}
} catch (Exception e) {}

hsyco.messageLog("Starting GENERIC TCP Server");
try {
genericserver = new GENERICTcpServer(this, port, threads);
genericserver.setDaemon(true);
genericserver.start();
return true;
} catch (Exception e) {
hsyco.errorLog("GENERIC server not started because of: " + e.getClass() + " - " + e.getLocalizedMessage());
}
return false;
}

// called repeatedly while the driver should be alive.
// Returning false will not update the keep-alive timer, causing the driver to quit after a while.
// This call should always return within about 30 seconds, to avoid the keep-alive watch-dog intervention
public boolean loop() {

try { Thread.sleep(5000); } catch (Exception e) {}
return genericserver.isAlive();
}

// called once when the driver quits after it has started correctly.
// Should always return true
public boolean end() {

genericserver.quit();
return true;
}

void ioWriteInternal(String name, String value) {
ioWriteForced(name, value);
}
}


class GENERICTcpServer extends Thread {

static final int MAX_BODY_LINES = 64;
private static boolean processorsInitialized = false;
private int numThreads;
private Driver driver;
private ServerSocket server;
private boolean stop = false;

GENERICTcpServer(Driver driver, int port, int numThreads) throws Exception {

this.driver = driver;
this.server = new ServerSocket(port);
this.numThreads = numThreads;
}

public void run() {

if (!processorsInitialized) {
for (int i = 0; i < numThreads; i++) {
Thread t = new Thread(new RequestProcessor(driver));
t.start();
}
processorsInitialized = true;
}

hsyco.messageLog("GENERIC server accepting connections on port: " + server.getLocalPort());
while (!stop) {
try {
server.setSoTimeout(2000);
Socket request = server.accept();
// hsyco.errorLog("SOCKET ACCEPT: " + request.getInetAddress()); // DIAGNOSTIC
request.setSoTimeout(60000);
RequestProcessor.processRequest(request);
} catch (IOException ex) {
// hsyco.errorLog("EXCEPTION: " + ex); // DIAGNOSTIC
}
}
}

public void quit() {

stop = true;
}
}

class RequestProcessor implements Runnable {
private static List<Socket> pool = new LinkedList<Socket>();
private static int numThreads = 0;
private static int activeConnections = 0;
private static boolean poolOverflow = false;
private Driver driver;

RequestProcessor(Driver driver) {

this.driver = driver;
numThreads++;
}

static class ContentTypeHeader {
String contentType;
String boundary;
String charset;
}

static void processRequest(Socket request) {

synchronized (pool) {
if (activeConnections < numThreads) {
if (poolOverflow) {
poolOverflow = false;
}
pool.add(pool.size(), request);
pool.notify();
} else {
if (!poolOverflow) {
poolOverflow = true;
hsyco.errorLog("GENERIC server error: too many connections [" + activeConnections + "]");
}
try {
request.close();
} catch (Exception e) {}
}
}
}

public void run() {
Thread currentThread = Thread.currentThread();

currentThread.setName(currentThread.getName() + "-genericserver");
while (true) {
Socket connection;
synchronized (pool) {
while (pool.isEmpty()) {
try {
pool.wait();
} catch (InterruptedException ex) {
}
}
try {
connection = pool.remove(0);
} catch (Exception e) {
break;
}
activeConnections++;
}

BufferedOutputStream raw = null;
Writer out = null;
InputStreamReader is = null;
BufferedReader in = null;
String line;

try {
raw = new BufferedOutputStream(connection.getOutputStream());
out = new OutputStreamWriter(raw, "UTF-8");
is = new InputStreamReader(connection.getInputStream(), "UTF-8");
in = new BufferedReader(is);

out.write("220 " + Configuration.publicServerName + " GENERIC\r\n");
out.flush();
line = in.readLine().toLowerCase();
driver.ioWriteInternal("line",line);
} catch (Exception e) {
if (Configuration.verboseLog) {
hsyco.errorLog("GENERIC Server - Generic exception in processor loop: " + e);
}
try {
Thread.sleep((long)(Math.random() * 5000));
} catch (Exception ex) {}
} finally {
// hsyco.errorLog("SOCKET CLOSING: " + connection.socket.getInetAddress() + ":" + connection.socket.getPort()); // DIAGNOSTIC
try { in.close(); } catch (Exception ex) {}
try { is.close(); } catch (Exception ex) {}
try { out.flush(); } catch (Exception ex) {}
try { out.close(); } catch (Exception ex) {}
try { connection.close(); } catch (Exception ex) {}
}
synchronized (pool) {
activeConnections--;
}
}
}
}

The setting.json file

settings.json is a simple text file in JSON format, used to let the Manager’s Settings application know about your custom driver and its configuration options.

settings.json is not strictly needed for the driver to function, but without it you would not be able to use the Manager’s Settings application for HSYCO configuration.

{
"GENERICTCPSERVER": {
"connection": "",
"auth": "no",
"options": {
"port": {
"type": "input",
"format": "[1-9]\\d*",
"key_format": "\\d",
},
"threads": {
"type": "input",
"format": "[1-9]\\d*",
"key_format": "\\d",
},
}
}
}

Implementation Details

Architecture

The driver consists of three main classes:

  1. Driver: Main driver class extending driverBase
  2. GENERICTcpServer: Server thread that accepts incoming connections
  3. RequestProcessor: Worker thread that handles individual client requests

Error Handling

  • Invalid configuration parameters fall back to defaults
  • Socket timeouts prevent hanging connections (60 seconds per connection, 2 seconds for accept)
  • Graceful handling of I/O exceptions
  • Automatic connection cleanup in finally blocks

Usage Examples

Basic TCP Client Test

You can test the driver using a simple telnet connection:

telnet localhost 8080

Expected interaction:

220 your-server-name GENERIC
hello world
[Connection closed by foreign host]

The string "hello world" will be available in HSYCO as the line I/O datapoint.

Troubleshooting

Common Issues

Port Already in Use

Ensure the configured port is not used by other applications. You can check which process is using a port with:

# On Linux/macOS
lsof -i :8080

# On Windows
netstat -ano | findstr :8080

Too Many Connections

If you see overflow errors in the logs, increase the threads parameter in your configuration:

Connection Timeouts

Check network connectivity and firewall settings. The driver uses these timeouts:

  • Connection accept: 2 seconds
  • Client socket: 60 seconds

Diagnostic Logging

Uncomment the diagnostic log lines in the code to enable detailed connection logging:

hsyco.errorLog("SOCKET ACCEPT: " + request.getInetAddress());
hsyco.errorLog("SOCKET CLOSING: " + connection.socket.getInetAddress() + ":" + connection.socket.getPort());
warning

Enable diagnostic logging only during development or troubleshooting.

Code Structure

The main driver file is organized as follows:

Driver.java
├── Driver class (extends driverBase)
│ ├── init() - Driver initialization
│ ├── loop() - Keep-alive loop
│ └── end() - Cleanup on shutdown
├── GENERICTcpServer class (extends Thread)
│ ├── Server socket management
│ └── Connection acceptance loop
└── RequestProcessor class (implements Runnable)
├── Thread pool management
└── Individual request processing