hxcpp icon indicating copy to clipboard operation
hxcpp copied to clipboard

Cross-Platform Socket Behavior Inconsistencies

Open barisyild opened this issue 4 months ago • 2 comments

There’s an issue with the hxcpp/hashlink/neko socket code platform-specific socket requests are either not being processed at all, or the waitForRead / accept calls aren’t being triggered.

Note: If you are having cors problems, try again using the plugin below. https://chromewebstore.google.com/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf

POC

Server Side

import sys.net.Host;
import sys.net.Socket;
import sys.thread.Thread;
import haxe.io.Bytes;
import haxe.io.Input;
using StringTools;

class Main {
	#if cpp
	private static var threadPool = new sys.thread.FixedThreadPool(32);
	#end

	public static function main() {
		var listener_socket = new Socket();
		// Binding 0.0.0.0 means, listen on "any / all IP addresses on this host"
		listener_socket.bind(new Host('0.0.0.0'), 8000);
		listener_socket.listen(9999); // big max connections

		while (true) {
			// Accepting socket
			trace('waiting to accept...');
			var peer_connection:Socket = listener_socket.accept();
			if (peer_connection != null) {
				trace('got connection from : ' + peer_connection.peer());

				// Spawn a reader thread for this connection:
				#if cpp threadPool.run #else Thread.create #end (() -> read(peer_connection));
			}
		}
	}

	static function read(peer_connection:Socket):Void {
		try {
			var rawRequest:String = parseRequestFromInputProtocol(peer_connection.input).toString();
			var request = parseRequest(rawRequest);
			var requestPath:String = request.path;
			trace(requestPath);
			if(requestPath.startsWith("/list"))
				handleUploadList(peer_connection);
			else if(requestPath.startsWith("/upload"))
				handleUpload(peer_connection);
		} catch(e) {
			trace(e);
			trace(e.stack);
		}
		peer_connection.close();
	}
	
	static function handleUploadList(peer_connection:Socket):Void {
		var chunks:Array<Dynamic> = [];
		for(i in 0...2) {
			chunks.push({start: 0, end: 16777216, key: 'chunk_${Math.random()}', createdAt: 0});
		}
		peer_connection.output.writeString('HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n${haxe.Json.stringify({chunks: chunks})}');
	}
	
	static function handleUpload(peer_connection:Socket):Void {
		trace(readUntilDelimiter(peer_connection.input));
	
		try {
			peer_connection.input.read(16777216);
		} catch(e) {
		
		}
	
		peer_connection.output.writeString('HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{"status":"success","sessionKey":"aWHdCaOsLwnwc90kEjfenp7UvxnVfwAf"}');
	}
	
	static function parseRequest(rawRequest:String):Dynamic {
		var lines = rawRequest.split("\r\n");
        var requestLine = lines[0].split(" ");
        #if debug
        trace(requestLine);
        #end
        var method = requestLine[0];
        var path = requestLine[1].urlDecode();
		
		return {method: method, path: path};
	}
	
	static function readUntilDelimiter(input:haxe.io.Input):String {
        var buffer = new StringBuf();
        var lastFourChars = "";

        try {
            while (true) {
                var char = String.fromCharCode(input.readByte());
                buffer.add(char);

                // Keep track of the last 4 characters
                lastFourChars += char;
                if (lastFourChars.length > 4) {
                    lastFourChars = lastFourChars.substr(lastFourChars.length - 4);
                }

                // Check if we've found the delimiter
                if (lastFourChars == "\r\n\r\n") {
                    break;
                }
            }
        } catch (e:Dynamic) {
            trace("Error reading from input: " + e);
        }

        return buffer.toString();
    }
	
	static function parseRequestFromInputProtocol(input:Input):Bytes
    {
		var httpRequestEnd = [0x0D, 0x0A, 0x0D, 0x0A];

        #if cpp
        var buffer:Array<cpp.UInt8> = new Array<cpp.UInt8>();
        #elseif java
        var buffer:Array<java.lang.Byte> = new Array<java.lang.Byte>();
        #else
        var buffer:Array<Int> = new Array<Int>();
        #end
        var index:Int = 0;
        while (true)
        {
            var found:Bool = false;
            buffer[index] = input.readByte();

            if (index >= 4)
            {
                found = true;
                for(i in 0...4)
                {
                    if(buffer[index - 3 + i] != httpRequestEnd[i])
                    {
                        found = false;
                        break;
                    }
                }
            }
            index++;

            if(found)
                break;
        }
        buffer.resize(buffer.length - httpRequestEnd.length);
        #if cpp
        return Bytes.ofData(buffer);
        #else
        var bytes = Bytes.alloc(buffer.length);
        for(i in 0...buffer.length)
        {
            bytes.set(i, cast buffer[i]);
        }
        return bytes;
        #end
    }
}

Client Side

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>List → Upload Loop (Real-Time Log + Continue)</title>
<style>
body{font-family:monospace;background:#0e1117;color:#ddd;margin:0;padding:10px}
#controls{margin-bottom:10px}
button{background:#1b4bff;border:none;color:#fff;padding:6px 12px;
        border-radius:6px;font-weight:bold;cursor:pointer}
#log{font-size:13px;white-space:pre-wrap}
.ok{color:#7CFC00}
.err{color:#FF6347}
.list{color:#00BFFF}
.upload{color:#FFD700}
.info{color:#aaa}
</style>
</head>
<body>
<div id="controls">
  <button id="continueBtn">Trigger Socket Select</button>
</div>
<h3>Real-Time List / Upload Log</h3>
<div id="log"></div>

<script>
const API_URL = "http://127.0.0.1:8000";
const CHUNK_SIZE = 16777216;
let id = 0;
let entries = [];

function render() {
  const now = performance.now();
  const html = entries.slice(-100).reverse().map(e=>{
    const elapsed = (now - e.start).toFixed(0);
    const dur = e.end ? `${e.duration} ms` : `${elapsed} ms`;
    const cls = e.type==="list"?"list":(e.ok===false?"err":(e.type==="continue"?"info":"upload"));
    const status = e.end ? (e.ok?"OK":"ERROR") : "•••";
    return `<span class="${cls}">[${e.type.toUpperCase()}] ${status} ${dur} ${e.msg}</span>`;
  }).join("\n");
  document.getElementById("log").innerHTML = html;
  requestAnimationFrame(render);
}

function addEntry(type,msg){
  const e={type,msg,start:performance.now(),end:0,ok:null,duration:0};
  entries.push(e);
  if(entries.length>200) entries.shift();
  return e;
}
function finishEntry(e,ok){
  e.ok=ok;
  e.end=performance.now();
  e.duration=(e.end-e.start).toFixed(0);
}

async function getList(){
  const ent=addEntry("list","→ /list?id="+id);
  try{
    const res=await fetch(`${API_URL}/list?id=${id++}`,{method:"POST"});
    if(!res.ok) throw new Error(res.status);
    const data=await res.json();
    finishEntry(ent,true);
    if(data.chunks?.length){
      await Promise.all(data.chunks.map(c=>uploadChunk(c.key)));
    }
    getList();
  }catch(e){
    finishEntry(ent,false);
    getList();
  }
}

async function uploadChunk(key){
  const ent=addEntry("upload","→ "+key);
  const blob=new Blob([new Uint8Array(CHUNK_SIZE)],{type:"application/octet-stream"});
  const formData=new FormData();
  formData.append("file",blob);
  try{
    const res=await fetch(`${API_URL}/upload?chunkKey=${key}`,{method:"POST",body:formData});
    if(!res.ok) throw new Error(res.status);
    finishEntry(ent,true);
  }catch(e){
    finishEntry(ent,false);
  }
}

// --- Continue button handler ---
document.getElementById("continueBtn").addEventListener("click", async ()=>{
  const ent = addEntry("continue","→ /");
  try {
    const res = await fetch(`${API_URL || ''}/`, {method:"GET"});
    if (!res.ok) throw new Error(res.status);
    finishEntry(ent,true);
  } catch (e) {
    finishEntry(ent,false);
  }
});

// start rendering and looping
render();
getList();
</script>
</body>
</html>

Backend Behavior

Windows

Image

HTTP requests usually fail.

Linux & FreeBSD

Image

HTTP requests usually don’t fail, but sometimes a connection isn’t processed until another connection triggers it. I think the socket select call gets stuck at some point, and the connection isn’t handled until a new one arrives.

When a request freezes, pressing the “Trigger Socket Select” button will cause the frozen request to be processed.

I think the problem is related to Chrome’s TCP pooling feature.

MacOS

Image

Working without any issue

barisyild avatar Nov 01 '25 17:11 barisyild

Looks like problem also appears on hashlink and neko target, i think both implementation is missing something. JVM is clearly working.

barisyild avatar Nov 01 '25 19:11 barisyild

I added Connection: close for fix possible connection keep problem but does not work.

barisyild avatar Nov 03 '25 10:11 barisyild