Server not receiving UDP messages from client (Python to MATLAB)

212 views Asked by At

I am trying to connect to a client computer in order to send integer values using UDP from my server computer. The problem arises (I believe) due to the server computer sending and listening to UDP communication in Python, while the client receives and sends messages from a MATLAB script. Theoretically this arrangement should not matter since the UDP communication should not be affected by the coding language at all.

Server-side code (Python):

import socket

commandVal = 0 #Message to be sent

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(np.uint8(commandVal), (hostIP, portNum)) #hostIP and portNum are defined separately

sock.connect((hostIP, portNum))
while True:
    data, addr = sock.recvfrom(1024)
    print("received message: %s" % data)

Client-side code (MATLAB):

packetlength = 50
waitdur = 15000

mssgA = judp('receive',port,packetlength,waitdur);
if mssgA(1) == 0
   judp('send',port, host,int8('error'))
else
   judp('send',port, host,int8('success'))

I know the ports and IP addresses are defined correctly, because I can send and receive messages if I use the MATLAB-based judp function to communicate from the server end.

When the Python code is used, the message is sent to the client, but no 'error' or 'success' message is received in return. What is the issue here?

I have tried changing firewall settings, and going through the documentation for both judp and socket. I haven't found a solution yet.

2

There are 2 answers

2
Hoodlum On

I am a bit confused as to who is the client and who is the server in your code. It appears your "server" is sending commands/requests to the "client", which seems backwards?

That being said, I think your main issue comes from the fact that you're never binding your "server" socket to a particular address, which means that your "client"'s messages are not recognized as being addressed to the "server".

Also, you're using the same port for sending and receiving in your MATLAB script, this could be a typo, or it could be intended, I'm afraid I can't say without more information on the rest of your code.

server.py

import socket

client_ip = '127.0.0.1' # using localhost for this example
client_port = 10001

server_ip = '127.0.0.1' # using localhost for this example
server_port = 10002

commandVal = 0  # Message to be sent
waitdur = 0.1 # wait for messages this many seconds, then break to
#   handle KeyboardInterrupts. note: don't do this in a real project.
#   it's a waste of CPU resources. for this demo, it's OK though, I guess.

# use a context manager ("with-statement") to make sure the socket is properly
#   closed and released on exit.
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
    # set a timeout so we can interrupt the program with CTRL+C later
    sock.settimeout(waitdur)

    print(f"binding address to server: {(server_ip, server_port)}")
    sock.bind((server_ip, server_port))

    print(f"connecting to 'client' at {(client_ip, client_port)}")
    sock.connect((client_ip, client_port))

    print("sending command value to 'client'")
    sock.send(int.to_bytes(commandVal, length=1, signed=False)) # check endianness if there's an issue with the value being sent. for a single byte this is irrelevant though

    print("listening for response")
    while True:
        try:
            data, addr = sock.recvfrom(1024)
            print(f"received '{data}' from {addr}")
            # note: since we already know the address we're receiving from (it's
            #   the one we connected to previously), the above is equivalent
            #   to the following:
            # data = sock.recv(1024)
            # print(f"received '{data}' from {(client_ip, client_port)}")

        except socket.timeout:
            pass

client.m

client_ip = '127.0.0.1'
client_port = 10001

server_ip = '127.0.0.1'
server_port = 10002

packetlength = 50
waitdur = 15000

mssgA = judp('receive', client_port, packetlength, waitdur);
if mssgA(1) == 0
   judp('send', server_port, server_ip, int8('error'))
else
   judp('send', server_port, server_ip, int8('success'))
0
micromoses On

TL;DR

Use the socket's bind method on the server so it is bound to a specific port number. It is generally advised to bind only to the specific IP addresses (or interfaces) through which the server is expected to communicate, but a wildcard (all interfaces - 0.0.0.0) can be used if appropriate.
Make sure to bind the socket before issuing any outgoing traffic, like so:

import socket

commandVal = 0 # Message to be sent

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind("0.0.0.0", server_port)

sock.sendto(...)

Overview

Usually in a client-server architecture, the client initiates the communication with the server (like someone asking a question), and the server replies accordingly (usually with an answer to the question). This is true for UDP as much as TCP (e.g. DHCP, NTP, DNS etc.). Some servers may initiate communication with subordinate machines (as in configuration management for example), but it can be argued that these machines are actually running "servers" (agents) to reply to the client's (the CM server's) queries. As @Hoodlum mentioned, having your "server" (Python code) make contact with the "client" (MATLAB) is quite disorientating, but that's a topic for a different thread (if at all).

Technicals

In a client-server architecture, the server would listen on specific ports for incoming client requests (e.g. port 80 for HTTP, 443 for HTTPS, 25 for SMTP etc.), whereas the clients would usually assign a pseudo-random source-port to which they are expecting to receive the server's answer (usually somewhat sequential, the point is that these port numbers cannot be relied upon). This is where things start to fall apart between the server and client in question:
From the server-side, the sendto works. As stated, the UDP datagram reaches the MATLAB client. The problems start when the client tries to send a response to port, on which the server is not listening. As the server's socket was not bound to a specific port, the outgoing message was given a pseudo-random (that sequential thingie) port number. The client is configured to send messages to a specific port number on which the Python application (probably) isn't listening, and that datagram would not reach its desired destination.

Solutions

[Disclaimer - I don't have a MATLAB license, so I ported the client's code to Python as best as I could. Some solutions I suggest require modifying the client's code, which I'm not sure if even possible with MATLAB]
This is provided as a summary with running examples. The code itself will be posted down below.

1. Bind the server's socket

==== TERMINAL 1 ====
$ ./example.py ported_client
Client received message: 'data=(b'\x00', ('127.0.0.1', 10001))', 'data_int=0'
Client has Not received The Answer to Life; data_int=0
$

==== TERMINAL 2 ====
$ ./example.py bound_server
Server received message: 'b'+''
Server formatted data: npint_data=43, addr=('127.0.0.1', 10002)

2. Bind and connect to client

Note that once connected, send/recv should be used instead of sendto/recvfrom

==== TERMINAL 1 ====
$ ./example.py ported_client
Client received message: 'data=(b'\x00', ('127.0.0.1', 10001))', 'data_int=0'
Client has Not received The Answer to Life; data_int=0
$

==== TERMINAL 2 ====
$ ./example.py connected_server
Server received message: 'b'+''
Server formatted data: npint_data=43

3. Make the client aware of the server's port (requires changes to client)

==== TERMINAL 1 ====
$ ./example.py adaptive_ported_client
Client received message: 'data=(b'\x00', ('127.0.0.1', 58264))', 'data_int=0'
Client has Not received The Answer to Life; data_int=0
$

==== TERMINAL 2 ====
$ ./example.py original_server
Server received message: 'b'+''
Server formatted data: npint_data=43, addr=('127.0.0.1', 10002)

4. Implement client-server where the server does not initiate communication (requires changes to client)
This has the additional benefit that the client can be triggered multiple times while the server keeps running

==== TERMINAL 1 ====
$ ./example.py simple_client
Client: 7 is NOT The Answer to Life; data_int=0
Client: 42 IS The Answer to Life; data_int=1
$
$ ./example.py simple_client
Client: 7 is NOT The Answer to Life; data_int=0
Client: 42 IS The Answer to Life; data_int=1
$

==== TERMINAL 2 ====
$ ./example.py simple_server
Server formatted data: npint_data=7, addr=('127.0.0.1', 61549)
Server formatted data: npint_data=42, addr=('127.0.0.1', 61549)
Server formatted data: npint_data=7, addr=('127.0.0.1', 49335)
Server formatted data: npint_data=42, addr=('127.0.0.1', 49335)

Code

First, let's show the problem does reproduce:

==== TERMINAL 1 ====
$ ./example.py ported_client
Client received message: 'data=(b'\x00', ('127.0.0.1', 49478))', 'data_int=0'
Client has Not received The Answer to Life; data_int=0
$

==== TERMINAL 2 ====
$ ./example.py original_server

[No other output]

Now some code (the full contents of example.py):

#!/usr/bin/env python

import sys
import socket
import argparse

import numpy as np


BUFSIZE = 50            # While the Python code sets it at 1024, MATLAB's sets it at 50. Both should accomodate for a uint8
SRV_COMMAND_VAL = 0     # replacing `commandVal`. You know, for testing.

server_host = "127.0.0.1"
server_port = 10001
client_host = server_host
client_port = server_port + 1


def ported_client(adaptive = False):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(15)     # Should match MATLAB's `waitdur`

    sock.bind((client_host, client_port))
    data = sock.recvfrom(BUFSIZE)
    data_int = np.frombuffer(data[0], dtype=np.uint8)[0]
    _server_port = data[1][1] if adaptive else server_port
    print(f"Client received message: '{data=}', '{data_int=}'")

    # I don't know what int8('error') or int8('success') return in MATLAB, but let's try returning THE answer:
    if data_int:    # != 0
        sock.sendto(np.uint8(42), (server_host, _server_port))
    else:           # == 0, is an empty sequence, False, None, ...
        # The MATLAB code seems to return `int8('error')` if data_int == 0, so let's Not return THE answer:
        sock.sendto(np.uint8(43), (server_host, _server_port))
    print(f"Client has {'' if data_int else 'Not '}received The Answer to Life; {data_int=}")


def adaptive_ported_client():
    return ported_client(adaptive=True)


def _srv_recv(sock):
    while True:
        data, addr = sock.recvfrom(BUFSIZE)
        print("Server received message: '%s'" % data)
        npint_data = np.frombuffer(data, dtype=np.uint8)[0]
        print(f"Server formatted data: {npint_data=}, {addr=}")


def original_server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(np.uint8(SRV_COMMAND_VAL), (client_host, client_port))

    sock.connect((client_host, client_port))
    _srv_recv(sock)


def bound_server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # `socket.bind()` must happen before `socket.sendto()`, otherwise an `OSError: [Errno 22] Invalid argument` will be raised;
    # I assume this has something to do with port assignments during `sendto`, but that's only a speculation on my end.
    sock.bind((server_host, server_port))
    sock.sendto(np.uint8(SRV_COMMAND_VAL), (client_host, client_port))

    _srv_recv(sock)


def connected_server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((server_host, server_port))

    # When connecting a socket to a specific remote host, use `send` and `recv` without the to/from suffixes
    sock.connect((client_host, client_port))
    sock.send(np.uint8(SRV_COMMAND_VAL))

    while True:
        data = sock.recv(BUFSIZE)
        print("Server received message: '%s'" % data)
        npint_data = np.frombuffer(data, dtype=np.uint8)[0]
        print(f"Server formatted data: {npint_data=}")


def simple_server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((server_host, server_port))

    while True:
        data, addr = sock.recvfrom(BUFSIZE)
        npint_data = np.frombuffer(data, dtype=np.uint8)[0]
        print(f"Server formatted data: {npint_data=}, {addr=}")
        is_the_answer = npint_data == 42
        sock.sendto(np.uint8(is_the_answer), addr)


def simple_client():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(15)     # Should match MATLAB's `waitdur`

    for the_answer_is in (7, 42):
        sock.sendto(np.uint8(the_answer_is), (server_host, server_port))
        data = sock.recvfrom(BUFSIZE)
        data_int = np.frombuffer(data[0], dtype=np.uint8)[0]
        print(f"Client: {the_answer_is} {'IS' if data_int else 'is NOT'} The Answer to Life; {data_int=}")


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("run_type", choices=("ported_client", "adaptive_ported_client", "original_server", "bound_server", "connected_server", "simple_server", "simple_client"))
    return parser.parse_args()


def main(run_type):
    return globals()[run_type]()


if __name__ == "__main__":
    args = parse_args()
    sys.exit(main(args.run_type))

Summary (finally...)

I like Hoodlum's solution, with some reservations. It is straightforward, and handles socket.close() (by using the with context). Basically, that's solution #2 of my post, just shorter and more readable.
Still, I thought this is worth the writing, possibly shedding some light on the reasons for this issue (or to glorify UDP, IDK).