Simple Comet framework
Posted: Thu Jun 17, 2010 10:53 am
Comet is a technique for sending stuff from a web server to client pages whenever the server wants to send it - not when the client has just requested it. It's ideal for newsfeeds, chatrooms, status updates and the like.
I've been curious about using Comet on my website since I first read about it in 2008. However, plagued by the apprehension of the gutless beginner, I've postponed "taking the dive" until now - which is ridiculous because Comet could be very useful for my website. And perhaps, yours too!
This implementation (which I've successfully tested with IE, Chrome and Opera) has three components:
There are other ways to do it, of course. I've done it this way because we end up with a generic framework which can be easily used in different ways. For example...
To send information to one visitor:
To send information to all visitors who are on a particular page:
To send information to all visitors on your site:
When the webpage opens the XMLHttpPost connection, it optionally sends a variable containing whatever information you want. This is passed to the slave exe, which passes it to the main server program. Thus, you can use it to filter out visitors according to whether their info contains a certain string. CometPage() and CometCast() take this as an optional parameter.
There's also another procedure, CometCast_OncePerIP(). This does the same as CometCast() except that it only sends the data to one page for each IP address. This could be used, for example, in the following situation: you're sending an alert out to everyone, and someone has two (or more) pages open on your site. Using CometCast(), the alert would show on every page they have open. Using CometCast_OncePerIP(), it will only appear on one page that they have open. Perhaps this could be finetuned so as to target the page they most recently opened, as that's what they're likely to be looking at.
So anyway, here's a sample webpage:
Obviously you'll want to change the GenericCometResponder() function in practice, but it'll do for this demo (hopefully).
Here's the code for the slave executable, which you should compile as a console program called "comet.exe" in your site's local folder. Change the "cometfolder" variable to something that suits you, but make sure the folder exists!
Here's the PBI for your server program. This program should be threadsafe and running all the time.
Before the line where you include this PBI, create a global string variable "cometfolder.s" and use it to store the directory where Comet request files will be saved by the slave executable(s). This variable should be identical in the slave code above.
In your main program loop, call the ReviewCometClients macro regularly - it watches the cometfolder for new requests.
That's it. All you need to do now is call any of the procedures when you want to send some information to your visitors.
Here's an example "server program"... make sure to compile it as threadsafe.Run that, open your test page, and hit your C and P keys!
Hope this is useful to someone,
Seymour.
I've been curious about using Comet on my website since I first read about it in 2008. However, plagued by the apprehension of the gutless beginner, I've postponed "taking the dive" until now - which is ridiculous because Comet could be very useful for my website. And perhaps, yours too!
This implementation (which I've successfully tested with IE, Chrome and Opera) has three components:
- the client webpage
- a "slave" executable
- an include for your main server program, containing some procedures (see below)
There are other ways to do it, of course. I've done it this way because we end up with a generic framework which can be easily used in different ways. For example...
To send information to one visitor:
Code: Select all
CometOne(info$,ipaddress$)
Code: Select all
CometPage(info$,pagename$)
Code: Select all
CometCast(info$)
There's also another procedure, CometCast_OncePerIP(). This does the same as CometCast() except that it only sends the data to one page for each IP address. This could be used, for example, in the following situation: you're sending an alert out to everyone, and someone has two (or more) pages open on your site. Using CometCast(), the alert would show on every page they have open. Using CometCast_OncePerIP(), it will only appear on one page that they have open. Perhaps this could be finetuned so as to target the page they most recently opened, as that's what they're likely to be looking at.
So anyway, here's a sample webpage:
Code: Select all
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE>Comet test</TITLE>
<SCRIPT type="text/javascript">
function DefineString(t1) {
var t2 = "";
if (!t1) {
return t2;
}
if (t1==null) {
return t2;
}
if (t1=="undefined") {
return t2;
}
if (typeof(t1)==undefined) {
return t2;
}
return t1;
}
var cometinfo = ""
var docomet = false;
function OpenCometConnection(info) {
info = DefineString(info);
if (info!="") {
cometinfo = info;
}
docomet = true;
CometWait(document.URL+"~"+cometinfo+"~");
}
function CloseCometConnection() {
docomet = false;
}
function CometWait(instrux) {
var CometReq = false;
var selfcomet = this;
// Mozilla/Safari
if (window.XMLHttpRequest) {
selfcomet.CometReq = new XMLHttpRequest();
}
// IE
else if (window.ActiveXObject) {
try {
selfcomet.CometReq = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
selfcomet.CometReq = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
selfcomet.CometReq = false; // real trouble here - couldn't create ANY XHR
}
}
}
selfcomet.CometReq.open("POST", "comet.exe", true);
selfcomet.CometReq.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
selfcomet.CometReq.onreadystatechange = function() {
//alert("selfcomet.CometReq.readyState: "+selfcomet.CometReq.readyState);
if (selfcomet.CometReq.readyState == 4) {
// data received...
if (docomet) {
if (selfcomet.CometReq.responseText!="null") {
//alert("COMET DATA...\n\n"+selfcomet.CometReq.responseText);
GenericCometResponder(selfcomet.CometReq.responseText);
}
// restart connection...
//alert("RESTARTING COMET");
setTimeout("OpenCometConnection()",10);
}
}
}
selfcomet.CometReq.send(instrux);
}
function GenericCometResponder(info) {
document.getElementById("news_display").innerHTML += "<BR><BR>"+info;
}
</SCRIPT>
</HEAD>
<BODY onload="OpenCometConnection()">
<DIV id="news_display"></DIV>
</BODY>
</HTML>
Here's the code for the slave executable, which you should compile as a console program called "comet.exe" in your site's local folder. Change the "cometfolder" variable to something that suits you, but make sure the folder exists!
Code: Select all
Structure CommonGatewayInterface
ContentLength.i
RemoteAddr.s
PostData.s
EndStructure
Global cgi.CommonGatewayInterface
Macro CGI_GetVariables
cgi\ContentLength = Val(GetEnvironmentVariable("CONTENT_LENGTH"))
cgi\RemoteAddr = GetEnvironmentVariable("REMOTE_ADDR") ; client's ip address
If cgi\ContentLength>0
*Buffer = AllocateMemory(cgi\ContentLength)
hInput = GetStdHandle_(#STD_INPUT_HANDLE)
ReadFile_(hInput, *Buffer, cgi\ContentLength, @bRead, 0)
cgi\PostData = PeekS(*Buffer)
FreeMemory(*Buffer)
CloseHandle_(hInput)
EndIf
EndMacro
;-
;- PROGRAM START
CGI_GetVariables
OpenConsole() ; needed for output
cometfolder.s = GetPathPart(ProgramFilename())+"comets\"
Macro CometRequestFile(code)
cometfolder+code+".request"
EndMacro
Macro CometResponseFile(code)
cometfolder+code+".response"
EndMacro
Macro CometResignationFile(code)
cometfolder+code+".resign"
EndMacro
Repeat ; create a uniquely-named "request file"
tokencode.s = ""
For a = 1 To 16
tokencode+Str(Random(8)+1)
Next a
tokencode = Str(ElapsedMilliseconds())+" "+tokencode
If FileSize(CometRequestFile(tokencode)) = -1
Break
EndIf
ForEver
f = CreateFile(#PB_Any,CometRequestFile(tokencode))
If f
cgi\PostData = URLDecoder(cgi\PostData)+cgi\RemoteAddr+"~"
WriteStringN(f,cgi\PostData)
CloseFile(f)
Else
; error creating request file. abort...
End
EndIf
; wait for response...
starttime = ElapsedMilliseconds()
Repeat
If FileSize(CometResponseFile(tokencode))>-1
; server has responded
fullreply.s = ""
f = ReadFile(#PB_Any,CometResponseFile(tokencode))
If f
While Not Eof(f)
fullreply + ReadString(f)+Chr(13)
Wend
CloseFile(f)
EndIf
DeleteFile(CometResponseFile(tokencode))
fullreply = "Content-type: text/plain;charset=UTF-8" + #CRLF$ + #CRLF$ + fullreply
written = WriteConsoleData(@fullreply,MemoryStringLength(@fullreply))
End
EndIf
Delay(250) ; change this to whatever you want. probably, a comet setup won't need to be very fast.
Until (ElapsedMilliseconds()-starttime)>30000 ; wait 30 seconds for response
; tell server we're no longer interested...
f = CreateFile(#PB_Any,CometResignationFile(tokencode))
If f : CloseFile(f) : EndIf
; send "null" reply to client...
fullreply.s = "Content-type: text/plain;charset=UTF-8" + #CRLF$ + #CRLF$ + "null"
written = WriteConsoleData(@fullreply,MemoryStringLength(@fullreply))
End
Here's the PBI for your server program. This program should be threadsafe and running all the time.
Before the line where you include this PBI, create a global string variable "cometfolder.s" and use it to store the directory where Comet request files will be saved by the slave executable(s). This variable should be identical in the slave code above.
Code: Select all
If cometfolder=""
MessageRequester("COMET","Need to set folder where comet request and response files will be saved.",0)
EndIf
Macro CometRequestFile(code)
cometfolder+code+".request"
EndMacro
Macro CometResponseFile(code)
cometfolder+code+".response"
EndMacro
Macro CometResignationFile(code)
cometfolder+code+".resign"
EndMacro
Structure CometStructure
pagename.s
ip.s
info.s
EndStructure
Global NewMap waitingcomet.CometStructure()
Macro ReviewCometClients
NewMap delcomet.b()
ForEach waitingcomet()
code.s = MapKey(waitingcomet())
If FileSize(CometResignationFile(code))>-1
AddMapElement(delcomet(),code)
DeleteFile(CometResignationFile(code))
EndIf
Next
ForEach delcomet()
code.s = MapKey(delcomet())
DeleteMapElement(waitingcomet(),code)
Next
FreeMap(delcomet())
d = ExamineDirectory(#PB_Any,cometfolder,"*.request")
If d
While NextDirectoryEntry(d)
fn.s = DirectoryEntryName(d)
code.s = StringField(fn,1,".")
fn = cometfolder+fn
If Not FindMapElement(waitingcomet(),code)
;MessageRequester("NEW COMET",code,0)
AddMapElement(waitingcomet(),code)
f = ReadFile(#PB_Any,fn)
If f
ln.s = ReadString(f)
waitingcomet(code)\pagename = GetURLPart(StringField(ln,1,"~"),#PB_URL_Path)
waitingcomet(code)\info = StringField(ln,2,"~")
waitingcomet(code)\ip = StringField(ln,3,"~")
CloseFile(f)
EndIf
DeleteFile(fn)
EndIf
Wend
FinishDirectory(d)
EndIf
EndMacro
Macro GiveCometInfo
code.s = MapKey(waitingcomet())
f = CreateFile(#PB_Any,cometfolder+code+".response")
If f
WriteStringN(f,sendinfo)
CloseFile(f)
EndIf
EndMacro
Macro DeleteSatedComets
ForEach delcomet()
code.s = MapKey(delcomet())
DeleteMapElement(waitingcomet(),code)
Next
EndMacro
Procedure.b CometOne(sendinfo.s,ip.s)
ForEach waitingcomet()
If waitingcomet()\ip = ip
GiveCometInfo
DeleteMapElement(waitingcomet(),code)
ProcedureReturn #True
EndIf
Next
EndProcedure
Procedure.b CometPage(sendinfo.s,client_pagename.s,client_eligibility.s="")
NewMap delcomet.b()
ForEach waitingcomet()
If waitingcomet()\pagename=client_pagename And (client_eligibility="" Or FindString(waitingcomet()\info,client_eligibility,1))
GiveCometInfo
AddMapElement(delcomet(),code)
EndIf
Next
DeleteSatedComets
EndProcedure
Procedure.b CometCast(sendinfo.s,client_eligibility.s="")
;R("COMETS: "+Str(MapSize(waitingcomet())))
NewMap delcomet.b()
ForEach waitingcomet()
;R("COMET CLIENT ON PAGE: "+waitingcomet()\pagename)
If client_eligibility="" Or FindString(waitingcomet()\info,client_eligibility,1)
;R("SENDING TO CLIENT: "+MapKey(waitingcomet()))
GiveCometInfo
AddMapElement(delcomet(),code)
EndIf
Next
DeleteSatedComets
EndProcedure
Procedure.b CometCast_OncePerIP(sendinfo.s,client_eligibility.s="")
NewMap ipdone.b()
NewMap delcomet.b()
ForEach waitingcomet()
If FindMapElement(ipdone(),waitingcomet()\ip) : Continue : EndIf
If client_eligibility="" Or FindString(waitingcomet()\info,client_eligibility,1)
AddMapElement(ipdone(),waitingcomet()\ip)
GiveCometInfo
AddMapElement(delcomet(),code)
EndIf
Next
DeleteSatedComets
EndProcedure
That's it. All you need to do now is call any of the procedures when you want to send some information to your visitors.
Here's an example "server program"... make sure to compile it as threadsafe.
Code: Select all
Global cometfolder.s = "C:\temp\comets\"
XIncludeFile "Comet.pbi"
Procedure.b FeedStuffToComets(void)
Repeat
Delay(10)
If GetAsyncKeyState_(#VK_P)
CometPage("Transmitting to everyone on this page","my_webpage.html")
EndIf
If GetAsyncKeyState_(#VK_C)
arr.s = "AVRIL LAVIGNE~KIM BASINGER~PAMELA ANDERSON~KRISTEN STEWART~BRITNEY SPEARS~CHRISTINA APPLEGATE~JULIETTE LEWIS~BILL CLINTON~"
CometCast("TRANSMITTING TO THE WHOLE WORLD!! TERRIBLE NEWS! ALIENS HAVE ABDUCTED "+StringField(arr,Random(CountString(arr,"~")-1)+1,"~")+"!!!!!!!")
EndIf
ForEver
EndProcedure
CreateThread(@FeedStuffToComets(),0)
While Not GetAsyncKeyState_(#VK_Escape)
ReviewCometClients
Delay(50)
Wend
End
Hope this is useful to someone,
Seymour.