Forums » Community Projects
Using the TCP socket support
As I mentioned before, we can't use luasockets because of event loop issues, so we exposed our own internal TCP sockets class without ever telling you even what it was called.
The class is TCPSocket.
Construction:
local sock = TCPSocket()
Methods:
- success, error = sock:Connect(host, port): establish a TCP connection to host:port. Returns 1,nil if successful, nil,"error message" otherwise. This is a *non-blocking* connect, which means it always returns immediately, whether or not the connection succeeded. The write callback (described below) will be called when the connect completes (whether it succeeds or fails).
- success, error = sock:Listen(port): listen on TCP port. Newly established connections will trigger a connection event, which we will get to shortly.
- newconn = sock:Accept(): return new TCPSocket object for the established connection.
- sock:SetConnectHandler(fn): set a callback which occurs when someone connects to a socket that is Listen()ing. You must Accept to get the new connection. Not used for outgoing connections.
- sock:SetReadHandler(fn): callback when data is available for reading. Disables callback if fn is nil.
- sock:SetWriteHandler(fn): callback when output buffer space is available for writing, or when the connection completes. Disables callback if fn is nil.
- nsent = sock:Send(string): send string to socket. Socket is in non-blocking mode (otherwise your whole client would lock up trying to send long strings) so will not always send entire string. nsent is the number of bytes sent.
- msg, errcode = sock:Recv(): get all available data. If there is a socket error, msg will be nil and errcode will contain the standard errno error code.
- errmsg = sock:GetSocketError(): if there is an error flagged on the socket, return the error message string. This is slightly wonky under OS X and Linux because it uses getsockopt(SO_ERROR) which isn't quite the same as errno, where errors usually go. Mostly useful for getting the status of a Connect() after the resulting write callback.
- hostname = sock:GetPeerName(): return string containing the IP and port of the remote end of the socket.
Now, this is somewhat confusing stuff. You can't just write regular straight-line send-this-recv-that socket code and expect it to work. Which is why I have a couple layers of wrappers we use, and you can use.
The first layer (tcpsock.lua) sets up an asynchronous buffered line protocol. You get a callback when someone sends a line to you, and you write lines (in response or whenever you want) and it takes care of input and output buffering.
The next layer (client.lua & server.lua) builds a coroutine-based RPC environment using JSON as the transport. It creates an object with a metatable that allows you to say RPCobject:anymethod("any", {arguments=true}, 23894) -- this will send a JSON-formatted request for 'anymethod' to the remote end, block the current coroutine until it gets a matching response from the other end, and then return it to the caller.
I built a database connection layer (dbclient.lua & dbserver.lua) using luasql at the server end using this. You can use it as an example; it won't work out of the box.
Code is available at http://a1k0n.net/vendetta/lua/tcpstuff/
UPDATE UPDATE: seems that the OS X and ia32 linux versions are missing this stuff; amd64 linux and windows have it. Not sure what happened there.
The class is TCPSocket.
Construction:
local sock = TCPSocket()
Methods:
- success, error = sock:Connect(host, port): establish a TCP connection to host:port. Returns 1,nil if successful, nil,"error message" otherwise. This is a *non-blocking* connect, which means it always returns immediately, whether or not the connection succeeded. The write callback (described below) will be called when the connect completes (whether it succeeds or fails).
- success, error = sock:Listen(port): listen on TCP port. Newly established connections will trigger a connection event, which we will get to shortly.
- newconn = sock:Accept(): return new TCPSocket object for the established connection.
- sock:SetConnectHandler(fn): set a callback which occurs when someone connects to a socket that is Listen()ing. You must Accept to get the new connection. Not used for outgoing connections.
- sock:SetReadHandler(fn): callback when data is available for reading. Disables callback if fn is nil.
- sock:SetWriteHandler(fn): callback when output buffer space is available for writing, or when the connection completes. Disables callback if fn is nil.
- nsent = sock:Send(string): send string to socket. Socket is in non-blocking mode (otherwise your whole client would lock up trying to send long strings) so will not always send entire string. nsent is the number of bytes sent.
- msg, errcode = sock:Recv(): get all available data. If there is a socket error, msg will be nil and errcode will contain the standard errno error code.
- errmsg = sock:GetSocketError(): if there is an error flagged on the socket, return the error message string. This is slightly wonky under OS X and Linux because it uses getsockopt(SO_ERROR) which isn't quite the same as errno, where errors usually go. Mostly useful for getting the status of a Connect() after the resulting write callback.
- hostname = sock:GetPeerName(): return string containing the IP and port of the remote end of the socket.
Now, this is somewhat confusing stuff. You can't just write regular straight-line send-this-recv-that socket code and expect it to work. Which is why I have a couple layers of wrappers we use, and you can use.
The first layer (tcpsock.lua) sets up an asynchronous buffered line protocol. You get a callback when someone sends a line to you, and you write lines (in response or whenever you want) and it takes care of input and output buffering.
The next layer (client.lua & server.lua) builds a coroutine-based RPC environment using JSON as the transport. It creates an object with a metatable that allows you to say RPCobject:anymethod("any", {arguments=true}, 23894) -- this will send a JSON-formatted request for 'anymethod' to the remote end, block the current coroutine until it gets a matching response from the other end, and then return it to the caller.
I built a database connection layer (dbclient.lua & dbserver.lua) using luasql at the server end using this. You can use it as an example; it won't work out of the box.
Code is available at http://a1k0n.net/vendetta/lua/tcpstuff/
UPDATE UPDATE: seems that the OS X and ia32 linux versions are missing this stuff; amd64 linux and windows have it. Not sure what happened there.
Yaaay!
Any idea when the ia32 Linux and Mac will added to the list of TCP-enabled clients?
** Zathras does the TCP dance **
Any idea when the ia32 Linux and Mac will added to the list of TCP-enabled clients?
** Zathras does the TCP dance **
UPDATE UPDATE UPDATE: ok, 1.7.35.2 released with corrected lunix and OS X executables.
YAAY! *does the Vendetta Fanboi dance*
pcall() and xpcall() are apparently not exported either. Grrrrrrr. I'm exporting those for the next version because they are important. If your TCPSocket callback function has an error, it will bomb the whole client. pcall and xpcall are used to catch those errors.
For now you can do this:
declare('pcall')
declare('xpcall')
function pcall(f,...) return true,f(unpack(arg)) end
function xpcall(f,e) return f() end
also, you'll need to add declare('TCP') etc to the top of my example code. Sorry about that. Also, I corrected some errors above. The write callback is called after an outgoing connection completes; the connection callback is only for incoming connections.
For now you can do this:
declare('pcall')
declare('xpcall')
function pcall(f,...) return true,f(unpack(arg)) end
function xpcall(f,e) return f() end
also, you'll need to add declare('TCP') etc to the top of my example code. Sorry about that. Also, I corrected some errors above. The write callback is called after an outgoing connection completes; the connection callback is only for incoming connections.
Thanks a1kon for your this great stuff, tcpsock.lua makes it real easy to write some nice stuff. For everybody who's interested, here's some example code I wrote based upon a1k0n's tcpsock.lua:
EchoServer - Runs an echo server in VO (returns everything you give to it):
http://vokb.svn.sourceforge.net/viewvc/vokb/vokb/client/trunk/echoserver.lua?view=markup
tcpsock.lua needs to be in the same directory, but it DOESN'T need to be modified.
All you have to do is:
/consoletoggle
/lua dofile('echoserver.lua')
/lua EchoServer.Start()
and then use your telnet client to connect to localhost on port 9999 and send something to it, then you'll get it back - extremely useful :-)
EDIT: Well, here's a second example, but this one is actually really useful:
http://vokb.svn.sourceforge.net/viewvc/vokb/vokb/client/trunk/http.lua?view=markup
The samples are now included in the file. GET works nicely, POST does too, except that you sometimes don't get anything back.
EchoServer - Runs an echo server in VO (returns everything you give to it):
http://vokb.svn.sourceforge.net/viewvc/vokb/vokb/client/trunk/echoserver.lua?view=markup
tcpsock.lua needs to be in the same directory, but it DOESN'T need to be modified.
All you have to do is:
/consoletoggle
/lua dofile('echoserver.lua')
/lua EchoServer.Start()
and then use your telnet client to connect to localhost on port 9999 and send something to it, then you'll get it back - extremely useful :-)
EDIT: Well, here's a second example, but this one is actually really useful:
http://vokb.svn.sourceforge.net/viewvc/vokb/vokb/client/trunk/http.lua?view=markup
The samples are now included in the file. GET works nicely, POST does too, except that you sometimes don't get anything back.
Thanks firsm. I started writing my own http layer this morning until I realised that you'd already done it. It's working fine for sending large amounts of POST data to Tomcat.
Apparently the listening socket (server) code doesn't work under Windows and maybe other platforms due to a dumb stubbed-out C++ server I have that isn't properly overridden sometimes. Next version will make listening sockets work.
a1k0n, it appears that the listening socket code segfaults under Windows whenever a client connects :/
Or at least under Windows.
Nevermind that I'm mixing segfaults and XP.
Or at least under Windows.
Nevermind that I'm mixing segfaults and XP.
Can you post/email me some code that exhibits this problem?
It looks like you called GetPeerName() on an TCP object that wasn't connected.
I'll fix it so it doesn't crash, but it will return nil instead.
It looks like you called GetPeerName() on an TCP object that wasn't connected.
I'll fix it so it doesn't crash, but it will return nil instead.
Regarding http://vokb.svn.sourceforge.net/viewvc/vokb/vokb/client/trunk/http.lua?view=markup
firsm, when I run your example code I get an error telling me that TCP is nil. I looked in the volb code and tried a bunch of things and I've come to the conclusion that my lua knowledge is not yet advanced enough to help me produce the proper line of code to allow it to work for me. Can you nudge me in the right direction please?
firsm, when I run your example code I get an error telling me that TCP is nil. I looked in the volb code and tried a bunch of things and I've come to the conclusion that my lua knowledge is not yet advanced enough to help me produce the proper line of code to allow it to work for me. Can you nudge me in the right direction please?
Make sure to convert the code for Lua 5.1; when firsm was working with that VO still used 5.0. (This is part of the reason the ingame VOKB client isn't working right now.)
Someone should fix that.
Thanks but I still get the same error message. Does it rely on another plugin that I missed? I have tcpsock.lua in the same directory. (has tcpsock.lua been updated?)
205: attempt to index global 'TCP' (a nil value)
205: attempt to index global 'TCP' (a nil value)
Try putting the rest of the TCPsock files that a1k0n links to in the folder if they're not there already; failing that, I honestly have no idea--I haven't worked with TCPsock yet, and only converted firsm's file to use Lua 5.1--Scuba might have a better idea.
I tried firsm's echoserver example which works fine, so I guess sockets work on my box in general.
http.lua isn't loading tcpsock.lua. You will have to do that, in addition to declaring the global "TCP".
Thank you nh, that was exactly the solution.