I’m spiking TCP. Not writing the article as I go. Instead, I’ll record bits of progress and what I may have learned.

When I did the spike with UDP datagrams, I just coded a little program. With TCP, I’m proceeding differently. I’ve built a little TCP Codea project, with CodeaUnit built in, and I’m writing tests focused on different bits of TCP.

For fun, let me say here what I think I know about TCP just now, while the spike is young.

TCP is part of LuaSockets, which provides UDP, TCP and some MIME and SMTP as well, if I recall. We’re mostly using TCP, though not entirely, as you’ll see below. Anyway …

The tcp library provides three kinds of sockets, which it calls master, server, and client. The master one seems just to be used to create the other two.

A server socket is open on some address and port and would-be clients can connect to it. I believe that the verb is listen, which will hang on the server until someone connects, at which point it will return a client connection to that someone.

The client connection is two-way. You can send to it or accept messages. Presumably the server program, upon getting a new client, will accept from it, do something, and send a result back.

I’m not sure what procedure a would-be client follows in order to get connected and shouting down its end of the connection. Presumably there’s some way to get a client-type socket at its end. I’ll need to find that out, of course.

Vague understanding, which is all I expect of myself at this point. I’ll show you my current test and then get to work.

Finding My IP

In the real app, presumably we’d open out server on some well-known address, www.robotuniverse.com or something. For my purposes, I’ll be using local addresses on my home network. Therefore, to open a server on my iPad, I need the program to know its address. Here’s my test showing how I do that:

        _:test("Get my IP", function()
            local socket = require("socket")
            local myIp = getMyIp()
            print(myIp)
        end)

And I drove out this function:

function getMyIp()
    local socket = require("socket")
    local s = socket.udp()
    s:setpeername("1.1.1.1",80)
    local myIp,_ignored = s:getsockname()
    s:close()
    return myIp
end

Now, to develop that function, I searched the web, and tried things in line, until I got a credible answer, at which point I pulled the code from the test and put it into a function.

I consider that code to be arcane. I think there is no way that you could guess it. Someone had to tell someone who told you, either in person or by writing it up. I think I found this code somewhere in Dave1707’s work. I can explain it somewhat:

socket.udp returns a naked datagram UDP socket. setpeername tells that socket what to talk to (but it’s not going to talk). You just have to do that. Arcane. Once that is done you can call getsockname, which, true to its name (???), returns your local IP and port. We just want the IP.

We politely close the socket and return the IP.

OK, I’m going to go silent now and work on getting a server and client. I’ll report back …

Client/Server

I was nearly done with this one, so in only a few moments, I have this:

        _:test("Server socket", function()
            local socket = require("socket")
            local myIp = getMyIp()
            local server = socket.bind(myIp,8888)
            local sIp, sPort = server:getsockname()
            print(sIp,":",sPort)
            server:close()
        end)

You’ll notice that my test doesn’t have any asserts. Let’s do that and then discuss what I’ve learned.

        _:test("Server socket", function()
            local socket = require("socket")
            local myIp = getMyIp()
            local server = socket.bind(myIp,8888)
            local sIp, sPort = server:getsockname()
            _:expect(sIp).is(myIp)
            _:expect(sPort).is("8888")
            server:close()
        end)

I was somewhat surprised to find that the port is a string. Anyway, the test runs. What have I learned?

I should mention some sources.

I’m reading about LuaSockets here. There are tiny links near the top for greater detail.

There is some useful info in the Lua documentation. The link applies to Lua 5.1. I have a more modern version as an eBook. It’s quite similar.

I just found a github example which I have not looked at. It just came up when I was trying to re-find the bit in the Lua manual. At a glance, nothing new here, but running code that is a bit different is often useful in helping to delineate ideas. I’ll try not to lose that page.

What have I learned from the test above? Well, you create a server with bind, providing an IP and port, receiving a server running on that IP/port. So far so good.

My next step will be to see whether I can get a client connected to my server. I’m going to do that from the same computer, for now. As far as I know, that should work just fine.

I’m not sure just what one does, but I’m going to try to create a socket, set its peer name, and, I don’t know … I’ll have to do some reading.

Back soon …

Getting a Client

After a few minutes, here’s what I’ve got so far:

        _:test("Client connected to server", function()
            local socket = require("socket")
            local host = getMyIp()
            local port = 8888
            local master = assert(socket.tcp())
            master:settimeout(0)
            local client = master:connect(host,port)
            _:expect(client, "couldn't connect").isnt(nil)
            if client then
                local cIp, cPort = client:getsockname()
                print(cIp, cPort)
                --tcp:send("hello")
            end
        end)

I put in the settimeout because I think connect may hang for a long time. I think if we have a server we’ll be OK

That took me some running and checking but it’s about what I expected: the connect can’t work because I have no server in this test. I think that I can create a server and then maybe things will go a bit further.

This is basically how I’m working: do a bit, see if things are as I expect, if not, tweak, if so, do a bit more.

        _:test("Client connection returns nil if no server", function()
            local myIp = getMyIp()
            local socket = require("socket")
            local tcp = assert(socket.tcp())
            local result = tcp:connect(myIp,8888)
            _:expect(result).is(nil)
            tcp:close()
        end)
        
        _:test("Client connected to server", function()
            local myIp = getMyIp()
            local socket = require("socket")
            local server = assert(getLocalServer())
            local tcp = assert(socket.tcp())
            local result = tcp:connect(myIp,8888)
            _:expect(result, "couldn't connect").isnt(nil)
            if result then
                local cIp, cPort = tcp:getsockname()
                _:expect(cIp, "client ip").is(myIp)
                print(cPort)
                --tcp:send("hello")
            end
            server:close()
            tcp:close()
        end)

I decided to document that tcp:connect returns nil when there is no connection. It seems to return 1 (carefully chosen non-nil?) when it succeeds.

In the second test above, result is non-nil and I get the expected IP and, it turns out, a random port. I didn’t know that it would do that, but it makes sense: my messages have to go to a unique client connection on the server side, so I get a special port to use. At least I suppose that’s what’s going on.

My next step will be to do the send and try to receive it. That will mean that I need to do a server accept in here somewhere and … hey, wait …

No one has done an accept on the connection. But I got a connection back. Interesting. Let’s do the send alone and see what happens. Essentially nothing. I believe that send never blocks, but if I were to do a receive on my tcp, it should hang.

        _:test("Client connected to server", function()
            local myIp = getMyIp()
            local socket = require("socket")
            local server = assert(getLocalServer())
            local tcp = assert(socket.tcp())
            local result = tcp:connect(myIp,8888)
            _:expect(result, "couldn't connect").isnt(nil)
            if result then
                local cIp, cPort = tcp:getsockname()
                _:expect(cIp, "client ip").is(myIp)
                print(cPort)
                tcp:send("hello")
                tcp:settimeout(5)
                line, error, partial = tcp:receive()
                if line == nil then
                    print("Error: ", error)
                end
            end
            server:close()
            tcp:close()
        end)

Sure enough, it hangs for 5 seconds and then prints “error”. This tells me that I’ve probably understood most of what’s happening so far.

So therefore, if after my send, I were to do a server:accept and get a client and then client:receive, I should get hello and I could do a client:send goodbye”. Let’s try it.

Well, no.

Trying To Accept/Receive

Here’s what I have in the test now:

        _:test("Client connected to server", function()
            local myIp = getMyIp()
            local socket = require("socket")
            local server = assert(getLocalServer())
            local tcp = assert(socket.tcp())
            local result = tcp:connect(myIp,8888)
            _:expect(result, "couldn't connect").isnt(nil)
            if result then
                local cIp, cPort = tcp:getsockname()
                _:expect(cIp, "client ip").is(myIp)
                print(cPort)
                tcp:send("hello")
                local serverconn = server:accept()
                local message = serverconn:receive()
                _:expect(message).is("hello")
                servercomm:send("goodbye")
                tcp:settimeout(5)
                line, error, partial = tcp:receive()
                _:expect(line).is("goodbye")
            end
            server:close()
            tcp:close()
        end)

That hangs forever. I had to quit Codea. We need some more set timeouts, I think. And probably I should make sure I’m calling those functions correctly: I just sort of guessed.

A Working Test

        _:test("Client connected to server", function()
            local myIp = getMyIp()
            local socket = require("socket")
            local server = assert(getLocalServer())
            server:settimeout(5)
            local tcp = assert(socket.tcp())
            tcp:settimeout(5)
            
            local result = tcp:connect(myIp,8888)
            _:expect(result, "couldn't connect").isnt(nil)
            if result then
                tcp:send("hello\n")
                local serverconn, error = server:accept()
                _:expect(serverconn, "serverconn").isnt(nil)
                serverconn:settimeout(5)
                local message = serverconn:receive()
                _:expect(message, "server looks for hello").is("hello")
                serverconn:send("goodbye\n")
                line, error, partial = tcp:receive()
                _:expect(line, "client looks for goodbye").is("goodbye")
            end
            server:close()
            tcp:close()
        end)

Woot! This works. What it does:

  1. It needs tidying, for sure.
  2. Create a socket, a server, and a tcp for the test to use.
  3. Try to connect the tcp. If it succeeds, it becomes the connection
  4. Send a message, “hello\n”. This message MUST end with a newline.
  5. While the message hangs in space, send accept to the server, which will return a server connection (or nil).
  6. As server, eceive the message, which will have its newline stripped off.
  7. As server, send “goodbye\n”
  8. As client, receive a message, which is “goodbye”.
  9. Close some stuff. Tidying needed.
  10. Cheer in delight. Do short goal line dance.

I am tired. I’ll commit this and get back to it later. Please join me then.

Summary

We have successfully coded a grubby but operational round trip TCP connection. We deserve a treat.

See you next time!