Multi-language Client Implementation
Below are WebSocket client implementation examples for various programming languages to connect and call iClick API.
Note
All examples implement the same functionality:
- WebSocket connection and auto-reconnect
- Request/response matching (based on
evtid) - Timeout handling
- Binary data support
- Event push handling
Warning
The following code examples may not be fully verified and are for reference and learning purposes only. Please ensure thorough testing and validation before using in production environments.
javascript
const wserverPort = 23188
const wsevents = {}
let wsclient = null
let reconnectTimer = null
export function reconnect() {
if( reconnectTimer || wsclient?.readyState === WebSocket.CONNECTING ){
return
}
reconnectTimer = setTimeout(() => {
console.log('API service reconnecting...')
connectServer().finally(() => reconnectTimer = null)
}, 1000 * 3)
}
export function connectServer() {
return new Promise((_resolve) => {
if( wsclient ){
return
}
wsclient = new WebSocket(`ws://127.0.0.1:${wserverPort}`)
wsclient.binaryType = 'arraybuffer'
wsclient.onopen = () => {
console.log('API service connected successfully')
_resolve()
}
wsclient.onmessage = (_message) => {
let _payload = _message.data,
_bindata = null
if( typeof _payload === 'string' ){
_payload = JSON.parse(_payload)
}else{
// Some interfaces return binary data, need to parse [metaDataLength][metaData][binary]
const _decoder = new TextDecoder('utf-8')
const _metaLength = _decoder.decode(_payload.slice(0, 6))
_bindata = _payload.slice(6 + parseInt(_metaLength))
_payload = JSON.parse(_decoder.decode(_payload.slice(6, 6 + parseInt(_metaLength))))
_payload.data = _bindata
_bindata = null
}
const _eventId = _payload.evtid
const _event = wsevents[ _eventId ]
if( _event ){
if( _payload.type === 'error' ){
_event.reject(new Error(_payload.error))
}else{
_event.resolve( _payload.data )
}
delete wsevents[ _eventId ]
}else if( _payload.event ){
// You can dispatch events according to different languages
}
}
wsclient.onclose = (code) => {
wsclient = null
console.log('API service connection closed:', code)
reconnect()
}
wsclient.onerror = (err) => {
console.error('API service connection error:', err)
}
})
}
export function apiInvoke(type, _params = {}, _timeout = 18) {
return new Promise((resolve, reject) => {
if( !wsclient || wsclient.readyState !== WebSocket.OPEN ){
reject( new Error('API service connection not ready') )
}else{
const _eventId = Math.random().toString(36).substring(2, 12)
const _payload = {
..._params,
type,
evtid: _eventId,
timeout: _timeout
}
wsevents[_payload.evtid] = { resolve, reject }
wsclient.send(JSON.stringify(_payload))
if( _timeout > 0 ){
setTimeout(() => {
reject(new Error(`API service: ${_payload.evtid} Invoke Timeout !`))
delete wsevents[_payload.evtid]
}, _timeout * 1000)
}
}
})
}
// Usage example
await connectServer()
const result = await apiInvoke('click', { x: 100, y: 200, deviceId: 'XXXXXXXXXX' })javascript
const WebSocket = require('ws')
const wserverPort = 23188
const wsevents = {}
let wsclient = null
let reconnectTimer = null
function reconnect() {
if( reconnectTimer || (wsclient && wsclient.readyState === WebSocket.CONNECTING) ){
return
}
reconnectTimer = setTimeout(() => {
console.log('API service reconnecting...')
connectServer().finally(() => reconnectTimer = null)
}, 1000 * 3)
}
function connectServer() {
return new Promise((_resolve, _reject) => {
if( wsclient ){
return _resolve()
}
wsclient = new WebSocket(`ws://127.0.0.1:${wserverPort}`)
wsclient.on('open', () => {
console.log('API service connected successfully')
_resolve()
})
wsclient.on('message', (_data) => {
let _payload = null,
_bindata = null
try {
// Node.js ws library always receives messages as Buffer type
// Check if the first byte is { or [ (JSON format)
const _firstByte = _data[0]
const _isJson = _firstByte === 0x7B || _firstByte === 0x5B // { or [
if (_isJson) {
// Direct JSON string
_payload = JSON.parse(_data.toString('utf-8'))
} else {
// Binary format: [metaDataLength][metaData][binary]
const _metaLength = _data.slice(0, 6).toString('utf-8').trim()
const _metaLengthInt = parseInt(_metaLength, 10)
_bindata = _data.slice(6 + _metaLengthInt)
const _metaJson = _data.slice(6, 6 + _metaLengthInt).toString('utf-8')
_payload = JSON.parse(_metaJson)
_payload.data = _bindata
_bindata = null
}
} catch (error) {
console.error('Parse message failed:', error.message)
return
}
const _eventId = _payload.evtid
const _event = wsevents[ _eventId ]
if( _event ){
if( _payload.type === 'error' ){
_event.reject(new Error(_payload.error))
}else{
_event.resolve( _payload.data )
}
delete wsevents[ _eventId ]
}else if( _payload.event ){
// You can dispatch events according to different languages
console.log('Received event push:', _payload.event, _payload.data)
}
})
wsclient.on('close', (code) => {
wsclient = null
console.log('API service connection closed:', code)
reconnect()
})
wsclient.on('error', (err) => {
console.error('API service connection error:', err.message)
_reject(err)
})
})
}
function apiInvoke(type, _params = {}, _timeout = 18) {
return new Promise((resolve, reject) => {
if( !wsclient || wsclient.readyState !== WebSocket.OPEN ){
reject( new Error('API service connection not ready') )
}else{
const _eventId = Math.random().toString(36).substring(2, 12)
const _payload = {
..._params,
type,
evtid: _eventId,
timeout: _timeout
}
wsevents[_payload.evtid] = { resolve, reject }
wsclient.send(JSON.stringify(_payload))
if( _timeout > 0 ){
setTimeout(() => {
if( wsevents[_payload.evtid] ){
reject(new Error(`API service: ${_payload.evtid} Invoke Timeout !`))
delete wsevents[_payload.evtid]
}
}, _timeout * 1000)
}
}
})
}
// Usage example
async function main() {
await connectServer()
try {
// Get device list
const devices = await apiInvoke('getDevices')
console.log('Device list:', devices)
// Click operation
if(devices && devices.length > 0) {
const result = await apiInvoke('click', {
x: 100,
y: 200,
deviceId: devices[0].id
})
console.log('Click result:', result)
}
} catch(error) {
console.error('Call failed:', error.message)
}
}
main()python
import asyncio
import websockets
import json
import time
from typing import Dict, Any, Optional
class IClickClient:
def __init__(self, port: int = 23188):
self.port = port
self.ws: Optional[websockets.WebSocketClientProtocol] = None
self.events: Dict[str, asyncio.Future] = {}
self.is_reconnecting = False
async def connect(self):
"""Connect to WebSocket server"""
try:
self.ws = await websockets.connect(f'ws://127.0.0.1:{self.port}')
print('API service connected successfully')
asyncio.create_task(self._message_handler())
except Exception as e:
print(f'API service connection error: {e}')
await self.reconnect()
async def reconnect(self):
"""Reconnect"""
if self.is_reconnecting:
return
self.is_reconnecting = True
print('API service reconnecting...')
await asyncio.sleep(3)
await self.connect()
self.is_reconnecting = False
async def _message_handler(self):
"""Handle received messages"""
try:
async for message in self.ws:
payload = self._parse_message(message)
self._handle_response(payload)
except websockets.exceptions.ConnectionClosed:
print('API service connection closed')
await self.reconnect()
except Exception as e:
print(f'Message processing error: {e}')
def _parse_message(self, message) -> dict:
"""Parse message"""
if isinstance(message, str):
return json.loads(message)
# Binary format response
meta_length = int(message[:6].decode('utf-8'))
meta_data = json.loads(message[6:6+meta_length].decode('utf-8'))
meta_data['data'] = message[6+meta_length:]
return meta_data
def _handle_response(self, payload: dict):
"""Handle response"""
evtid = payload.get('evtid')
if evtid and evtid in self.events:
future = self.events.pop(evtid)
if not future.done():
if payload.get('type') == 'error':
future.set_exception(Exception(payload.get('error', 'Unknown error')))
else:
future.set_result(payload.get('data'))
def _generate_event_id(self) -> str:
"""Generate random event ID"""
return f"{int(time.time() * 1000000) % 1000000000:09d}{id(self) % 1000:03d}"
async def invoke(self, api_type: str, params: Dict[str, Any] = None, timeout: int = 18) -> Any:
"""Invoke API"""
if not self.ws or self.ws.close_code is not None:
raise Exception('API service connection not ready')
evtid = self._generate_event_id()
payload = {
'type': api_type,
'evtid': evtid,
'timeout': timeout,
**(params or {})
}
future = asyncio.Future()
self.events[evtid] = future
await self.ws.send(json.dumps(payload))
try:
return await asyncio.wait_for(future, timeout=timeout + 1)
except asyncio.TimeoutError:
self.events.pop(evtid, None)
raise Exception(f'API call timeout: {api_type}')
async def disconnect(self):
"""Disconnect"""
if self.ws:
await self.ws.close()
self.ws = None
# Usage example
async def main():
client = IClickClient()
await client.connect()
try:
# Get device list
devices = await client.invoke('getDevices')
print(f'Device list: {devices}')
else:
print('No available devices')
finally:
await client.disconnect()
if __name__ == '__main__':
asyncio.run(main())java
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.security.SecureRandom;
public class IClickClient {
private static final int PORT = 23188;
private static final SecureRandom RANDOM = new SecureRandom();
private static final String CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
private WebSocketClient wsClient;
private final ConcurrentHashMap<String, CompletableFuture<Object>> events = new ConcurrentHashMap<>();
private final Gson gson = new Gson();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicBoolean isReconnecting = new AtomicBoolean(false);
public synchronized void connect() throws Exception {
// Close old connection
if (wsClient != null) {
wsClient.close();
}
URI uri = new URI("ws://127.0.0.1:" + PORT);
wsClient = new WebSocketClient(uri) {
@Override
public void onOpen(ServerHandshake handshake) {
System.out.println("API service connected successfully");
isReconnecting.set(false);
}
@Override
public void onMessage(String message) {
handleMessage(gson.fromJson(message, JsonObject.class));
}
@Override
public void onMessage(ByteBuffer bytes) {
byte[] data = bytes.array();
int metaLength = Integer.parseInt(new String(data, 0, 6, StandardCharsets.UTF_8).trim());
String metaJson = new String(data, 6, metaLength, StandardCharsets.UTF_8);
handleMessage(gson.fromJson(metaJson, JsonObject.class));
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("API service connection closed: " + code);
reconnect();
}
@Override
public void onError(Exception ex) {
System.err.println("API service connection error: " + ex.getMessage());
}
};
wsClient.connect();
}
private void handleMessage(JsonObject payload) {
if (!payload.has("evtid")) return;
String evtid = payload.get("evtid").getAsString();
CompletableFuture<Object> future = events.remove(evtid);
if (future != null) {
if (payload.has("type") && "error".equals(payload.get("type").getAsString())) {
future.completeExceptionally(new Exception(payload.get("error").getAsString()));
} else {
future.complete(payload.get("data"));
}
}
}
private void reconnect() {
if (!isReconnecting.compareAndSet(false, true)) return;
scheduler.schedule(() -> {
try {
System.out.println("API service reconnecting...");
connect();
} catch (Exception e) {
System.err.println("Reconnection failed: " + e.getMessage());
isReconnecting.set(false);
}
}, 3, TimeUnit.SECONDS);
}
private static String generateEventId() {
StringBuilder sb = new StringBuilder(10);
for (int i = 0; i < 10; i++) {
sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length())));
}
return sb.toString();
}
public CompletableFuture<Object> invoke(String type, Map<String, Object> params, int timeout) {
CompletableFuture<Object> future = new CompletableFuture<>();
if (wsClient == null || !wsClient.isOpen()) {
future.completeExceptionally(new Exception("API service connection not ready"));
return future;
}
String evtid = generateEventId();
Map<String, Object> payload = new HashMap<>(params != null ? params : new HashMap<>());
payload.put("type", type);
payload.put("evtid", evtid);
payload.put("timeout", timeout);
events.put(evtid, future);
wsClient.send(gson.toJson(payload));
// Set timeout
scheduler.schedule(() -> {
CompletableFuture<Object> f = events.remove(evtid);
if (f != null) {
f.completeExceptionally(new TimeoutException("API service timeout"));
}
}, timeout, TimeUnit.SECONDS);
return future;
}
public void close() {
scheduler.shutdown();
if (wsClient != null) {
wsClient.close();
}
}
// Usage example
public static void main(String[] args) throws Exception {
IClickClient client = new IClickClient();
client.connect();
Thread.sleep(1000);
Map<String, Object> params = new HashMap<>();
params.put("x", 100);
params.put("y", 200);
params.put("deviceId", "XXXXXXXXXX");
client.invoke("click", params, 18)
.thenAccept(result -> System.out.println("Click result: " + result))
.exceptionally(ex -> {
System.err.println("Call failed: " + ex.getMessage());
return null;
})
.thenRun(client::close);
}
}csharp
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class IClickClient
{
private const int Port = 23188;
private ClientWebSocket wsClient;
private ConcurrentDictionary<string, TaskCompletionSource<object>> events = new();
private CancellationTokenSource cts;
private readonly SemaphoreSlim reconnectLock = new(1, 1);
private bool isReconnecting = false;
public async Task ConnectAsync()
{
// Clean up old connection
if (wsClient != null)
{
try { await wsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "Reconnect", CancellationToken.None); }
catch { }
wsClient?.Dispose();
}
cts?.Cancel();
cts?.Dispose();
cts = new CancellationTokenSource();
wsClient = new ClientWebSocket();
await wsClient.ConnectAsync(new Uri($"ws://127.0.0.1:{Port}"), cts.Token);
Console.WriteLine("API service connected successfully");
_ = Task.Run(MessageHandler);
}
private async Task MessageHandler()
{
var messageBuffer = new List<byte>();
var buffer = new byte[8192]; // Increase buffer size
try
{
while (wsClient.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
{
var result = await wsClient.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
// Handle fragmented messages
messageBuffer.AddRange(new ArraySegment<byte>(buffer, 0, result.Count));
if (result.EndOfMessage)
{
if (result.MessageType == WebSocketMessageType.Text)
{
string message = Encoding.UTF8.GetString(messageBuffer.ToArray());
HandleJsonMessage(message);
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
HandleBinaryMessage(messageBuffer.ToArray());
}
else if (result.MessageType == WebSocketMessageType.Close)
{
Console.WriteLine("API service connection closed");
await ReconnectAsync();
break;
}
messageBuffer.Clear();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"API service connection error: {ex.Message}");
await ReconnectAsync();
}
}
private void HandleJsonMessage(string message)
{
var payload = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(message);
ProcessPayload(payload);
}
private void HandleBinaryMessage(byte[] data)
{
string metaLengthStr = Encoding.UTF8.GetString(data, 0, 6);
int metaLength = int.Parse(metaLengthStr.Trim());
string metaJson = Encoding.UTF8.GetString(data, 6, metaLength);
var payload = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(metaJson);
byte[] binaryData = new byte[data.Length - 6 - metaLength];
Array.Copy(data, 6 + metaLength, binaryData, 0, binaryData.Length);
ProcessPayload(payload, binaryData);
}
private void ProcessPayload(Dictionary<string, JsonElement> payload, byte[] binaryData = null)
{
if (payload.ContainsKey("evtid"))
{
string evtid = payload["evtid"].GetString();
if (events.TryRemove(evtid, out var tcs))
{
if (payload.ContainsKey("type") && payload["type"].GetString() == "error")
{
tcs.TrySetException(new Exception(payload["error"].GetString()));
}
else
{
object data = binaryData ?? payload.GetValueOrDefault("data");
tcs.TrySetResult(data);
}
}
}
}
private async Task ReconnectAsync()
{
if (isReconnecting) return;
await reconnectLock.WaitAsync();
try
{
if (isReconnecting) return;
isReconnecting = true;
await Task.Delay(3000);
Console.WriteLine("API service reconnecting...");
await ConnectAsync();
}
finally
{
isReconnecting = false;
reconnectLock.Release();
}
}
private static string GenerateEventId()
{
return Guid.NewGuid().ToString("N")[..10]; // Use first 10 characters of GUID
}
public async Task<object> InvokeAsync(string type, Dictionary<string, object> parameters = null, int timeout = 18)
{
if (wsClient == null || wsClient.State != WebSocketState.Open)
{
throw new Exception("API service connection not ready");
}
string evtid = GenerateEventId();
var payload = new Dictionary<string, object>(parameters ?? new())
{
["type"] = type,
["evtid"] = evtid,
["timeout"] = timeout
};
var tcs = new TaskCompletionSource<object>();
events[evtid] = tcs;
byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload));
await wsClient.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, cts.Token);
// Timeout handling
using var timeoutCts = new CancellationTokenSource(timeout * 1000);
timeoutCts.Token.Register(() => {
if (events.TryRemove(evtid, out var t)) {
t.TrySetException(new TimeoutException("API service timeout"));
}
});
return await tcs.Task;
}
// Usage example
public static async Task Main(string[] args)
{
var client = new IClickClient();
await client.ConnectAsync();
var parameters = new Dictionary<string, object>
{
["x"] = 100,
["y"] = 200,
["deviceId"] = "XXXXXXXXXX"
};
try
{
var result = await client.InvokeAsync("click", parameters);
Console.WriteLine($"Click result: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"Call failed: {ex.Message}");
}
}
}go
package main
import (
"encoding/json"
"fmt"
"math/rand"
"sync"
"time"
"github.com/gorilla/websocket"
)
type IClickClient struct {
port int
conn *websocket.Conn
events map[string]chan Response
eventsMutex sync.RWMutex
reconnectMutex sync.Mutex
isReconnecting bool
}
type Response struct {
Data interface{}
Error error
}
type Payload struct {
Type string `json:"type,omitempty"`
EvtID string `json:"evtid,omitempty"`
Timeout int `json:"timeout,omitempty"`
Event string `json:"event,omitempty"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func NewIClickClient() *IClickClient {
return &IClickClient{
port: 23188,
events: make(map[string]chan Response),
}
}
func (c *IClickClient) Connect() error {
// Close old connection
if c.conn != nil {
c.conn.Close()
}
url := fmt.Sprintf("ws://127.0.0.1:%d", c.port)
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
c.conn = conn
fmt.Println("API service connected successfully")
go c.messageHandler()
return nil
}
func (c *IClickClient) messageHandler() {
defer func() {
fmt.Println("API service connection closed")
c.reconnect()
}()
for {
messageType, message, err := c.conn.ReadMessage()
if err != nil {
return
}
var payload Payload
switch messageType {
case websocket.TextMessage:
if err := json.Unmarshal(message, &payload); err != nil {
fmt.Printf("JSON parse error: %v\n", err)
continue
}
case websocket.BinaryMessage:
// Parse binary message
var metaLength int
fmt.Sscanf(string(message[:6]), "%d", &metaLength)
if err := json.Unmarshal(message[6:6+metaLength], &payload); err != nil {
fmt.Printf("Metadata parse error: %v\n", err)
continue
}
payload.Data = message[6+metaLength:]
}
c.processPayload(payload)
}
}
func (c *IClickClient) processPayload(payload Payload) {
if payload.EvtID != "" {
c.eventsMutex.RLock()
ch, exists := c.events[payload.EvtID]
c.eventsMutex.RUnlock()
if exists {
response := Response{Data: payload.Data}
if payload.Type == "error" {
response.Error = fmt.Errorf("%s", payload.Error)
}
ch <- response
c.eventsMutex.Lock()
delete(c.events, payload.EvtID)
c.eventsMutex.Unlock()
}
}
}
func (c *IClickClient) reconnect() {
c.reconnectMutex.Lock()
if c.isReconnecting {
c.reconnectMutex.Unlock()
return
}
c.isReconnecting = true
c.reconnectMutex.Unlock()
time.Sleep(3 * time.Second)
fmt.Println("API service reconnecting...")
if err := c.Connect(); err != nil {
fmt.Printf("Reconnection failed: %v\n", err)
}
c.reconnectMutex.Lock()
c.isReconnecting = false
c.reconnectMutex.Unlock()
}
func (c *IClickClient) generateEventID() string {
return fmt.Sprintf("%d%03d", time.Now().UnixNano()%1000000000, rand.Intn(1000))
}
func (c *IClickClient) Invoke(apiType string, params map[string]interface{}, timeout int) (interface{}, error) {
if c.conn == nil {
return nil, fmt.Errorf("API service connection not ready")
}
evtID := c.generateEventID()
payload := map[string]interface{}{
"type": apiType,
"evtid": evtID,
"timeout": timeout,
}
for k, v := range params {
payload[k] = v
}
ch := make(chan Response, 1)
c.eventsMutex.Lock()
c.events[evtID] = ch
c.eventsMutex.Unlock()
data, _ := json.Marshal(payload)
if err := c.conn.WriteMessage(websocket.TextMessage, data); err != nil {
c.eventsMutex.Lock()
delete(c.events, evtID)
c.eventsMutex.Unlock()
return nil, fmt.Errorf("failed to send message: %w", err)
}
select {
case response := <-ch:
return response.Data, response.Error
case <-time.After(time.Duration(timeout) * time.Second):
c.eventsMutex.Lock()
delete(c.events, evtID)
c.eventsMutex.Unlock()
return nil, fmt.Errorf("API service timeout")
}
}
func (c *IClickClient) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
// Usage example
func main() {
client := NewIClickClient()
if err := client.Connect(); err != nil {
fmt.Printf("Connection failed: %v\n", err)
return
}
defer client.Close()
time.Sleep(time.Second)
params := map[string]interface{}{
"x": 100,
"y": 200,
"deviceId": "XXXXXXXXXX",
}
result, err := client.Invoke("click", params, 18)
if err != nil {
fmt.Printf("Call failed: %v\n", err)
return
}
fmt.Printf("Click result: %v\n", result)
}Next Steps
- Read more specific API usage examples