; docformat = 'rst'
;+
; The MGnetSocket class implements client and server-side internet sockets
; using the TCP/IP or UDP/IP protocols.
;
; This class was originally developed to provide UDP/IP support in IDL. While
; it provides a few methods that may be convienient, if you need TCP/IP client
; sockets you may want to consider the built in support in IDL.
;
; This class depends on the idl_net DLM which provides the interface to the OS
; sockets library. This DLM is based *heavily* on Randall Frank's `idl_sock.c`
; code distributed with his idl_tools DLM.
;
; Note on byteswapping: Make sure you swap in the correct place. If you are
; reading into the buffer, and you are extracting other than byte type you
; need to swap on the call to ReadBuffer. If you are simply receiving data via
; ::receive or extracting byte data from `::readBuffer` then you can swap on
; either the call to `::receive` or `::readBuffer`.
;
; :Todo:
; Should add in a network endianess property so that swapping is automagic.
; This would require Receive to be modified such that Rx'ing to buffer
; would not swap even if the local and remote endianess were different.
; Further, if receiving to buffer, data should not be returned to the
; caller so there is no confusion. If this endianess property is undefined
; (the default) no swapping is performed.
;
; :Categories:
; networking
;
; :Examples:
; Try the main-level program at the end of this file::
;
; IDL> .run mgnetsocket__define
;
; :History:
; Original written by Randall Frank in idl_sock.c in his idl_tools DLM
; Modified by Rick Towler, 20 July 2007
; Modified by Michael Galloy
;
; :Properties:
; localhost
; localport
; open
; true if the socket is open, false if not
; remotehost
; remoteport
; type
; type of connection: 'LISTEN_TCP', 'UDP', 'IO_TCP', 'PEERED_UDP'
;-
;+
; Connect to a TCP socket listener on a specified host and port or
; opens a UDP socket and sets its default destination host and port.
;-
function mgnetsocket::connect, destHost, $
destPort, $
buffer=buffer, $
localPort=locPort, $
nodelay=nodelay, $
udp=udp, $
tcp=tcp
compile_opt strictarr
; check if socket is currently open
if (self.sockId ge 0L) then self->close
; process keywords
locPort = (n_elements(locPort) ne 1) ? 0L : locPort
; resolve hostname and get hostid
if (size(destHost, /type) ne 13L) then begin
; destination host specified as string - resolve
self.hostId = mg_net_name2host(destHost[0])
if (self.hostId eq 0L) then begin
message, string(destHost[0], $
format='(%"unable to resolve hostname ''%s''")'), $
/continue
return, -2L
endif
endif else begin
; host ID has already been converted to ULONG
self.hostId = destHost[0]
endelse
if (keyword_set(udp)) then begin
; open UDP port and set default destination
sId = mg_net_connect(self.hostID, destPort[0], /udp, local_port=locPort)
if (sId lt 0) then begin
message, string(locPort, format='(%"unable to open port %d")'), /continue
return, -2L
endif
self.type = 3B
endif else begin
; connect using TCP sockets
if (n_elements(buffer) eq 1) then begin
sId = mg_net_connect(self.hostId, destPort[0], $
/tcp, local_port=locPort, $
nodelay=keyword_set(nodelay), buffer=buffer)
endif else begin
sId = mg_net_connect(self.hostId, destPort[0], $
/tcp, local_port=locPort, $
nodelay=keyword_set(nodelay))
endelse
if (sId lt 0L) then begin
message, string(destHost, destPort, $
format='(%"unable to connect to host %s at port %d")'), $
/continue
return, -2L
endif
self.type = 2B
endelse
; get local port if auto assigned
if (locPort eq 0L) then err = mg_net_query(sId, local_port=locPort)
self.destPort = destPort[0]
self.locPort = locPort
self.sockID = sId
*self.buffer = 0B
return, 1L
end
;+
; Creates a socket listening on the specified port. Socket can be either TCP
; or UDP based. Specify a local port of 0 to allow the OS to select an open
; port for you.
;
; :Returns:
;
; :Params:
; locPort : in, optional, type=long
; port to create; the OS will select an open port if this parameter is
; undefined or set to 0
;
; :Keywords:
; tcp : in, optional, type=boolean
; set to create a TCP port
; udp : in, optional, type=boolean
; set to create a UDP port
;-
function mgnetsocket::createPort, locPort, tcp=tcp, udp=udp
compile_opt strictarr
; check if socket is currently open
if (self.sockId ge 0L) then self->close
; provide default value for locPort
if (n_params() eq 0L) then locPort = 0L
if (keyword_set(udp)) then begin
; create UDP socket
sId = mg_net_createport(locPort[0], /udp)
self.type = 1B
endif else begin
; create LISTENER TCP socket
sId = mg_net_createport(locPort[0], /tcp)
self.type = 0B
endelse
; get local port if auto assigned
if (locPort eq 0) then err = mg_net_query(sId, local_port=locPort)
if (sId lt 0) then begin
message, string(locPort, format='(%"unable to open port %d")'), /continue
return, -2L
endif
self.locPort = locPort[0]
self.sockId = sId
*self.buffer = 0B
return, 1L
end
;+
; Accepts a requested TCP/IP connection and returns an MGnetSocket on which
; I/O can be performed.
;
; :Returns:
; MGnetSocket object
;
; :Keywords:
; buffer
; nodelay
; timeout : in, optional, type=long, default=0L
; timeout
;-
function mgnetsocket::accept, buffer=buffer, nodelay=nodelay, timeout=timeout
compile_opt strictarr
newSock = obj_new()
if (self.type gt 0) then begin
message, 'I/O sockets cannot accept connections', /continue
return, newSock
endif
; check if there are any pending connections
if (n_elements(timeout) eq 1L) then begin
pc = mg_net_select(self.sockID, timeout)
endif else begin
pc = mg_net_select(self.sockID, 0L)
endelse
if (pc gt 0L) then begin
; connection has been requested - accept
if (n_elements(buffer) eq 1L) then begin
newSId = mg_net_accept(self.sockId, nodelay=keyword_set(nodelay), $
buffer=buffer)
endif else begin
newSId = mg_net_accept(self.sockId, nodelay=keyword_set(nodelay))
endelse
if (newSId ge 0L) then begin
newSock = obj_new('MGnetSocket', newSId)
endif else begin
message, 'error accepting socket connection', /continue
return, newSock
endelse
endif else begin
; no connection pending
return, newSock
endelse
end
;+
; Send data.
;
; :Returns:
; number of bytes send
;
; :Params:
; data : in, required, type=array
; data to send
;-
function mgnetsocket::send, data
compile_opt strictarr
; check if socket is open
if (self.sockId lt 0L) then begin
message, 'socket is closed; cannot send data', /continue
return, 0L
endif
; check if we can send data
if (self.type eq 0L) then begin
message, 'cannot send data from a socket in a listening state', /continue
return, 0L
endif
; send the data
if (self.type gt 1L) then begin
ns = mg_net_send(self.sockId, data)
endif else begin
message, 'UDP Socket is not peered; use the sendTo method', /continue
return, 0L
endelse
return, ns
end
;+
; Send data to a specified host and port.
;
; :Returns:
; number of bytes sent
;
; :Params:
; data : in, required, type=array
; data to send
; destHost : in, required, type=string or ulong
; host to sent data to specified as a hostname or host identifier
; destPort : in, required, type=long
; host port
;-
function mgnetsocket::sendTo, data, destHost, destPort
compile_opt strictarr
; check if socket is open
if (self.sockId lt 0L) then begin
message, 'socket is closed; cannot send data', /continue
return, 0L
endif
; check if we can send data
if (self.type eq 0L) then begin
message, 'cannot send data from a socket in a listening state', /continue
return, 0L
endif
if (size(destHost, /type) ne 13L) then begin
; destination host specified as string - resolve
destHost = self->name2Host(destHost)
endif
; send the data
ns = mg_net_sendto(self.sockId, data, destHost, destPort)
return, ns
end
;+
; Receive data.
;
; :Returns:
; number of bytes received as a long; errors will return a negative value
;
; :Keywords:
; byteswap : in, optional, type=boolean
; set to swap the byte order of the returned data
; data : out, optional, type=array
; set to a named variable to return the received data
; tobuffer : in, optional, type=boolean
; set to put data in buffer to be read later by readBuffer method
;-
function mgnetsocket::receive, byteswap=byteswap, $
data=data, $
tobuffer=tobuffer
compile_opt strictarr
if (self.sockId lt 0L) then begin
message, 'socket is closed, nothing to read.', /continue
return, -2L
endif
; check if we can receive data
if (self.type eq 0) then begin
message, 'cannot receive data from a socket in a listening state', /continue
return, -2L
endif
; check for data in the socket buffer
err = mg_net_query(self.sockId, available_bytes=nr)
if (nr eq 0L) then return, nr
; read data from socket buffer
nr = mg_net_recv(self.sockId, data)
; swap, if requested
if (keyword_set(byteswap)) then swap_endian_inplace, data
if (keyword_set(tobuffer)) then begin
; copy data to buffer
if (self.bsize eq 0L) then begin
*self.buffer = data
endif else begin
; TODO: probably should implement more efficient buffering here...
*self.buffer = [*self.buffer, data]
endelse
self.bsize += nr
endif else begin
; simply return data to caller
*self.buffer = 0B
self.bsize = 0L
endelse
return, nr
end
;+
; Return data stored in the local (object's) buffer. This method can be used
; to return specific data types, even mixed types, from the buffer by
; specifying the type.
;
; Returns NaN if the buffer is empty.
;
; :Returns:
; array of the type specified by the TYPE keyword
;
; :Params:
; nbytes : out, optional, type=long
; number of bytes read
;
; :Keywords:
; type : in, optional, type=string, default=''
; type of data: 'integer', 'double', 'float', 'long', 'string', 'uint',
; 'ulong', 'l64', or 'ul64'; if not specified or not one of the above
; types, data is returned as bytes
; peek : in, optional, type=boolean
; set the peek keyword to "take a peek" at data but not remove it from
; the buffer
; skipbytes : in, optional, type=long
; bytes to skip at the beginning of the buffer
; byteswap : in, optional, type=boolean
; set to swap endianness of data
; nels : in, out, optional, type=long
; the number of elements read; if not specified, as many elements as
; possible are read
;-
function mgnetsocket::readBuffer, nbytes, $
byteswap=byteswap, $
nels=nels, $
peek=peek, $
skipbytes=skipbytes, $
type=type
compile_opt strictarr
_type = (n_elements(type) gt 0) ? type[0] : ''
nels = (n_elements(nels) gt 0) ? nels[0] : -1L
_skipbytes = (n_elements(skipbytes) gt 0L) ? skipbytes[0] : 0L
ts = 1L
; check if there is any more data unread in the buffer
if (self.bsize le 0L) then begin
nbytes = 0L
return, !values.f_nan
endif
; extract requested data by type
case strlowcase(_type) of
'integer': begin
ts = 2L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1L)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = fix((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'double': begin
ts = 8L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1L)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = double((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'float': begin
ts = 4L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1L)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = float((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'long': begin
ts = 4L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1L)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = long((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'string': begin
if (nels lt 0L) then nels = self.bsize
eidx = 0L > nels - 1L < (self.bsize - 1L)
data = string((*self.buffer)[_skipbytes:eidx])
end
'uint': begin
ts = 2L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = uint((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'ulong': begin
ts = 4L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1 < (self.bsize - 1)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = ulong((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'l64': begin
ts = 8L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1L)
eidx -= (eidx + 1L) mod ts
nels = (eidx + 1L) / ts
data = long64((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
'ul64': begin
ts = 8L
if (nels lt 0L) then nels = self.bsize / ts
eidx = 0L > (nels * ts) - 1L < (self.bsize - 1L)
eidx -= (eidx + 1) mod ts
nels = (eidx + 1) / ts
data = ulong64((*self.buffer)[_skipbytes:eidx], 0L, nels)
end
else: begin
if (nels lt 0L) then nels = n_elements(*self.buffer)
eidx = 0L > nels - 1L < (self.bsize - 1L)
data = (*self.buffer)[_skipbytes:eidx]
end
endcase
; swap, if requested
if (keyword_set(byteswap)) then swap_endian_inplace, data
nbytes = nels * ts
if (~keyword_set(peek)) then begin
if (eidx eq self.bsize -1) then begin
*self.buffer = 0B
self.bsize = 0L
endif else begin
*self.buffer = (*self.buffer)[eidx + 1L:self.bsize - 1L]
self.bsize = self.bsize - (nbytes + _skipbytes)
endelse
end
return, data
end
;+
; Return a host ID as ULONG given a string hostname.
;
; :Returns:
; long
;
; :Params:
; name : in, optional, type=string
; hostname
;-
function mgnetsocket::name2Host, name
compile_opt strictarr
if (size(name, /type) ne 7L) then begin
message, 'hostname must be passed as a string'
endif
if (n_params() eq 0L) then begin
hostId = mg_net_name2host()
endif else begin
hostid = mg_net_name2host(name)
endelse
if (hostID eq 0L) then begin
message, string(name, format='(%"unable to resolve hostname ''%s''")'), $
/continue
endif
return, hostId
end
;+
; Return a hostname as string given a ULONG host ID.
;
; :Returns:
; string
;
; :Params:
; hostId : in, optional, type=ulong
; host identifier
;-
function mgnetsocket::host2Name, hostId
compile_opt strictarr
on_error, 2
if (size(hostID, /type) ne 13L) then begin
message, 'host ID must be passed as ULONG'
endif
if (n_params() eq 0L) then begin
name = mg_net_host2name()
endif else begin
name = mg_net_host2name(hostID)
endelse
if (strcmp(name, '')) then begin
message, 'unable to resolve hostname from host ID', /continue
endif
return, name
end
;+
; Return the number of bytes waiting in the socket buffer.
;
; :Returns:
; long
;-
function mgnetsocket::check
compile_opt strictarr
err = mg_net_query(self.sockId, available_bytes=nBytes)
if (err lt 0L) then nBytes = 0L
return, nBytes
end
;+
; Close a socket.
;-
pro mgnetsocket::close
compile_opt strictarr
; close UDP port if open
if (self.sockId ge 0L) then err = mg_net_close(self.sockId)
; reset porperties
self.destPort = 0L
self.hostId = 0UL
self.locPort = 0L
self.sockId = -1L
*self.buffer = 0B
end
;+
; Set properties of the socket.
;-
pro mgnetsocket::setProperty
compile_opt strictarr
; There aren't any properties that are settable by the user at this point.
; The network endianess prop is one that would be.
end
;+
; Get properties of the socket.
;-
pro mgnetsocket::getProperty, localhost=locHost, $
localport=locPort, $
open=open, $
remotehost=destHost, $
remoteport=destPort, $
type=type
compile_opt strictarr
destPort = self.destPort
locPort = self.locPort
case self.type of
0: type = 'LISTEN_TCP'
1: type = 'UDP'
2: type = 'IO_TCP'
3: type = 'PEERED_UDP'
endcase
if (arg_present(locHost)) then begin
err = mg_net_query(self.sockId, local_host=lh)
locHost = mg_net_host2name(lh)
endif
if (arg_present(destHost)) then begin
err = mg_net_query(self.sockID, remote_host=rh)
destHost = mg_net_host2name(rh)
endif
open = self.sockId ge 0L ? 1L : 0L
end
;+
; Free resources held by the socket object.
;-
pro mgnetsocket::cleanup
compile_opt strictarr
; close the socket if open
if (self.sockId ge 0) then self->close
; free the lone pointer
ptr_free, self.buffer
end
;+
; Create a socket object.
;
; :Returns:
; 1 for success, 0 for failure
;
; :Params:
; sockId : in, optional, type=long
; socket identifier
;-
function mgnetsocket::init, sockId
compile_opt strictarr
if (n_params() gt 0) then begin
; internal call from MGnetSocket::accept
err = mg_net_query(sockId, $
local_port=self.locPort, $
remote_host=self.hostID, $
remote_port=self.destPort)
if (err lt 0L) then return, 0
self.sockId = sockId
self.type = 2B
endif else begin
self.sockId = -1L
endelse
self.buffer = ptr_new(0B)
return, 1
end
;+
; Defines instance variables.
;
; :Fields:
; buffer
; buffer of read data
; bsize
; size of buffer in bytes
; sockId
; socket identififer
; type
; type code: 0 => 'LISTEN_TCP', 1 => 'UDP', 2 => 'IO_TCP', 3 =>
; 'PEERED_UDP'
; hostId
; host identifier
; locPort
; local port
; destPort
; destination port
;-
pro mgnetsocket__define
compile_opt strictarr
define = { MGnetSocket, $
buffer: ptr_new(), $
bsize: 0L, $
sockId: 0L, $
type: 0B, $
hostId: 0UL, $
locPort: 0L, $
destPort: 0L $
}
end
; main-level example
; This is a simple example that requests time via TCP port 37 taken from a
; recent thread on comp.lang.idl-pvwave.
;
; Note that time-a.nist.gov doesn't always return data so it may time out on
; you. Just try again.
;
; The point is not to actually get the actual time, just an example of how to
; do this using MGnetSocket. This also illustrates a problem that is probably
; best handled via IDL's built in client side socket support.
timeServer = 'time-a.nist.gov'
timePort = 37
; connect to the time server
timeSock = obj_new('MGnetSocket')
ok = timeSock->connect(timeServer, timePort, /tcp)
timeSock->getProperty, localport=locPort, localhost=locHost
print, locHost, locPort, format='(%"Local TCP socket created on %s port %d")'
; wait for the time - MGnetSocket performs non-blocking reads
timeout = 0
while (timeSock->check() eq 0L) do begin
wait, 0.1
timeout += 0.1
if (timeout gt 5.) then break
endwhile
; receive the time - store in MGnetSocket's buffer
nBytes = timeSock->receive(/tobuffer)
print, nBytes, timeServer, format='(%"Received %d bytes from %s")'
if (nBytes gt 0L) then begin
; time is sent as an unsigned 32bit integer - need to byteswap
time = timeSock->readBuffer(/byteswap, nels=1, type='ulong')
print, time / 60. / 60. / 24. / 365.26, $
format='(%"Approximate number of years since Midnight Jan 1, 1900: %f")'
endif
timeSock->close
end