项目作者: Papierkorb

项目描述 :
Lightning fast data serialization and RPC for Crystal
高级语言: Crystal
项目地址: git://github.com/Papierkorb/cannon.git
创建时间: 2017-02-13T22:42:41Z
项目社区:https://github.com/Papierkorb/cannon

开源协议:Mozilla Public License 2.0

下载


The Cannon Build Status

Really fast data de-/serialisation and remote procedure calling, for when your
process has other things to do than data serialisation.

Benchmark

The main point about Cannon is speed. This is achieved by cutting some
corners.

  1. io = IO::Memory.new
  2. data = [ 5, 6, 7 ]
  3. Benchmark.ips do |x|
  4. x.report("encode") do
  5. Cannon.encode(io, data)
  6. io.rewind
  7. end
  8. x.report("decode") do
  9. Cannon.decode(io, typeof(data))
  10. io.rewind
  11. end
  12. end

On my i5 6600K (Skylake) I get numbers like these:

  1. encode 96.84M ( 10.33ns) 4.19%) fastest
  2. decode 27.07M ( 36.95ns) 3.79%) 3.58× slower

Usage

(Tip: You can find all of these in the samples/ directory)

Many common data types have support built-in:

  1. require "cannon" # Require the shard
  2. # Data de-/serialization. Cannon operates on IOs
  3. io = IO::Memory.new # Use an in-memory store for this
  4. original = [ 5, 6, 7 ] # Some data to serialize
  5. Cannon.encode io, original # Write `data` into `io`
  6. io.rewind # Don't forget to rewind the stream
  7. copy = Cannon.decode io, typeof(data) # And read it back
  8. pp original, copy # original == copy

Your own data structures can also be serialized. Either by implementing
#to_cannon_io(io) and .from_cannon_io(io) yourself, or simply use
Cannon::Auto.

  1. require "cannon"
  2. class Session
  3. include Cannon::Auto # Magic include
  4. property username : String
  5. property email : String
  6. def initialize(@username, @email)
  7. end
  8. end
  9. io = IO::Memory.new # Like in the example above
  10. original = Session.new("alice", "alice@example.com")
  11. Cannon.encode io, original
  12. io.rewind
  13. decoded = Cannon.decode io, Session
  14. pp original, decoded

Even better, if your data structure is a struct, @[Packed] and only uses
simple types, use Cannon::FastAuto.

  1. require "cannon"
  2. @[Packed] # Go with packed if you want to go fast!
  3. struct Addition
  4. include Cannon::FastAuto # Faster magic include
  5. property a : Int32
  6. property b : Int32
  7. def initialize(@a, @b)
  8. end
  9. end
  10. io = IO::Memory.new # Like in the example above
  11. original = Addition.new(4, 5)
  12. Cannon.encode io, original
  13. io.rewind
  14. copy = Cannon.decode io, Addition
  15. pp original, copy

Cutting corners

You’d be surprised how much Cannon actually supports. Here are some tricks
done to speed things up:

  • Primitives, like Int32, are written directly.
  • Arrays containing simple types are basically Slices.
  • Slice is easy to blast out.
  • Slices not containing primitive types are not supported at the moment.
  • Custom structs can be marked as being “simple”, meaning it can be serialized
    by type-casting it to a Slice.
  • Tuples are treated automatically as Slice, if they only contain “simple”
    data types.
  • Nil is represented as nothing.
  • The RPC mechanism transparently uses UInt32 identifiers instead of method names.

However, there are things to keep in mind:

  • The data format is binary and uses the host endianness
  • The data is effectively unstructured, if you’re using the wrong format things
    will blow up :)

Simple data structures

Simple data structures can be read and written directly. Their constructor,
if any, will most likely not be invoked at all, they just exist.

Requirements are as follows:

  • Must be a struct - Not a class!
  • Only contains other “simple” data, like primitives
  • Does not contain any variable-length data like Slices or Arrays

Do not falter: If your data-structure does not fit these, you can still use it
just fine! It just means it will be fast, but not blazing fast :)

RPC

Cannon also comes with a RPC module. It does the heavy lifting for you, so
that you can focus on actually writing code. And it’s fast too!

Want to see some real code? Look into samples/rpc/!

Services

The RPC module works on a service methodology. A server provides one, or more
services for a client to consume. In Cannon, both ends can provide services
for the other to consume.

For this to work, you need three components per Service:

  1. The description module, which describes the interface through abstract
    methods.
  2. The service class, which includes Cannon::Rpc::Service. This object lives
    on the server. There can be one or more instances of each service. Each
    instance has its own unique identifier, or id, which is a UInt32. A
    service can be owned by a client, more on that below.
  3. The client class, which includes Cannon::Rpc::RemoteService. This object
    lives on the client. It’s bound to a Cannon::Rpc::Connection and the
    remote services id.

In real usage, you’ll probably have two kinds of services: First, those used by
every client, and second, those used exclusively by one (or few) client(s).

Singleton services

Singleton services have no owner (Its owner is nil), and are registered to
well-known a identifiers. Usually, only one instance of this service exists on
the server.

You can make your life easy by including Cannon::Rpc::SingletonService into
the description module of the service. This module is instantiated with an
identifier. When you now derive your service and client classes from it,
they’ll automatically bind to the singletons service identifier.

Instance services

The second kind of services are instance services. These are used exclusively
by one client, or by few clients. If there’s only one client, you can give the
client ownership over that service instance.

When a client owns a service, it may release it (remove it) later on. This can
be done through the #release_now! method of a client class. Or you just
forget about the client, wait until it’s garbage-collected, and it’ll
automatically be released remotely for you. The same happens when a connection
is closed automatically, too.

The client

The client is more or less auto-generated from the description module using
Cannon::Rpc::RemoteService. An actually complete example is this:

  1. class MyServiceClient
  2. include Cannon::Rpc::RemoteService(MyServiceDescription)
  3. end

That’s it! The class will get a #initializer which you pass the Connection
first and the service id (optional if it’s a singleton service). The
implemented abstract methods from the description module will point at the
remote service, and function like normal methods to you.

If you don’t care about the methods results anyway, use the
_without_response version, which is also generated for each method.

  1. my_client.greet("Alice") # Wait for response
  2. my_client.greet_without_response("Alice") # Don't wait

Getting the calling connection

One last thing: If you need to know which Connection exactly is making the
call in your service class, just add an argument of type Connection to the
end of the argument list. For the client, this argument will “disappear”. The
service instance will have it “injected”.

Gotchas and Troubleshooting

Type your method arguments and result

It’s really important to type your methods. It’s acceptable to not type the
result, in which case it’s treated as Nil, and thus will silently drop
anything returned from the method.

  1. # Won't work
  2. abstract def add(a, b)
  3. abstract def greet(user : String, email)
  4. abstract def return_something_important
  5. # Will work fine
  6. abstract def add(a : Int32 | Float32, b : Int32) : Float64
  7. abstract def greet(user : String, email : String?) : String
  8. abstract def return_something_important : Hash(String, Int32)

You can’t just pass a Service instance around

Right now, you can’t pass a Service or RemoteService instance around.
Pass around its service_id instead, and rebuild the client on the other
end.

  1. # Won't compile
  2. def create_chat_room(name : String) : ChatRoomService
  3. ChatRoomService.new(name)
  4. end
  5. # Will work fine
  6. def create_chat_room_service(name : String) : UInt32
  7. manager.add ChatRoomService.new(name)
  8. end

Then, add a wrapper method to your client doing the conversion for you:

  1. class ChatClient
  2. # ...
  3. def create_chat_room(name : String) : ChatRoomClient
  4. ChatRoomClient.new connection, create_chat_room_service(name)
  5. end
  6. end

When to use The Cannon

  1. Speed is your primary concern
  2. You don’t care about inter-operability
  3. You’re fine with sacrificing structure

Installation

Add this to your application’s shard.yml:

  1. dependencies:
  2. cannon:
  3. github: Papierkorb/cannon

Contributing

  1. Fork it ( https://github.com/Papierkorb/cannon/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am ‘Add some feature’)
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

This library is licensed under the Mozilla Public License 2.0 (“MPL-2”).

For a copy of the full license text see the included LICENSE file.

For a legally non-binding explanation visit:
tl;drLegal

Contributors

Still looking down here?

Have nice day!