+1 vote

This node provides a network manager and transceiver object that can be used to access data over a network using StreamPeerTCP. The transceiver does asynchornous operations and provides appropriate signals for the various network activity events.

One caveat is in the StreamPeerTCP.connect() call. If the endpoint is anything other than available this method will permablock and not even return an error when it could (such as connection refused when no application listens at the specified port). I work around this be wrapping the .connect() in thread and then time the thread. If it times out the thread should be killed. There's no way to kill the thread so you wind up with the debugger complaining about the thread still active when the reference is destroyed (so obviously that doesn't kill the thread). I have another question posted asking about how to kill a stuck thread which has yet to receive any answer (How to Kill a Permablocked Thread).

Mostly works, except for not being able to kill the thread with a blocked .connect()

Anyways here's the class (inherits node):

Transceiver.gd:

extends Node

#
# Network stream management
#
# Allows asynchronous network access with signals to indicate
# completion of accesses.
#
# The process loop will consume network data as it is received
# and place it into a recieving FIFO.  Data bound for the network
# (the send_xxx methods) will placed into a sending FIFO.  Bytes
# will come off the send FIFO as the underlying OS FIFO takes
# it up (put_data_partial).
#
# An additional signal is present and will fire if the connection
# is dropped.
#

var connect_timeout=10

signal Connected(host)
signal ConnectTimeout()
signal Disconnected(host)
signal NetworkError(host)
signal GotData(host,data)
signal SendingFinished(host)

var host=null
var signalConnect=false
var wasConnected=false
var connect_thread=null
var connect_attempt_elapsed
var _thd_done

var recvFIFO=RawArray()
var sendFIFO=RawArray()

func left_delete(raw,amt):
    # deletes amt bytes from beginning of rawarray
    if amt>=raw.size():
        raw.resize(0)
        return raw
    if amt<=0:
        # nothing to do
        return raw
    #
    # *** THIS IS A HACK ***
    # We need a subarray (slice) operation to do this properly
    # This hack will copy the entire data three times!!! O(N)
    # as RawArray's copy when modified.
    # Alas this hack is still faster than scriptlooping to
    # left-delete using .remove() and having the data copied on
    # each pass through the loop! O(N^2+N) (for a mere 10 KB
    # of data scriptlooping results in 50 MB!!! of wasted copying,
    # whereas this hack only copies 30K)
    #
    raw.invert()
    raw.resize(raw.size()-amt)
    raw.invert()
    return raw

func connect_worker(args):
    var addr=args[0]
    var port=args[1]
    var host=StreamPeerTCP.new()
    var err=host.connect(addr,port)
    while host.get_status()==1:
        pass
    _thd_done=true
    return [err,host]

func connect(addr,port):
    connect_attempt_elapsed=0.0
    connect_thread=Thread.new()
    _thd_done=false
    connect_thread.start(self,"connect_worker",[addr,port])

var _st
var pair
var partial
func _process(delta):

    if connect_thread!=null:
        connect_attempt_elapsed+=delta
        if _thd_done:
            # this means the thread connected
            var rc=connect_thread.wait_to_finish()
            connect_thread=null
            attach(rc[1])
            signalConnect=true
        else:
            if connect_attempt_elapsed>=connect_timeout:
                # API gives no way to kill a thread???
                # best we cand do then is force its refcount to 0
                connect_thread=null
                # indicate the timeout downstream
                emit_signal("ConnectTimeout")

    if host==null:
        pass
    else:
        _st=host.get_status()
        if _st==2:
            wasConnected=true
            if signalConnect:
                signalConnect=false
                emit_signal("Connected",host)
        if (_st==0 or !host.is_connected()) and wasConnected:
            emit_signal("Disconnected",host)
            detach()
        if _st==3:
            emit_signal("NetworkError",host)
            detach()

        if sendFIFO.size()>0:
            pair=host.put_partial_data(sendFIFO)
            partial=pair[1]
            sendFIFO=left_delete(sendFIFO,partial)
            if sendFIFO.size()==0:
                emit_signal("SendingFinished",host)

        var avl=host.get_available_bytes()
        if avl>0:
            recvFIFO.append_array(host.get_data(avl)[1])
            emit_signal("GotData",host,recvFIFO)
            recvFIFO.resize(0)

func write(what):
    sendFIFO.append_array(what)

func attach(stream):
    var st=stream.get_status()
    if st==1 or st==2:
        host=stream
        wasConnected=true
        if st==1:
            signalConnect=true
        return true
    return false

func detach():
    host=null
    recvFIFO.resize(0)
    sendFIFO.resize(0)

func _ready():
    set_process(true)
in Engine by (43 points)

I've had to change .connect() in the script to .connectToPeer() otherwise it's not possible to set up the signals from a script using PackedScene.instance() to create multiple transceivers to handle multiple connections.

Changes to this code are now also on github.

Please log in or register to answer this question.

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.