Client Server Experiment?
It’s time to bite the bullet, lightly, and do some real client-server experimentation. I started last night, on my iPad. Aside from that and a bit of reading, I am entirely ignorant about sockets. Let’s fix that a bit.
I subscribe to RealPython. I don’t often work through a tutorial or view a video, but often I’ll scan a tutorial or other article to get a sense of how to do something. I found them a valuable resource when I was just starting with Python and chose to support them a bit, as is my fashion.
They have a very comprehensive (but not free) document on sockets, which I’ll be following, at least part way, here, in a series of articles. For actual learning, you might want to use the RelPython information. What you’ll find here is a look at how I use that sort of material. And I’m sure you’ll find plenty of things to laugh at in what I do.
Last night, in a fit of weirdness. I pasted the two simple examples, shown below, into two separate Python apps on my iPad. (I have both Juno and Pyto.) I suspect I could have started up two instances of one of them, had I thought of it, but until just now, I didn’t. Anyway, since the iPad doesn’t really do multi-tasking, I have no idea what it does with all those cores, I had to start the server in Juno, pop over to Pyto, start the client, pop back to Juno to let it run, pop back.
Curiously, with all that going on, the messages actually got back and forth between the two Python apps. I was pleased, and a little surprised.
This morning, I think I’ll begin, in a new PyCharm project, by replicating that experiment. If my sources are correct, I should be able to just set up two files in PyCharm and run them both and it “should just work”. We’ll see how that goes.
Here’s the echo-server:
# echo-server.py
import socket
HOST = "127.0.0.1" # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
And the echo-client:
# echo-client.py
import socket
HOST = "127.0.0.1" # The server's hostname or IP address
PORT = 65432 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b"Hello, world")
data = s.recv(1024)
print(f"Received {data!r}")
Now, according to my theories, I can just run these two files, server first, in PyCharm, and something good should happen. Let’s FAFO.
The echo-server window in PyCharm now says:
/Users/ron/PycharmProjects/cs1/.venv/bin/python /Users/ron/PycharmProjects/cs1/echo-server.py
Connected by ('127.0.0.1', 52169)
Process finished with exit code 0
And the client window says:
/Users/ron/PycharmProjects/cs1/.venv/bin/python /Users/ron/PycharmProjects/cs1/echo-cliient.py
Received b'Hello, world'
Process finished with exit code 0
I see that I misspelled client when I typed the file name. Easily fixed. SublimeText, my article-writing editor, objects to a lot of other words in the program. All better now.
OK, this worked. What have I learned?
Reflection
First and most important to me, I have verified that it is possible to run both the server and client side code in one instance of PyCharm. That will make development a lot easier. We’ll still have to do a bit of shutting down and starting up, but it’ll be a lot more straightforward than running a bunch of terminal sessions, saving files in PyCharm, going to terminal and restarting things and all that jazz.
I had been reliably informed that that would work, and I had suspected it even before that, but now I’ve done it with my own grubby little fingers.
And now, with the code before me in my favorite Python editor, I’ll see if I can restate what I have learned from these two nearly-trivial programs.1
Start with the server:
HOST = "127.0.0.1" # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
My understanding of this is that we create a socket and do some stuff with it in the with
. Apparently ‘AF_INET’ means IPv4. Mostly I tend to ignore boilerplate and just copy-pasta it around. I’m sure none of you are like that.
The bind
just hooks up the socket s
to the provided host and port from above. And listen
starts the socket listening for connections. I guess that without that, it would be configured only for sending, but that’s a guess.
You could study the tutorial, you know.
Yes, I could and will do that. But looking at the real code and figuring out as much as I can helps me far more than just reading a tutorial. YMMV.
Then we call accept
. Rather obviously the conn
is the connection, and we see in the output from the server that the addr
must be the caller’s address, which in our experiment was ('127.0.0.1', 52169)
, surely host and port again.
So then we drop into a nearly infinite loop. Pretty clearly recv
is the operation that reads the data from the connection. 1024 is surely the length, and it appears that we might get no data, given the if
. A quick check tells me that an empty string is false in Python, a fact I might have hoped never to know. So, if we get no data, we will break and end the program. Otherwise we send all the data back to the connection.
One thing that this code does not tell me, but my reading did, is that the recv
is not guaranteed to have everything that the client sent all in one go. I want to know more about that, but a more robust server would apparently accumulate the result from multiple recv
calls and only process them when the message was complete. There is lots of rigmarole around that seemingly simple sentence.
Now the client:
HOST = "127.0.0.1" # The server's hostname or IP address
PORT = 65432 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b"Hello, world")
data = s.recv(1024)
print(f"Received {data!r}")
Much simpler. Creates a socket the same way. Calls connect
, which must dismiss until the connect has happened. Perhaps it can error, you’d kind of think so. Then it sends the string. I gather that the vanilla send
, given some bytes to send, may not send them all, so you have to loop until it completes. I cannot imagine why one would consider that to be a feature, but anyway, for now we have sendall
. I guess there is no recvall
, probably because recognizing the end of the message is app dependent.
In both cases, the with
assures us that the socket involved will be properly closed and disposed of in a respectful fashion.
And the b"Hello, world"
? Well, that’s a string of bytes (8-bit ints) containing the characters according to some encoding scheme that I don’t know about yet. I suspect it relates to the encode/decode
that one uses in HTTP.
You probably know all this stuff. I do not, because the things I work on are not the same as the things you work on. So, feel free to inform me about things that I don’t understand or don’t get right. My mastodon address is down there in the footer, I think.
Summary
I think I’ve squeezed about all the juice I can get out of these tiny files. If I were less certain about what’s going on, or more curious about what the magic values are, I’d insert some print statements to get more information. In this case, I’m satisfied.
Ive read somewhere that the listen
operation will handle multiple connections, with a backlog. There is some way to select what you deal with and I think the method might actually be select
. We’ll deal with that as we move forward in our learning.
My scanning of the RealPython article and associated code tells me that there are, I think, two more example pairs of server-client programs, of increasing complexity. I’m not sure whether we’ll follow that path or not. The complexity of the final one, which I did scan, is more than I’m likely to be able to create based on knowing what one could read about the calls. There appears to be some occult knowledge built in. But I do like rolling my own code.
In this case, the nuances may be critical, so I might stick pretty close to the tutorial. Ill read a bit more and decide. You’ll know what happens almost as soon as I do. See you next time!
-
Both Juno and Pyto on iPad are impressive. I think Juno may be marginally better, but both work rather nicely, especially considering that iPads aren’t really intended to be development machines. But for reading the code and doing quick modifications, an iPad with no keyboard, in one’s lap, is not the hot development setup. Really good for little experiments while hanging out in the living room or wherever. ↩