UDP is a datagram protocol, and the best API for it is a direct event model. 29Nov11 CMcC
I have written a DNS client using the extension. DNS over UDP extension
RLH: Why "Udp" and not "udp"? That just looks strange and everytime I would go to type it I would type it wrong.
Small test script for Udp package
lappend auto_path [pwd]/Udp
package require Udp
puts stderr [info commands ::udp::*]
proc moop {lport datagram remote port lchan} {
if {[catch {
puts stderr "Moop: $lchan:$lport '$datagram' $remote:$port"
} e eo]} {
puts stderr "$e ($eo)"
}
}
puts stderr server:[set server [::udp create 9999 {moop 9999}]]
puts stderr sopts:[fconfigure $server]
puts stderr client:[set client [::udp create]]
set count 0
time {::udp send $client localhost 9999 MOOOOOOOP[incr count]} 10
#close $client
time {after 1 ::udp::send $client localhost 9999 MOOOOOOOP[incr count]} 10
vwait forever
udp.tcl - critcl udp extension - make with critcl3.kit -libdir $PWD -pkg Udp udp.tcl
# UDP an extension to provide minimal UDP support to Tcl using direct events for reception
package provide Udp 1.3
if {[info commands ::udp::send] ne ""} {
namespace eval ::udp {
proc close {udp} {
chan close $udp
}
namespace export -clear *
namespace ensemble create -subcommands {}
}
return
}
package require critcl
::critcl::tsources udp.tcl
critcl::ccode {
/* UDP client in the internet domain */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <tcl.h>
#include <errno.h>
static char errBuf[256];
/*
* This structure describes per-instance state
* of a udp channel.
*
*/
typedef struct udpState {
int sock; /* inderlying (tcp) file descriptor */
Tcl_Obj *script; /* script prefix for incoming */
Tcl_Interp *interp; /* interp this was instantiated in */
Tcl_Channel channel; /* associated chan */
int addr; /* local bound address */
uint16_t port; /* local bound port */
int multicast; /* indicator set for multicast add */
Tcl_Obj *groupsObj; /* list of the mcast groups */
} UdpState;
/* ----------------------------------------------------------------------
*
* LSearch --
*
* Find a string item in a list and return the index of -1.
*/
static int
LSearch(Tcl_Obj *listObj, const char *group)
{
int objc, n;
Tcl_Obj **objv;
Tcl_ListObjGetElements(NULL, listObj, &objc, &objv);
for (n = 0; n < objc; n++) {
if (strcmp(group, Tcl_GetString(objv[n])) == 0) {
return n;
}
}
return -1;
}
/*
* ----------------------------------------------------------------------
*
* UdpMulticast --
*
* Action should be IP_ADD_MEMBERSHIP | IP_DROP_MEMBERSHIP
*
*/
static int
UdpMulticast(ClientData instanceData, Tcl_Interp *interp,
const char *grp, int action)
{
UdpState *statePtr = (UdpState *)instanceData;
struct ip_mreq mreq;
struct hostent *name;
memset(&mreq, 0, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(grp);
if (mreq.imr_multiaddr.s_addr == -1) {
name = gethostbyname(grp);
if (name == NULL) {
Tcl_SetResult(interp, "invalid group name", TCL_STATIC);
return TCL_ERROR;
}
memcpy(&mreq.imr_multiaddr.s_addr, name->h_addr,
sizeof(mreq.imr_multiaddr));
}
mreq.imr_interface.s_addr = INADDR_ANY;
if (setsockopt(statePtr->sock, IPPROTO_IP, action,
(const char*)&mreq, sizeof(mreq)) < 0) {
Tcl_SetResult(interp, "error changing multicast group", TCL_STATIC);
return TCL_ERROR;
}
if (action == IP_ADD_MEMBERSHIP) {
int ndx = LSearch(statePtr->groupsObj, grp);
if (ndx == -1) {
statePtr->multicast++;
Tcl_ListObjAppendElement(interp, statePtr->groupsObj,
Tcl_NewStringObj(grp,-1));
}
} else {
int ndx = LSearch(statePtr->groupsObj, grp);
if (ndx != -1) {
Tcl_Obj *old, *ptr;
int dup = 0;
old = ptr = statePtr->groupsObj;
statePtr->multicast--;
if ((dup = Tcl_IsShared(ptr))) {
ptr = Tcl_DuplicateObj(ptr);
}
Tcl_ListObjReplace(interp, ptr, ndx, 1, 0, NULL);
if (dup) {
statePtr->groupsObj = ptr;
Tcl_IncrRefCount(ptr);
Tcl_DecrRefCount(old);
}
}
}
if (interp != NULL)
Tcl_SetObjResult(interp, statePtr->groupsObj);
return TCL_OK;
}
/*
*----------------------------------------------------------------------
*
* udpClose --
*
* This function is invoked by the generic IO level to perform
* channel-type-specific cleanup when a UDP socket based channel is
* closed.
*
* Results:
* 0 if successful, the value of errno if failed.
*
* Side effects:
* Closes the socket of the channel.
*
*----------------------------------------------------------------------
*/
static int udpClose(
ClientData instanceData, /* The socket to close. */
Tcl_Interp *interp) /* For error reporting - unused. */
{
int objc;
Tcl_Obj **objv;
int errorCode = 0;
UdpState *state = (UdpState *) instanceData;
/*
* If there are multicast groups added they should be dropped.
*/
if (state->groupsObj) {
int n = 0;
Tcl_ListObjGetElements(interp, state->groupsObj, &objc, &objv);
for (n = 0; n < objc; n++) {
UdpMulticast((ClientData)state, interp,
Tcl_GetString(objv[n]), IP_DROP_MEMBERSHIP);
}
Tcl_DecrRefCount(state->groupsObj);
}
if (state->script) {
Tcl_DecrRefCount(state->script);
}
if (close(state->sock) < 0) {
errorCode = Tcl_GetErrno();
}
Tcl_Free((char*)state);
return errorCode;
}
/*
*----------------------------------------------------------------------
*
* udpInput --
*
* This function is invoked by the generic IO level to read input from a
* UDP socket based channel. It is meaningless for UDP
*
* Results: EINVAL
*
*----------------------------------------------------------------------
*/
static int
udpInput(
ClientData instanceData, /* Socket state. */
char *buf, /* Where to store data read. */
int bufSize, /* How much space is available in the buffer? */
int *errorCodePtr) /* Where to store error code. */
{
*errorCodePtr = EINVAL;
return -1;
}
/*
*----------------------------------------------------------------------
*
* udpOutput --
*
* This function is invoked by the generic IO level to write output to a
* UDP socket based channel. It is meaningless for UDP.
*
* Results: EINVAL
*
*----------------------------------------------------------------------
*/
static int
udpOutput(
ClientData instanceData, /* Socket state. */
const char *buf, /* The data buffer. */
int toWrite, /* How many bytes to write? */
int *errorCodePtr) /* Where to store error code. */
{
*errorCodePtr = EINVAL;
return -1;
}
/*
*----------------------------------------------------------------------
*
* udpGetOption --
*
* Computes an option value for a UDP socket based channel, or a list of
* all options and their values.
*
* Note: This code is based on code contributed by John Haxby.
*
* Results:
* A standard Tcl result. The value of the specified option or a list of
* all options and their values is returned in the supplied DString. Sets
* Error message if needed.
*
* Side effects:
* None.
*
*----------------------------------------------------------------------
*/
static int
udpGetOption(ClientData instanceData, Tcl_Interp *interp,
char *optionName, Tcl_DString *optionValue)
{
UdpState *statePtr = (UdpState *)instanceData;
CONST84 char * options[] = { "myport", "mcastgroups", "broadcast", "ttl", NULL};
int r = TCL_OK;
if (optionName == NULL) {
Tcl_DString ds;
const char **p;
Tcl_DStringInit(&ds);
for (p = options; *p != NULL; p++) {
char op[16];
sprintf(op, "-%s", *p);
Tcl_DStringSetLength(&ds, 0);
udpGetOption(instanceData, interp, op, &ds);
Tcl_DStringAppend(optionValue, " ", 1);
Tcl_DStringAppend(optionValue, op, -1);
Tcl_DStringAppend(optionValue, " ", 1);
Tcl_DStringAppendElement(optionValue, Tcl_DStringValue(&ds));
}
} else {
Tcl_DString ds, dsInt;
Tcl_DStringInit(&ds);
Tcl_DStringInit(&dsInt);
if (!strcmp("-myport", optionName)) {
Tcl_DStringSetLength(&ds, TCL_INTEGER_SPACE);
sprintf(Tcl_DStringValue(&ds), "%u", ntohs(statePtr->port));
} else if (!strcmp("-mcastgroups", optionName)) {
int objc, n;
Tcl_Obj **objv;
Tcl_ListObjGetElements(interp, statePtr->groupsObj, &objc, &objv);
for (n = 0; n < objc; n++) {
Tcl_DStringAppendElement(&ds, Tcl_GetString(objv[n]));
}
} else if (!strcmp("-broadcast", optionName)) {
int tmp = 1;
socklen_t optlen = sizeof(int);
if (getsockopt(statePtr->sock, SOL_SOCKET, SO_BROADCAST,
(char *)&tmp, &optlen)) {
/*UDPTRACE("UDP error - getsockopt\n");*/
Tcl_SetResult(interp, "error in getsockopt", TCL_STATIC);
r = TCL_ERROR;
} else {
Tcl_DStringSetLength(&ds, TCL_INTEGER_SPACE);
sprintf(Tcl_DStringValue(&ds), "%d", tmp);
}
} else if (!strcmp("-ttl", optionName)) {
unsigned int tmp = 0;
socklen_t optlen = sizeof(unsigned int);
int cmd = IP_TTL;
if (statePtr->multicast > 0)
cmd = IP_MULTICAST_TTL;
if (getsockopt(statePtr->sock, IPPROTO_IP, cmd,
(char *)&tmp, &optlen)) {
/*UDPTRACE("UDP error - getsockopt");*/
Tcl_SetResult(interp, "error in getsockopt", TCL_STATIC);
r = TCL_ERROR;
} else {
Tcl_DStringSetLength(&ds, TCL_INTEGER_SPACE);
sprintf(Tcl_DStringValue(&ds), "%u", tmp);
}
} else {
CONST84 char **p;
Tcl_DString tmp;
Tcl_DStringInit(&tmp);
for (p = options; *p != NULL; p++)
Tcl_DStringAppendElement(&tmp, *p);
r = Tcl_BadChannelOption(interp, optionName, Tcl_DStringValue(&tmp));
Tcl_DStringFree(&tmp);
}
if (r == TCL_OK) {
Tcl_DStringAppend(optionValue, Tcl_DStringValue(&ds), -1);
}
Tcl_DStringFree(&dsInt);
Tcl_DStringFree(&ds);
}
return r;
}
/*
* ----------------------------------------------------------------------
* udpGetService --
*
* Return the service port number in network byte order from either a
* string representation of the port number or the service name. If the
* service string cannot be converted (ie: a name not present in the
* services database) then set a Tcl error.
* ----------------------------------------------------------------------
*/
static int
udpGetService(Tcl_Interp *interp, const char *service,
unsigned short *servicePort)
{
struct servent *sv = NULL;
char *remainder = NULL;
int r = TCL_OK;
sv = getservbyname(service, "udp");
if (sv != NULL) {
*servicePort = sv->s_port;
} else {
*servicePort = htons((unsigned short)strtol(service, &remainder, 0));
if (remainder == service) {
Tcl_ResetResult(interp);
Tcl_AppendResult(interp, "invalid service name: \"", service,
"\" could not be converted to a port number",
TCL_STATIC);
r = TCL_ERROR;
}
}
return r;
}
/*
* ----------------------------------------------------------------------
* udpSetOption --
*
* Handle channel configuration requests from the generic layer.
*
* ----------------------------------------------------------------------
*/
static int
udpSetOption(ClientData instanceData, Tcl_Interp *interp,
char *optionName, char *newValue)
{
UdpState *statePtr = (UdpState *)instanceData;
char * options = "remote mcastadd mcastdrop broadcast ttl";
int r = TCL_OK;
if (!strcmp("-mcastadd", optionName)) {
r = UdpMulticast(instanceData, interp,
(const char *)newValue, IP_ADD_MEMBERSHIP);
} else if (!strcmp("-mcastdrop", optionName)) {
r = UdpMulticast(instanceData, interp,
(const char *)newValue, IP_DROP_MEMBERSHIP);
} else if (!strcmp("-broadcast", optionName)) {
int tmp = 1;
r = Tcl_GetInt(interp, newValue, &tmp);
if (r == TCL_OK) {
if (setsockopt(statePtr->sock, SOL_SOCKET, SO_BROADCAST,
(const char *)&tmp, sizeof(int))) {
Tcl_AppendResult(interp,
"setsockopt broadcast \"", Tcl_ErrnoMsg(Tcl_GetErrno()), "\"",
(char *) NULL);
r = TCL_ERROR;
} else {
Tcl_SetObjResult(interp, Tcl_NewIntObj(tmp));
}
}
} else if (!strcmp("-ttl", optionName)) {
unsigned int tmp = 0;
int cmd = IP_TTL;
if (statePtr->multicast > 0)
cmd = IP_MULTICAST_TTL;
r = Tcl_GetInt(interp, newValue, &tmp);
if (r == TCL_OK) {
if (setsockopt(statePtr->sock, IPPROTO_IP, cmd,
(const char *)&tmp, sizeof(unsigned int))) {
Tcl_AppendResult(interp,
"setsockopt ttl \"", Tcl_ErrnoMsg(Tcl_GetErrno()), "\"",
(char *) NULL);
r = TCL_ERROR;
} else {
Tcl_SetObjResult(interp, Tcl_NewIntObj(tmp));
}
}
} else {
r = Tcl_BadChannelOption(interp, optionName, options);
}
return r;
}
/*
*----------------------------------------------------------------------
*
* udpNotifyChannel --
*
* This procedure is called by a channel driver when a driver detects an
* event on a channel. This procedure is responsible for actually
* handling the event by invoking any channel handler callbacks.
*
* Results:
* None.
*
* Side effects:
* Whatever the channel handler callback procedure does.
*
*----------------------------------------------------------------------
*/
void
udpNotifyChannel(UdpState *state, int mask)
{
int n,s, argc;
Tcl_Obj **argv;
Tcl_Obj *result = Tcl_DuplicateObj(state->script);
Tcl_Interp *interp = state->interp;
struct sockaddr_in from;
socklen_t fromlen;
char buf[1024];
fromlen = sizeof(struct sockaddr_in);
n = recvfrom(state->sock,buf,1024,0,(struct sockaddr *)&from,&fromlen);
if (n < 0) {
/* error in reception - got to report */
Tcl_ListObjAppendElement(interp, result,
Tcl_NewStringObj(Tcl_ErrnoMsg(Tcl_GetErrno()), -1));
} else {
unsigned char addrbuf[sizeof(struct in6_addr)];
Tcl_ListObjAppendElement(interp, result, Tcl_NewByteArrayObj(buf, n));
if (inet_ntop(AF_INET, (void*)&from.sin_addr, addrbuf, sizeof(struct in6_addr))) {
Tcl_ListObjAppendElement(interp, result, Tcl_NewStringObj(addrbuf,-1));
} else {
Tcl_ListObjAppendElement(interp, result,
Tcl_NewStringObj(Tcl_ErrnoMsg(Tcl_GetErrno()),-1));
}
Tcl_ListObjAppendElement(interp, result,
Tcl_NewIntObj(ntohs(from.sin_port)));
Tcl_ListObjAppendElement(interp, result,
Tcl_NewStringObj(Tcl_GetChannelName(state->channel),-1));
}
Tcl_ListObjGetElements(interp, result, &argc, &argv);
Tcl_EvalObjv(interp, argc, argv, TCL_EVAL_DIRECT|TCL_EVAL_GLOBAL);
}
/*
*----------------------------------------------------------------------
*
* udpWatch --
*
* Initialize the notifier to watch the sock from this channel.
*
* Results:
* None.
*
* Side effects:
* Sets up the notifier so that a future event on the channel will be
* seen by Tcl.
*
*----------------------------------------------------------------------
*/
static void
udpWatch(
ClientData instanceData, /* The socket state. */
int mask) /* Events of interest; an OR-ed combination of
* TCL_READABLE, TCL_WRITABLE and
* TCL_EXCEPTION. */
{}
/*
*----------------------------------------------------------------------
*
* udpGetHandle --
*
* Called from Tcl_GetChannelHandle to retrieve OS handles from inside a
* UDP socket based channel.
*
* Results: EINVAL
*
* Side effects:
* None.
*
*----------------------------------------------------------------------
*/
static int
udpGetHandle(
ClientData instanceData, /* The socket state. */
int direction, /* Not used. */
ClientData *handlePtr) /* Where to store the handle. */
{
UdpState *state = (UdpState *) instanceData;
return state->sock;
}
static Tcl_ChannelType udp_chantype = {
"udp", /* Type name. */
NULL, /* Set blocking/nonblocking behaviour. NULL'able */
udpClose, /* Close channel, clean instance data */
udpInput, /* Handle read request */
udpOutput, /* Handle write request */
NULL, /* Move location of access point. NULL'able */
udpSetOption, /* Set options. NULL'able */
udpGetOption, /* Get options. NULL'able */
udpWatch, /* Initialize notifier */
udpGetHandle, /* Get OS handle from the channel. */
};
}
namespace eval ::udp {
critcl::cproc send {Tcl_Interp* interp char* udp char* destination long port Tcl_Obj* dgram} ok {
int n, dglen;
char *dgb;
Tcl_Channel chan;
struct sockaddr_in addr;
struct hostent *hp;
UdpState *state;
chan = Tcl_GetChannel(interp, udp, NULL); /* The channel to send on. */
if (chan == (Tcl_Channel) NULL) {
return TCL_ERROR;
}
state = Tcl_GetChannelInstanceData(chan);
hp = gethostbyname(destination);
if (hp==0) {
Tcl_AppendResult(interp, "Unknown host \"", destination, "\"", (char *) NULL);
return TCL_ERROR;
}
memcpy((char *)&addr.sin_addr, (char *)hp->h_addr, hp->h_length);
addr.sin_port = htons(port);
addr.sin_family = AF_INET;
dgb = Tcl_GetByteArrayFromObj(dgram, &dglen);
n=sendto(state->sock, dgb, dglen, 0, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (n != dglen) {
Tcl_AppendResult(interp, "sendto error \"", Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", (char *) NULL);
return TCL_ERROR;
}
return TCL_OK;
}
critcl::ccommand create {clientdata interp objc objv} {
int length;
static int udp_count = 0;
char channelName[24];
struct sockaddr_in addr;
UdpState *state = (UdpState *) Tcl_Alloc((unsigned) sizeof(UdpState));
Tcl_Channel chan;
state->interp = interp;
state->sock = socket(AF_INET, SOCK_DGRAM, 0);
#if HAVE_FLAG_FD_CLOEXEC
fcntl(state->sock, F_SETFD, FD_CLOEXEC);
#endif
if (state->sock < 0) {
Tcl_AppendResult(interp, "Opening udp socket \"", Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", NULL);
Tcl_Free((char*)state);
return TCL_ERROR;
}
state->groupsObj = Tcl_NewListObj(0, NULL);
state->script = NULL;
if (objc > 1) {
/* get port */
state->port = 0;
if (udpGetService(interp, Tcl_GetStringFromObj(objv[1], NULL), &state->port) != TCL_OK) {
udpClose(state,interp);
return TCL_ERROR;
}
if (objc == 4) {
/* set address and script */
const char *host = Tcl_GetStringFromObj(objv[2], NULL);
struct hostent *hp = gethostbyname(host);
if (hp == 0) {
Tcl_AppendResult(interp, "Host unknown \"", Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", NULL);
udpClose(state,interp);
return TCL_ERROR;
}
memcpy((char *)&state->addr, (char *)hp->h_addr, hp->h_length);
state->script = Tcl_DuplicateObj(objv[3]); /* record script prefix */
} else if (objc == 3) {
/* set script */
state->addr = INADDR_ANY;
state->script = Tcl_DuplicateObj(objv[2]); /* record script prefix */
} else {
Tcl_WrongNumArgs(interp, 1, objv, "udp create port ?addr? script");
udpClose(state,interp);
return TCL_ERROR;
}
length = sizeof(addr);
memset(&addr,0,length);
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=state->addr;
addr.sin_port=state->port;
if (bind(state->sock,(struct sockaddr *)&addr,length)<0) {
Tcl_AppendResult(interp, "Bind \"", Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", (char *) NULL);
udpClose(state,interp);
return TCL_ERROR;
}
/* generate events on socket readable */
Tcl_CreateFileHandler(state->sock, TCL_READABLE,
(Tcl_FileProc *) udpNotifyChannel,
(ClientData) state);
}
sprintf(channelName, "udp_%d", udp_count++);
chan = Tcl_CreateChannel(&udp_chantype, channelName, (ClientData)state, 0);
if (chan == (Tcl_Channel)NULL) {
close(state->sock);
Tcl_Free((char*)state);
return TCL_ERROR;
}
Tcl_RegisterChannel(interp, chan);
state->channel = chan;
Tcl_SetResult(interp, channelName, TCL_VOLATILE);
return TCL_OK;
}
}
There's also a little syslog client:
# syslog using Udp extension
# Colin McCormack
lappend auto_path [pwd]/Udp
package require Udp
package require TclOO
package provide Syslog 1.0
oo::class create Syslog {
method timestamp {time} {
# return a timestamp of $time
if {![string is integer -strict $time]} {
set time [clock scan $time -timezone :UTC]
}
return [clock format $time -format "%Y-%m-%dT%H:%M:%SZ"]
}
method log {message args} {
variable template
set props [dict merge $template $args]
foreach {p script} {
timestamp {clock seconds}
} {
if {![dict exists $props $p]} {
dict set props $p [eval $script]
}
}
dict with props {
set timestamp [my timestamp $timestamp]
if {![string is integer -strict $facility]} {
variable facilities
set facility [dict get $facilities $facility]
}
if {![string is integer -strict $priority]} {
variable priorities
set priority [dict get $priorities $priority]
}
set PRI [expr {($facility * 8) + $priority}]
set line "<$PRI>1 $timestamp $hostname $appname $procid $msgid - $message"
#puts stderr $line
}
variable syslog; variable server; variable port
::udp send $syslog $server $port $line
}
destructor {
variable syslog
catch {chan close $syslog}
}
constructor {args} {
variable facility user
variable priority debug
variable hostname [info host]
variable procid [pid]
variable appname $::argv0
variable msgid -
variable template {}
variable port 514 ;# syslog's port
variable {*}$args
if {![info exists server]} {
error "must specify Syslog server"
}
variable syslog [::udp create]
foreach v {hostname procid appname msgid facility priority} {
if {![dict exists $template $v]} {
dict set template $v [set $v]
}
}
variable facilities
set i 0
foreach f {
kern user mail daemon auth syslog
lrp news uucp cron authpriv ftp ntp audit alert clock
local0 local1 local2 local3 local4 local5 local6 local7
} {
dict set facilities $f $i
dict set facilities $i $f
incr i
}
variable priorities
set i 0
foreach f {emergency alert critical error warning notice info debug} {
dict set priorities $f $i
dict set priorities $i $f
incr i
}
}
}
if {[info exists argv0] && ($argv0 eq [info script])} {
Syslog create syslog server box
syslog log "This is a test"
}