Skip to content

Cross-Platform Socket Behavior Inconsistencies #1264

@barisyild

Description

@barisyild

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions