Package: darkriftpy
DarkRiftPyDarkRiftPy isDarkrift2written in
Python 3. The implementation is fully compatible with the original version. So
you can write a client side on Python that connects to a Darkrift2 server
written in C# using the original Darkrift2 library, and vice versa.DarkRiftPy is built on top ofasyncio, Python's standard
asynchronus I/O library, and provides a convenient high-level async/await API.Installation$python3-mpipinstalldarkriftpyQuick usage exampleA simple exampls contains two separate scriptsclient.pyandserver.pyfor
client and server respectively.After client is connected to the server the latter waits for a darkrift message
with tag 1, which contains a list of int32 integers in the payload. Once the
message with tag 1 is received, the server starts to randomly select a value
from the given list and sends it back to the client.client.py:importasyncioimportrandomimportdarkriftpyRND_POOL=20MIN_INT32=(2**31)*-1MAX_INT32=2**31-2asyncdefprocess_message(message:darkriftpy.DarkriftMessage)->None:ifmessage.tag!=2:raiseValueError("wrong message received")num=message.get_reader().read_int32()print(f"the server chose the number:{num}")asyncdefmain()->None:try:asyncwithdarkriftpy.connect("127.0.0.1",4296,4296)asclient:items=[random.randint(MIN_INT32,MAX_INT32)for_inrange(RND_POOL)]writer=darkriftpy.DarkriftWriter()writer.write_int32s(items)awaitclient.send(darkriftpy.DarkriftMessage(1,writer.bytes))asyncformessageinclient:awaitprocess_message(message)print("connection has been closed by the server")exceptConnectionError:print("failed to connect to the server")if__name__=="__main__":asyncio.run(main())server.py:importasyncioimportrandomimportdarkriftpyasyncdefhandle_client(client:darkriftpy.DarkriftClient)->None:message=awaitclient.recv()ifmessage.tag!=1:raiseRuntimeError("wrong client message received")client.close()awaitclient.wait_closed()returnreader=message.get_reader()items=reader.read_int32s()whileTrue:writer=darkriftpy.DarkriftWriter()writer.write_int32(random.choice(items))try:awaitclient.send(darkriftpy.DarkriftMessage(2,writer.bytes))exceptdarkriftpy.ConnectionClosedError:print(f"the client({client.connection_id}) has been disconnected")awaitclient.wait_closed()returnawaitasyncio.sleep(1)asyncdefmain()->None:asyncwithdarkriftpy.serve(handle_client,"127.0.0.1",4296,4296)asserver:awaitasyncio.Future()if__name__=="__main__":asyncio.run(main())User defined messagesdarkriftpyprovides a convinient way to create/send/receive user-defined
messages. There is aMessageclass that can be used as a base class for
user-defined ones. The Darkrift tag of a user-defined message is defined by
passing the keywordtagargument in the class definition:importdarkriftpyclassChooseMessage(darkriftpy.Message,tag=1):...For now, theChooseMessagemessage contains no payload. Since theChooseMessageclass is implicitly decorated with the@dataclassdecorator, the user can define class variables withtype
annotationswhich will be automatically deserialized from or serialized to a binary stream
usingDarkriftReaderandDarkriftWriterclasses. Only the following native
types can be used as a class variable type:str,bytes,bool,float.
SinceDarkrift2allows to use types
which are not natively available in python, thedarkriftpy.typesmodule
providesNewTypeextensions to cover all the required Darkrift2 types.importdarkriftpyfromdarkriftpy.typesimportint32classChooseMessage(darkriftpy.Message,tag=1):items:list[int32]As you can see we used theint32type from thedarkriftpy.typesmodule to
define 4 byte signed integer. Since theChooseMessageclass is implicitly
decorated with the@dataclassdecorator and there is no custom constructor, the following constructor will be
created automatically:__init__(self, items: lsit[int32])Therefore, theChooseMessageclass can be instantiated as follows:importrandomimportdarkriftpyfromdarkriftpy.typesimportint32MIN_INT32=(2**31)*-1MAX_INT32=2**31-2classChooseMessage(darkriftpy.Message,tag=1):items:list[int32]message=ChooseMessage([random.randint(MIN_INT32,MAX_INT32)for_inrange(10)])# message.items contains a list with 10 int32 integersSince thedarkriftpy.Messageis inherited fromdarkriftpy.DarkriftMessagethe user-defined message can be passed as is to thesendmethod of thedarkriftpy.DarkriftClientobject.To convert a receiveddarkriftpy.DarkriftMessagemessage to the user-defined
one, the user can do the following:...client:darkriftpy.DarkriftClientmessage:darkriftpy.DarkriftMessage=awaitclient.recv()try:choose_message=ChooseMessage.read(message.get_reader())exceptRuntimeError:# failed to parse the received message...print(choose_message.items)Thedarkriftpypackage provides theMessageContainerclass to
simplify the message serialization and de-siarilization.importdarkriftpyfromdarkriftpy.typesimportint32messages=darkriftpy.MessageContainer()@messages.addclassChooseMessage(darkriftpy.Message,tag=1):items:list[int32]@messages.addclassChoiceMessage(darkriftpy.Message,tag=2):item:int32...client:darkriftpy.DarkriftClientmessage:darkriftpy.DarkriftMessage=awaitclient.recv()try:msg=messages.convert(message)exceptRuntimeError:# failed to convert the received darkrift message# to the user-defined oneifisinstance(msg,ChooseMessage):print(msg.items)elifisinstance(msg,ChoiceMessage):print(msg.item)We used theaddmethod of theMessageContainerclass as decorator to add
the user-defined class into the message containermessages.Theconvertmethod of theMessageContainerclass allows us to convert a raw
darkrift message to the user-defined specific one.Using all these we can create a client wrapper that will return already
deserialized messages.fromcollections.abcimportAsyncIteratorimportdarkriftpyclassClient:def__init__(self,client:darkriftpy.DarkriftClient,messages:darkriftpy.MessageContainer):self._client=clientself._messages=messagesasyncdefrecv(self)->darkriftpy.DarkriftMessage:message=awaitself._client.recv()try:returnself._messages.convert(message)exceptRuntimeError:# just return the message as ispassreturnmessageasyncdefsend(self,message:darkriftpy.DarkriftMessage,reliable:bool=True)->None:awaitself._client.send(message,reliable)def__aiter__(self)->AsyncIterator[darkriftpy.DarkriftMessage]:returnselfasyncdef__anext__(self)->darkriftpy.DarkriftMessage:"""Returns the next message.Stop iteration when the connection is closed."""try:returnawaitself.recv()exceptdarkrift.ConnectionClosedError:raiseStopAsyncIteration()So now we can use the client wrapper to send and receive user specified
messages.Let's update the first example to use all described features.client.py:importasyncioimportrandomfromcollections.abcimportAsyncIteratorimportdarkriftpyfromdarkriftpy.typesimportint32RND_POOL=20MIN_INT32=(2**31)*-1MAX_INT32=2**31-2messages=darkriftpy.MessageContainer()@messages.addclassChooseMessage(darkriftpy.Message,tag=1):items:list[int32]@messages.addclassChoiceMessage(darkriftpy.Message,tag=2):item:int32classClient:def__init__(self,client:darkriftpy.DarkriftClient,messages:darkriftpy.MessageContainer):self._client=clientself._messages=messagesasyncdefrecv(self)->darkriftpy.DarkriftMessage:message=awaitself._client.recv()try:returnself._messages.convert(message)exceptRuntimeError:# just return the message as ispassreturnmessageasyncdefsend(self,message:darkriftpy.DarkriftMessage,reliable:bool=True)->None:awaitself._client.send(message,reliable)def__aiter__(self)->AsyncIterator[darkriftpy.DarkriftMessage]:returnselfasyncdef__anext__(self)->darkriftpy.DarkriftMessage:"""Returns the next message.Stop iteration when the connection is closed."""try:returnawaitself.recv()exceptdarkrift.ConnectionClosedError:raiseStopAsyncIteration()asyncdefprocess_message(message:darkriftpy.DarkriftMessage)->None:ifnotisinstance(message,ChoiceMessage):raiseValueError("wrong message received")print(f"the server chose the number:{message.item}")asyncdefmain():try:c:darkriftpy.DarkriftClientasyncwithdarkriftpy.connect("127.0.0.1",4296,4296)asc:client=Client(c,messages)choose_message=ChooseMessage([random.randint(MIN_INT32,MAX_INT32)for_inrange(RND_POOL)])awaitclient.send(choose_message)asyncformessageinclient:awaitprocess_message(message)print("Connection has been closed by the server")exceptConnectionError:print("failed to connect to the server")if__name__=="__main__":asyncio.run(main())server.py:importasyncioimportrandomfromcollections.abcimportAsyncIteratorimportdarkriftpyfromdarkriftpy.typesimportint32messages=darkriftpy.MessageContainer()@messages.addclassChooseMessage(darkriftpy.Message,tag=1):items:list[int32]@messages.addclassChoiceMessage(darkriftpy.Message,tag=2):item:int32classClient:def__init__(self,client:darkriftpy.DarkriftClient,messages:darkriftpy.MessageContainer):self._client=clientself._messages=messagesasyncdefrecv(self)->darkriftpy.DarkriftMessage:message=awaitself._client.recv()try:returnself._messages.convert(message)exceptRuntimeError:# just return the message as ispassreturnmessageasyncdefsend(self,message:darkriftpy.DarkriftMessage,reliable:bool=True)->None:awaitself._client.send(message,reliable)def__aiter__(self)->AsyncIterator[darkriftpy.DarkriftMessage]:returnselfasyncdef__anext__(self)->darkriftpy.DarkriftMessage:"""Returns the next message.Stop iteration when the connection is closed."""try:returnawaitself.recv()exceptdarkrift.ConnectionClosedError:raiseStopAsyncIteration()asyncdefhandle_client(c:darkriftpy.DarkriftClient)->None:client=Client(c,messages)message=awaitclient.recv()ifnotisinstance(message,ChooseMessage):raiseRuntimeError("wrong client message received")c.close()awaitc.wait_closed()returnwhileTrue:choice_message=ChoiceMessage(random.choice(message.items))try:awaitclient.send(choice_message)exceptdarkriftpy.ConnectionClosedError:print(f"the client({c.connection_id}) has been disconnected")awaitc.wait_closed()returnawaitasyncio.sleep(1)asyncdefmain():asyncwithdarkriftpy.serve(handle_client,"127.0.0.1",4296,4296)asserver:awaitasyncio.Future()if__name__=="__main__":asyncio.run(main())TODO[  ] - Add multiprocessing support to improve performance and scalability
(Fork + Multiplexing I/O).[  ] - Cover the codebase with tests ;).