Communicating Between Godot and Python Over TCP

Godot Version

4.2

Question

I’m trying to implement some simple back and forth communication between my game and a separate process running on my server. I’ve been trying to use StreamPeerTCP but I’m running into a strange issue. One-way communication works without issue, with the following code, I get the expected Received '12345' on my server.

# Godot snippet
func create_session() -> void:
	var peer = StreamPeerTCP.new()

	peer.connect_to_host("localhost", 6789)
	while peer.get_status() != StreamPeerTCP.STATUS_CONNECTED:
		peer.poll()

	peer.put_data("12345".to_ascii_buffer())

	# while peer.get_available_bytes() < 5:
	#	peer.poll()

	# var data := peer.get_data(5)

	# print(data)
# Python TCP server
async def main():
    server = await asyncio.start_server(handle_request, 'localhost', 6789)

    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')

    async with server:
        await server.serve_forever()


async def handle_request(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    data = await reader.read()
    message = data.decode("ascii")

    print(f"Received '{message}'") 
    # writer.write(b"54321")
    # await writer.drain()

    # writer.close()

However, when I attempt to send a response back (i.e. uncomment the commented-out lines), the Godot process freezes up, continuously polling and never receiving any data. What’s even more strange is that the Python process never gets the sent data until I forcibly close the Godot process. I’ve tried setting the no-delay mode to true, but that didn’t help.

What am I doing wrong here? Is there a better way to handle this use case? I’m truly at a loss.

Looks like a TCP-level issue - it buffers small messages into larger buffers so it doesn’t have to actually transmit data as often. I think if you were to send data at regular intervals this could soften somewhat to receiving data but with a delay.

Did you turn off delay on both ends?

I added peer.set_no_delay(true) to the Godot side and server.sockets[0].setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) to the Python side (which seems to be the default for TCP sockets anyway), and the behavior is the same.

From the behaviour you described it sounds like peer.put_data won’t send the data if the main thread is blocked.
Have you already tried polling the peer every frame instead of just blocking the whole main thread?

I tried putting the read inside of _process:

func _process(_delta: float) -> void:
	if not tcp_peer.get_status() == StreamPeerTCP.STATUS_CONNECTED:
		return

	tcp_peer.poll()
	if tcp_peer.get_available_bytes() < 5:
		#print("Still waiting: ", tcp_peer.get_available_bytes())
		return

	var data := tcp_peer.get_data(5)
	create_session_callback(data)

The main thread no longer blocks, but it still never receives data, and the Python process only gets the sent data when I exit Godot.

In your python server code, I changed

data = await reader.read()

into

data = await reader.readexactly(5)

and it worked. So it seems reader.read() got stuck forever waiting for more data - according to the docs (Streams — Python 3.12.1 documentation) it will wait for EOF.

I guess in the send-only case the StreamPeerTCP would send EOF when going out of scope, and that’s why it was working before. Nice catch!