Code archives/Networking/Networking Library
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
This library handles all the basic functionality of a client/server game system: -Client/server connections. -Fast client disconnections. -Chat. -Name changes. -IP banning. -Public server list. -Pinging clients, server, and unconnected servers. | |||||
SuperStrict Import pub.enet Private Const ENET_PACKET_FLAG_UNSEQUENCED:Int=2 Const SERVERUPDATEFREQUENCY:Int=4*1000*60 Function enet_host_port:Int( peer:Byte Ptr ) Local ip:Int=(Int Ptr peer)[1] Local port:Int=(Short Ptr peer)[4] ?LittleEndian ip=(ip Shr 24) | (ip Shr 8 & $ff00) | (ip Shl 8 & $ff0000) | (ip Shl 24) ? Return port EndFunction Function enet_host_ip:Int( peer:Byte Ptr ) Local ip:Int=(Int Ptr peer)[1] Local port:Int=(Short Ptr peer)[4] ?LittleEndian ip=(ip Shr 24) | (ip Shr 8 & $ff00) | (ip Shl 8 & $ff0000) | (ip Shl 24) ? Return ip EndFunction Function enet_peer_port:Int( peer:Byte Ptr ) Local ip:Int=(Int Ptr peer)[3] Local port:Int=(Short Ptr peer)[8] ?LittleEndian ip=(ip Shr 24) | (ip Shr 8 & $ff00) | (ip Shl 8 & $ff0000) | (ip Shl 24) ? Return port EndFunction Function enet_peer_ip:Int( peer:Byte Ptr ) Local ip:Int=(Int Ptr peer)[3] Local port:Int=(Short Ptr peer)[8] ?LittleEndian ip=(ip Shr 24) | (ip Shr 8 & $ff00) | (ip Shl 8 & $ff0000) | (ip Shl 24) ? Return ip EndFunction Public Type TNetworkNode Field port:Int Field ip:Int Field enethost:Byte Ptr Field enetpeer:Byte Ptr Method Delete() Free() EndMethod Method Free() If enethost enet_host_destroy(enethost) enethost=Null EndIf EndMethod Method Update() Local ip:Int,port:Int,client:TClient,ev:ENetEvent=New ENetEvent,id:Byte,packet:TPacket If Not Self.enethost RuntimeError "Can't update a remote server." Repeat If enet_host_service(Self.enethost,ev,0) Select ev.event Case ENET_EVENT_TYPE_CONNECT id=NETWORK_CONNECT Case ENET_EVENT_TYPE_DISCONNECT id=NETWORK_DISCONNECT Case ENET_EVENT_TYPE_RECEIVE Local size:Int=enet_packet_size(ev.packet) Local data:Byte[size] MemCopy(Varptr id,enet_packet_data(ev.packet),1) If size>1 packet=New TPacket packet._bank.resize(size-1) MemCopy(packet._bank.buf(),enet_packet_data(ev.packet)+1,size-1) EndIf Default Continue EndSelect EvaluateEvent(id,packet,ev.peer) Else Exit EndIf If enethost=Null Exit Forever EndMethod Method EvaluateEvent(id:Int,packet:TPacket,enetpeer:Byte Ptr) Abstract EndType 'Public Const NETWORK_CONNECT:Int=1 Const NETWORK_DISCONNECT:Int=2 Const NETWORK_PINGREQUEST:Int=3 Const NETWORK_PINGRESPONSE:Int=4 Const NETWORK_JOINREQUEST:Int=5 Const NETWORK_JOINRESPONSE:Int=6 Const NETWORK_CHAT:Int=7 Const NETWORK_LEAVEGAME:Int=8 Const NETWORK_CHANGENAMEREQUEST:Int=9 Const NETWORK_CHANGENAMERESPONSE:Int=10 Const NETWORK_PLAYERJOINED:Int=11 Const NETWORK_PLAYERLEFT:Int=12 Const NETWORK_LEAVEGAMEREQUEST:Int=13 Const NETWORK_LEAVEGAMERESPONSE:Int=14 Const NETWORK_PLAYERCHANGEDNAME:Int=15 Const SEND_RELIABLE:Int=ENET_PACKET_FLAG_RELIABLE Const SEND_UNSEQUENCED:Int=ENET_PACKET_FLAG_UNSEQUENCED Type TServer Extends TNetworkNode Field clients:TList=New TList Field callback(server:TServer,client:TClient,id:Int,packet:TPacket) Field bannedips:Int[] Field clientmap:TMap=New TMap Field url:String Field lastrefreshtime:Int Method Free() If url Remove() url="" EndIf Super.Free() EndMethod Function EncodeURL:String(t:String) Local newString:String Local i:Int,c:String,asciival:Int,hexstr:String,newhexstr:String For i = 0 To Len(t) c:String = t[i..i+1] asciival = Asc(c) If asciival > 32 And asciival < 123 ' handle replacing the special set of chars c = Replace(c,"'","%27") c = Replace(c,"%","%25") c = Replace(c,"<","%3C") c = Replace(c,">","%3E") c = Replace(c,"\","%5C") c = Replace(c,"^","%5E") c = Replace(c,"[","%5B") c = Replace(c,"]","%5D") c = Replace(c,"+","%2B") c = Replace(c,"$","%24") c = Replace(c,",","%2C") c = Replace(c,"@","%40") c = Replace(c,":","%3A") c = Replace(c,";","%3B") c = Replace(c,"/","%2F") c = Replace(c,"!","%21") c = Replace(c,"#","%23") c = Replace(c,"?","%3F") c = Replace(c,"=","%3D") c = Replace(c,"&","%26") newString:+c Else hexstr$ = Hex(asciival) newhexstr$ = "%" + hexstr[Len(hexstr)-2..Len(hexstr)] newstring:+newhexstr EndIf Next newstring = newstring[..Len(newstring)-3] Return newstring EndFunction Function Create:TServer(port:Int=0,portrange:Int=40) Local addr:Byte Ptr,server:TServer=New TServer server.ip=ENET_HOST_ANY If portrange<=1 addr=enet_address_create(server.ip,server.port) server.enethost=enet_host_create(addr,32,0,0) enet_address_destroy(addr) Else If port=0 port=7777 For Local n:Int=port To port+portrange-1 addr=enet_address_create(ENET_HOST_ANY,n) server.enethost=enet_host_create(addr,64,0,0) enet_address_destroy(addr) If server.enethost server.port=n Exit EndIf Next EndIf If Not server.enethost Return Null Return server EndFunction Method Publish:Int(servername:String="",gamename:String="",url:String="http::www.leadwerks.com/gameservers/gameservers.php") Local stream:TStream gamename=EncodeURL(gamename) servername=EncodeURL(servername) Self.url=url stream=OpenStream(url+"?action=addserver&gamename="+gamename+"&servername="+port+"|"+servername) If Not stream Return False Self.url=url While Not stream.Eof() Local s:String=stream.ReadLine() If s.Trim() stream.close() Return False EndIf Wend stream.close() lastrefreshtime=MilliSecs() Return True EndMethod Method Remove:Int() Local stream:TStream stream=OpenStream(url+"?action=removeserver") If stream stream.close() Return True Else Return False EndIf EndMethod Function Query:String[](gamename:String="",url:String="") Local stream:TStream,s:String,sarr:String[],n:Int If url="" url="http::www.leadwerks.com/gameservers/gameservers.php" stream=OpenStream(url+"?action=listservers&gamename="+gamename) If Not stream Return Null While Not stream.Eof() s=ReadLine(stream) If s.Trim().length>0 sarr=sarr[..sarr.length+1] sarr[sarr.length-1]=s.Trim() Local sa:String[]=sarr[sarr.length-1].split("|") Local name:String=sa[0] sarr[sarr.length-1]=HostIp(name)+"|"+sarr[sarr.length-1] EndIf Wend stream.close() Return sarr EndFunction Method Refresh:Int() Local stream:TStream stream=OpenStream(url+"?action=refreshserver") If Not stream Return False stream.close() Return True EndMethod Method FindClientByName:TClient(name:String) Return TClient(clientmap.valueforkey(name)) EndMethod Method FindClientByPeer:TClient(peer:Byte Ptr) Local client:TClient For client=EachIn clients If client.enetpeer=peer Return client Next EndMethod Method FindClient:TClient(ip:Int,port:Int) Local client:TClient For client=EachIn clients If client.ip=ip And client.port=port Return client Next EndMethod Method Update() If url Local time:Int=MilliSecs() If time-lastrefreshtime>serverupdatefrequency lastrefreshtime=time Refresh() EndIf EndIf Super.Update() EndMethod Method EvaluateEvent(id:Int,packet:TPacket,enetpeer:Byte Ptr) Local client:TClient=FindClient(enet_peer_ip(enetpeer),enet_peer_port(enetpeer)) Select id Case NETWORK_PINGRESPONSE client.latency=MilliSecs()-packet.ReadInt() Case NETWORK_LEAVEGAMEREQUEST If client Send(client,NETWORK_LEAVEGAMERESPONSE,Null,SEND_RELIABLE) Local relay:TPacket=New TPacket relay.WriteLine(client.name) Broadcast(NETWORK_PLAYERLEFT,relay,SEND_RELIABLE) clients.remove(client) If clientmap.valueforkey(client.name)=client clientmap.remove(client.name) EndIf Case NETWORK_JOINREQUEST If client Disconnect(client,1) Local relay:TPacket=New TPacket relay.WriteLine(client.name) Broadcast(NETWORK_PLAYERLEFT,relay,SEND_RELIABLE) EndIf client=TClient.Find(enet_peer_ip(enetpeer),enet_peer_port(enetpeer)) client.enetpeer=enetpeer If IPBanned(client.ip) Disconnect(client,0) Return Else client.name=packet.ReadLine() If Not FindClientByName(client.name) clientmap.insert(client.name,client) clients.AddLast(client) Local responsepacket:TPacket=New TPacket responsepacket.WriteByte(1) responsepacket.WriteByte(clients.count()-1) Local peer:TClient For peer=EachIn clients If peer<>client responsepacket.WriteLine(peer.name) Next Send(client,NETWORK_JOINRESPONSE,responsepacket,SEND_RELIABLE) id=NETWORK_PLAYERJOINED 'Tell everyone he joined responsepacket=New TPacket responsepacket.WriteLine(client.name) Broadcast(NETWORK_PLAYERJOINED,responsepacket,SEND_RELIABLE) Else packet=New TPacket packet.WriteByte(0)'no, you can't joint packet.WriteByte(1)'reason: name already taken Send(client,NETWORK_JOINRESPONSE,packet,SEND_RELIABLE) 'Disconnect(client,0) Return EndIf EndIf Case NETWORK_CHANGENAMEREQUEST Local name:String=packet.ReadLine() packet=New TPacket If client Local oldname:String=client.name If FindClientByName(name)<>Null And client.name<>name packet.WriteByte(0) send(client,NETWORK_CHANGENAMERESPONSE,packet,SEND_RELIABLE) Return Else packet.WriteByte(1) packet.WriteLine(name) send(client,NETWORK_CHANGENAMERESPONSE,packet,SEND_RELIABLE) clientmap.remove(client.name) client.name=name clientmap.insert(name,client) If oldname<>name packet=New TPacket packet.WriteLine(oldname) packet.WriteLine(name) Broadcast(NETWORK_PLAYERCHANGEDNAME,packet,SEND_RELIABLE) EndIf EndIf Else If FindClientByName(name)=Null packet.WriteByte(1) packet.WriteLine(name) send(client,NETWORK_CHANGENAMERESPONSE,packet,SEND_RELIABLE) Return EndIf EndIf Return Case NETWORK_CONNECT Return Case NETWORK_DISCONNECT If client clients.remove(client) If clientmap.valueforkey(client.name)=client clientmap.remove(client.name) If Not client.enethost client.free() EndIf Case NETWORK_PINGREQUEST If Not client client=New TClient client.enetpeer=enetpeer EndIf Send(client,NETWORK_PINGRESPONSE,packet) Return Case NETWORK_CHAT Local relay:TPacket=New TPacket Local count:Int=packet.ReadByte() relay.WriteLine(client.name) relay.WriteLine(packet.ReadLine()) If count=0 Broadcast(NETWORK_CHAT,relay,SEND_RELIABLE) Else For Local n:Int=1 To count client=FindClientByName(packet.ReadLine()) If client Send(client,NETWORK_CHAT,relay,SEND_RELIABLE) EndIf Next EndIf Return EndSelect If callback If packet packet.seek(0) callback(Self,client,id,packet) EndIf EndMethod Method Send:Int(client:TClient,id:Int,packet:TPacket=Null,flags:Int=0,channel:Int=0) Local enetpacket:Byte Ptr Local result:Int If Not client.enetpeer RuntimeError "Can't send to local client." Local data:Byte[] If packet If packet._bank.size()=0 packet=Null EndIf If packet data=New Byte[packet._bank.size()+1] MemCopy(Varptr data[1],packet._bank.buf(),packet._bank.size()) Else data=New Byte[1] EndIf data[0]=id enetpacket=enet_packet_create(data,data.length,flags) result=(enet_peer_send(client.enetpeer,channel,enetpacket)=0) Return result EndMethod Method Broadcast:Int(id:Int,packet:TPacket=Null,flags:Int=0,channel:Int=0) Local result:Int=1 For Local client:TClient=EachIn clients If Not Send(client,id,packet,flags,channel) result=0 Next Return result Rem Local enetpacket:Byte Ptr Local result:Int Local data:Byte[] If packet If packet._bank.size()=0 packet=Null EndIf If packet data=New Byte[packet._bank.size()+1] MemCopy(Varptr data[1],packet._bank.buf(),packet._bank.size()) Else data=New Byte[1] EndIf data[0]=id enetpacket=enet_packet_create(data,data.length,flags) enet_host_broadcast(Self.enethost,channel,enetpacket) EndRem EndMethod Method Disconnect(client:TClient,force:Int=False) If client.enetpeer If force enet_peer_reset(client.enetpeer) Else enet_peer_disconnect(client.enetpeer) EndIf clients.remove(client) If Not client.enethost client.link.remove() EndIf If clientmap.valueforkey(client.name)=client clientmap.remove(client.name) EndIf client.enetpeer=Null EndIf EndMethod Method BanIP(ip:Int) bannedips=bannedips[..bannedips.length+1] bannedips[bannedips.length-1]=ip EndMethod Method IPBanned:Int(ip:Int) For Local n:Int=0 To bannedips.length-1 If ip=bannedips[n] Return True Next Return False EndMethod Method Kick(client:TClient) BanIP(client.ip) Disconnect(client) EndMethod EndType Type TClient Extends TNetworkNode Const maxplayers:Int=64 Global list:TList=New TList Field name:String Field link:TLink Field server:TServer Field connected:Int Field joined:Int=0 Field callback(client:TClient,id:Int,packet:TPacket) Field userdata:Object Field channels:Int=16 Field latency:Int Method Free() If link link.remove() link=Null EndIf Super.Free() EndMethod Function Find:TClient(ip:Int,port:Int) Local client:TClient For client=EachIn list If client.ip=ip And client.port=port Return client Next client=New TClient client.ip=ip client.port=port client.link=list.addlast(client) Return client EndFunction Function Create:TClient(port:Int=0,portrange:Int=40) Const maxpeers:Int=32 Local client:TClient=New TClient Local addr:Byte Ptr If portrange<=1 client.ip=ENET_HOST_ANY client.port=port addr=enet_address_create(client.ip,client.port) client.enethost=enet_host_create(addr,maxpeers,0,0) enet_address_destroy(addr) If Not client.enethost Return Null client.link=list.addlast(client) Else If port=0 port=7776 For Local n:Int=port To port+portrange-1 addr=enet_address_create(ENET_HOST_ANY,n) client.enethost=enet_host_create(addr,maxpeers,0,0) enet_address_destroy(addr) If client.enethost client.port=port Exit EndIf Next EndIf Return client EndFunction Method Disconnect(force:Int=False) Const disconnecttimeout:Int=10000 If Not Self.enethost RuntimeError "Can't update a remote server." If server If force enet_peer_reset(server.enetpeer) Else Send(NETWORK_LEAVEGAMEREQUEST,Null,SEND_RELIABLE) Local ev:ENetEvent=New ENetEvent Local start:Int=MilliSecs() Repeat If MilliSecs()-start>disconnecttimeout Exit If enet_host_service(Self.enethost,ev,100) Select ev.event Case ENET_EVENT_TYPE_RECEIVE If ev.packet Local id:Int,packet:TPacket Local size:Int=enet_packet_size(ev.packet) Local data:Byte[size] MemCopy(Varptr id,enet_packet_data(ev.packet),1) If size>1 packet=New TPacket packet._bank.resize(size-1) MemCopy(packet._bank.buf(),enet_packet_data(ev.packet)+1,size-1) EndIf Select id Case NETWORK_LEAVEGAMERESPONSE Exit EndSelect EndIf EndSelect Else Exit EndIf Forever enet_peer_disconnect(server.enetpeer) EndIf server=Null EndIf joined=False EndMethod Method SetName(name:String) If name.length>16 name=name[..16] If server Local packet:TPacket=New TPacket packet.WriteLine(name) Send(NETWORK_CHANGENAMEREQUEST,packet,SEND_RELIABLE) Else Self.name=name EndIf EndMethod Method Connect:Int(ip:Int,port:Int) Local addr:Byte Ptr If ip=0 ip=2130706433 If Not Self.enethost RuntimeError "Remote client cannot connect to server." If server Disconnect() server=New TServer server.ip=ip server.port=port addr=enet_address_create(server.ip,server.port) server.enetpeer=enet_host_connect(enethost,addr,channels) enet_address_destroy(addr) If server.enetpeer=Null server=Null Return 0 EndIf Rem Local _callback:Byte Ptr=Null _callback=callback Local start:Int=MilliSecs() Repeat Update() If MilliSecs()-start>10000 Or joined=1 Exit Forever callback=_callback EndRem Return 1 EndMethod Method Send:Int(id:Int,packet:TPacket=Null,flags:Int=0,channel:Int=0) Local enetpacket:Byte Ptr Local result:Int If Not connected Return 0 If Not server RuntimeError "Client is not connected." Local data:Byte[] If packet If packet._bank.size()=0 packet=Null EndIf If packet data=New Byte[packet._bank.size()+1] MemCopy(Varptr data[1],packet._bank.buf(),packet._bank.size()) Else data=New Byte[1] EndIf data[0]=id enetpacket=enet_packet_create(data,data.length,flags) result=(enet_peer_send(server.enetpeer,channel,enetpacket)=0) Return result EndMethod Method EvaluateEvent(id:Int,packet:TPacket,enetpeer:Byte Ptr) Select id Case NETWORK_CONNECT Self.connected=True packet=New TPacket packet.WriteLine(name) Send(NETWORK_JOINREQUEST,packet) Case NETWORK_JOINRESPONSE joined=packet.ReadByte() If joined=0 Disconnect() Case NETWORK_CHANGENAMERESPONSE If packet.ReadByte()=1 name=packet.ReadLine() EndIf Case NETWORK_PINGREQUEST Send(NETWORK_PINGRESPONSE,packet) Return Case NETWORK_PINGRESPONSE latency=MilliSecs()-packet.ReadInt() packet.WriteInt(enet_peer_ip(enetpeer)) packet.WriteInt(enet_peer_port(enetpeer)) If enetpeer=pingpeer enet_peer_disconnect(pingpeer) pingpeer=Null EndIf EndSelect If callback If packet packet.seek(0) callback(Self,id,packet) EndIf EndMethod Method Join() If Not joined Local packet:TPacket=New TPacket packet.WriteLine(name) Send(NETWORK_JOINREQUEST,packet) EndIf EndMethod Function ConvertEvent:Int(ev:EnetEvent,packet:TPacket) Select ev.event Case ENET_EVENT_TYPE_CONNECT Return NETWORK_CONNECT Case ENET_EVENT_TYPE_DISCONNECT Return NETWORK_DISCONNECT Case ENET_EVENT_TYPE_RECEIVE If ev.packet Local id:Int Local size:Int=enet_packet_size(ev.packet) Local data:Byte[size] MemCopy(Varptr id,enet_packet_data(ev.packet),1) If size>1 packet._bank.resize(size-1) MemCopy(packet._bank.buf(),enet_packet_data(ev.packet)+1,size-1) EndIf enet_packet_destroy(ev.packet) Return id EndIf EndSelect EndFunction Field pingpeer:Byte Ptr Method Ping:Int(ip:Int=0,port:Int=0) Const disconnecttimeout:Int=1000 Local packet:TPacket=New TPacket,addr:Byte Ptr,enetpacket:Byte Ptr,ev:EnetEvent,result:Int,id:Int,start:Int If server If server.ip=ip And server.port=port ip=0 EndIf If Not enethost RuntimeError("Can't ping remote client.") If ip If pingpeer enet_peer_disconnect(pingpeer) pingpeer=Null EndIf ev=New EnetEvent addr=enet_address_create(ip,port) pingpeer=enet_host_connect(enethost,addr,1) enet_address_destroy(addr) If pingpeer start=MilliSecs() Repeat If MilliSecs()-start>disconnecttimeout enet_peer_disconnect(pingpeer) pingpeer=Null 'Print "Connect timeout" Return 0 EndIf If enet_host_service(enethost,ev,0) id=ConvertEvent(ev,packet) Select id Case NETWORK_CONNECT Exit EndSelect EndIf Forever Local data:Byte[5] data[0]=NETWORK_PINGREQUEST start=MilliSecs() MemCopy(Varptr data[1],Varptr start,4) enetpacket=enet_packet_create(data,data.length,0) If enet_peer_send(pingpeer,0,enetpacket)=0 Return 1 Else enet_peer_disconnect(pingpeer) pingpeer=Null 'Print "can't send packet" Return 0 EndIf Else 'Print "Cant connect to peer at "+ip+", "+port Return 0 EndIf Else packet:TPacket=New TPacket packet.WriteInt(MilliSecs()) Return Send(NETWORK_PINGREQUEST,packet) EndIf EndMethod Method Say:Int(text:String,recipients:String[]=Null) Local packet:TPacket=New TPacket If recipients packet.WriteByte(recipients.length) packet.WriteLine(text) For Local n:Int=0 To recipients.length-1 packet.WriteLine(recipients[n]) Next Else packet.WriteByte(0) packet.WriteLine(text) EndIf Return Send(NETWORK_CHAT,packet) EndMethod EndType Type TPacket Extends TBankStream Method New() _bank=New TBank EndMethod EndType Function CreatePacket:TPacket() Local packet:TPacket=New TPacket Return packet EndFunction |
Comments
| ||
Here's an example of an advanced chat program using this system: chat.bmx appsettings.bmx |
| ||
Very nice! Thanks for sharing. |
| ||
In Create:TServer() you need to assign the server.port when portrange <= 1. Basically... If portrange<=1 server.port = port addr=enet_address_create(server.ip,server.port) instead of If portrange<=1 addr=enet_address_create(server.ip,server.port) ' server.port starts out as 0... or else server.port will equal 0 unless you set server.port directly before hand. Kinda undoes having a port argument being passed. Also kids, remember to do Server.Update or Client.Update were needed, or else NOTHING will happen. >.> It's a shame you decided to use MaxGUI for the example, it's so cluttered with GUI crap it's hard to see the actual networking side of things. *Edit* and shouldn't in TClient.EvaluateEvent() this: Case NETWORK_CONNECT Self.connected=True packet=New TPacket packet.WriteLine(name) Send(NETWORK_JOINREQUEST,packet) be this? Case NETWORK_CONNECT Self.connected=True Self.Join() |
| ||
When I test this code as-is (Bmax 1.41), I get an Array OOB error at this line on the client side: game[n].port=Int(serverinfo[2]) If Not client client=TClient.Create(Int(GadgetText(gadget_clientport))) If client client.callback=clientcallback game=New TGame[sarr.length] currentpinggame=0 DebugLog sarr.length-1 For Local n:Int=0 To sarr.length-1 Local serverinfo:String[]=sarr[n].split("|") game[n]=New TGame game[n].index=n game[n].ip=Int(serverinfo[0]) game[n].port=Int(serverinfo[2]) game[n].name=serverinfo[3] AddGadgetItem(list_server,sarr[n]) game[n].Update() Next Else Notify "Failed to create client.",1 EndIf Remming it out just makes the client give a dummy list of bad servers. I only took a cursory glance at the source but didn't see anything superficially wrong to me. |
| ||
i cant start a server or connect to anything |
Code Archives Forum