When you have a server, you create a socket and then you bind it to an IP and port, which gives the socket a unique way to be identified based on a unique socket type, address family, IP and port. You then listen() to set the socket to server mode, and then you do accept(), which waits on a connection which will have inbound packets with target IP/port that cause the packets to be queued on that socket. You don't need to bind it to an IP however, it can accept connections on all interfaces.
When you have a client, you create a socket, and then you connect() the socket to a remote IP and port, which will also bind 0.0.0.0 and a random unused ephemeral port to the socket if it hasn't been already bound to an IP and port using bind(INADDR_ANY, 0). connect() returns when it connects, and uses the IP and port as the source address in the outbound packets, where 0.0.0.0 is always substituted with an IP based on src hint in the routing table or the IP of the selected interface (if it has multiple IPs then first IP with same or bigger scope is selected), and then you use sendall to send application data.
INADDR_ANY is faster than programmatically acquiring the current internal IP of the interface, which could change at any moment, and packets will no longer be received on the port, but they'd still be received on 0.0.0.0 because it is any address.
Note that a socket can be bound to 0.0.0.0, but not port 0, because its a wildcard that makes it give the socket a random ephemeral port, so when you use bind(INADDR_ANY, 0) it binds to 0.0.0.0 and a random ephemeral port.