Packet - Networking library

Yup the packet needs to be set by the server but the order is not important

Even if you registered it on the server side after the client has registered it the client will update the id after the packet has already been created

but this should almost never be happening the server should have all the packets assigned as soon as it starts

if you for some reason are assigning to the server late you can do

assert(packet.Id, "Packet is not registered on the server")
packet:Fire()

or add the check inside the Fire function but I don’t think its a big enough problem to need to be there by default

The reason why I discovered this error is that I have my packets categorized by ModuleScripts, and I had only required the module in a client script (because I forgot to require it on the server), and if other people have that aswell and do this mistake, it may be nice for them with an error message stating it rather than them being confused.

:arrow_up: Update 1.3

  1. Improved CPU performance
  2. Any type now supports tables
  3. Any type now supports EnumItems
  4. Added BooleanNumber type
  5. Added rate limit to how many events the client can send per server heartbeat [this only effects exploiters]
  6. Client is no longer framerate dependant and will fire a maximum of 60 events every second
  7. Bug fixes

here we can see the CPU time it takes to Fire() an array of 50 booleans and an array of 50 dictionaries with numbers in them
image
and if you want to run the test for yourself here is the benchmark i used
Benchmark.rbxl (93.3 KB)


its now possible to send tables using the Any type

-- this will send 22 bytes [1 byte for the packet id, 3 bytes for the table, 11 bytes for "MyCoolKey", 7 bytes for "Hello"]
local packet = Packet("Name", Packet.Any)
packet:Fire({MyCoolKey = "Hello"})
-- this will send 7 bytes [1 byte for the packet id, 6 bytes for "Hello"]
local packet = Packet("Name", {MyCoolKey = Packet.String})
packet:Fire({MyCoolKey = "Hello"})

as you can see sending tables with the Any type using a lot more data compared to predefining your tables so its always recommended to predefine your types to get the best performance

one benefit to using the Any type for tables is you can use Any type for the key for example

local packet = Packet("Name", Packet.Any)
packet:Fire({[Vector3.new(1, 2, 3)] = Enum.Material.Grass})
packet:Fire({[{Key1 = {1, 4, 7}, Key2 = "Test"}] = false})

you can now send EnumItems with the any type

local packet1 = Packet("Name1", Packet.Any)
packet:Fire(Enum.Material.Grass)

this will send 4 bytes for the EnumItem vs 3 if we had defined it


the BooleanNumber type lets you send 1 boolean and 1 number for just 1 byte

local packet1 = Packet("Name1", Packet.BooleanNumber)
packet:Fire({Boolean = true, Number = 54})

the number only has 7 bits so has a range between 0 - 127


the client is expected to fire at most 60 events every second 1 per server heartbeat
if exploiters sends more then 10 events per heartbeat the server will now ignore them
this limit can be modified by editing this line in the main module

local bytes = (playerBytes[player] or 0) + math.max(buffer.len(receivedBuffer), 800)
if bytes > 8_000 then
	if RunService:IsStudio() then
		warn(player.Name, "is exceeding the data/rate limit; some events may be dropped")
	end
	return
end

here we can see there is a limit of 8000 bytes [8KB] per heartbeat
and we can see that a event costs a minimum of 800 bytes even if the size is less
8000 / 800 = 10 events per heartbeat

the reason we must except more then 1 event per heartbeat is because ping can fluctuate and some events might arrive on the same frame normally this will be around 2-3 events per frame but to give some extra headroom I have set it to a max of 10 if you feel that this is not enough you can modify the 800 to something like 400 to allow 20 events per heartbeat

also remember that this limit is only from Client > Server there are no limits going from Server > Client


if you find any bugs please report them and ill do my best to fix them

6 Likes

Hey, u should add this Entities, its recommended due its more variant than StructOfNumbers.

-- Entities
local amount = 50
local array = table.create(amount)
for index = 1, amount do
	local random = Random.new(index*index)
	table.insert(array, {
		brand = string.char(random:NextInteger(0, 255)):rep(random:NextInteger(2, 100)),
		carName = string.char(random:NextInteger(0, 255)):rep(random:NextInteger(2, 100)),
		carId = random:NextInteger(0, 65535),
		carColor = Color3.fromRGB(random:NextInteger(0, 255), random:NextInteger(0, 255), random:NextInteger(0, 255)),
		available = random:NextInteger(0, 1) == 1,
		dealers = random:NextInteger(0, 127),
		price = random:NextNumber(0, 1e12),
	})
end
return array

it would be better to randomise each character instead of repeating them also its not a fair test because your making a random length string would be better if the strings had a fixed length

also would be nice to add a array of cframes and array of vector3s

I feel that packets CPU usage should be better for these more complex types as it has function inlineing for all types

-- Entities
local HttpService = game:GetService("HttpService")
local amount = 50
local array = table.create(amount)
for index = 1, amount do
	local random = Random.new(index*index)
	local dealers = random:NextInteger(1, 127)
	local vect3 = Vector3.new(random:NextNumber(-255, 255), random:NextNumber(-255, 255), random:NextNumber(-255, 255))
	table.insert(array, {
		brand = HttpService:GenerateGUID(false):gsub('-', ' '),
		carName = HttpService:GenerateGUID(false):gsub('-', ' '),
		carId = random:NextInteger(0, 65535),
		carColors = table.create(random:NextInteger(2, 5), Color3.fromRGB(random:NextInteger(0, 255), random:NextInteger(0, 255), random:NextInteger(0, 255))),
		dealerLocations = table.create(dealers, vect3),
		available = random:NextInteger(0, 1) == 1,
		dealers = dealers,
		price = random:NextNumber(0, 1e12),
	})
end
return array

this will generate the same string every time based on the random seed this should be better so that we can repeat the exact same benchmark if we need to

also when using F32 it best to use the full range of the float because if we don’t there will be a lot of 0 bits in there that might be optimized by Roblox so by using the full range we get to see the worse case scenario

local amount = 50
local stringLength = 20
local random = Random.new(908248)
local array = table.create(amount)
local f32Max = 170141183460469231731687303715884105728
local rotationMax = math.pi * 2
for index = 1, amount do
	local characters = table.create(stringLength)
	for index = 1, stringLength do table.insert(characters, string.char(random:NextInteger(0, 255))) end
	table.insert(array, {
		Number = random:NextInteger(0, 65535),
		String = table.concat(characters)
		Color = Color3.new(random:NextNumber(), random:NextNumber(), random:NextNumber())
		Vector3 = Vector3.new(random:NextNumber() * f32Max , random:NextNumber() * f32Max , random:NextNumber() * f32Max)
		CFrame = CFrame.fromEulerAnglesXYZ(random:NextNumber() * rotationMax, random:NextNumber() * rotationMax, random:NextNumber() * rotationMax) +
				Vector3.new(random:NextNumber() * F32Max , random:NextNumber() * F32Max , random:NextNumber() * F32Max)
	})
end
return array

F32 goes from -f32Max to f32Max but going from 0 to f32Max should be good enough

i mean my version of entities is closer to real uses in games due it has more variative and i forgot to put cframe’s

Maybe its closer to real usage

if your using the same types for each lib then they should all get the same result if they utilize buffers the same

with your method you could get different results every time you run the benchmark
but this difference has nothing to do with the libraries it has to do with how Roblox replicates buffer
and it could mask if there are any differences between the libraries

my method should give you the same result every time you run the benchmark
and will only show you the differences with the libraries

Does this mean I can pass whatever to the client without having to define the types? Because I’m planning to use this for a replication event that the server will fire to every client with varying types, including instances, strings all the way to vector3. However the first argument will always be a string to indicate which replication effect the client should do, while the rest can be any type.

Also any bandwidth throttling? Since like I said, the event will be used heavily sometimse even fired twice in one frame. All from server to clients.

1 Like

the Any type can send any supported value without needing to define it

from the server to the client there is no throttling

1 Like

this is really great i had trouble making use of netray but compare to this really fun and easy to use!

I tried to do that with tuples but only the first string argument is sent to the clients.

ocal _packet = require(game:WaitForChild("ReplicatedStorage", 500):WaitForChild("MainModule", 500))

local _newPacket = _packet("_replicateEffects", _packet.String, ... :: _packet.Any)

_newPacket.OnClientEvent:Connect(function(_effectName, ...)
	print(_effectName, ...)
end)
local _packet = require(game:WaitForChild("ReplicatedStorage", 500):WaitForChild("MainModule", 500))

local _newPacket = _packet("_replicateEffects", _packet.String, ... :: _packet.Any)

task.wait(1)
-- Number of arguments varies between which effect to replicate.
_newPacket:Fire("_", 1, "_", workspace, workspace.Baseplate.Position)

instead of a tulip you can use an array

local p = Packet(name, {Packet.Any})

p:Fire(table.unpack(...))

like this

1 Like

Does this module not work on ReplictedFirst? For some reason the client is not receiving the event at all, no errors on both sides and the server seems to have fired the event safely.

The event does get created on the client, but the event doesn’t seem to quite “fire”, keep in mind I created the event through a module script required by the server, this is so every other script can have the same event, unless this is a built in function please let me know.

I seemed to have fix the issue by parenting the created event in ReplicatedStorage instead but the passed arguments are not correct.

image
What I sent:

_clientReplicate:FireClient(Player, "a4a_doKillstreak", {0})

Nevermind, I fixed that on my own since I accidentally removed it (the passed string) on the client response.

RemoteEvents don’t work in ReplictedFirst

you would need to parant the RemoteEvent into ReplictedStorage or place the module in ReplictedStorage

2 Likes

I fixed it by parenting the created event in ReplicatedStorage, thanks, also to be sure, the Constructor on server does and will return a previously created event right? Since it mentions it here.

Is there a way to deal with the “buffer access out of bounds”? I tried sending the client some long strings or numbers in this case and it just errored trying to read it.

was your string longer then 255 characters if so you need to update the string type to support more then 255 characters

1 Like

No it wasn’t, these are the arguments I passed to the cilent.

(...,"e4e_onBadgeGain", {_badgeInfo.Name --[[Welcome Back]] , _badgeInfo.IconImageId --[[only 10 characters of numbers]] }