├── .gitignore ├── .travis.yml ├── COPYING ├── PodSixNet ├── Channel.py ├── Connection.py ├── EndPoint.py ├── Server.py ├── __init__.py ├── asyncwrapper.py ├── rencode.py └── test.py ├── README.md ├── examples ├── ChatClient.py ├── ChatServer.py ├── LagTimeClient.py ├── LagTimeServer.py ├── Whiteboard.py ├── WhiteboardClient.py └── WhiteboardServer.py ├── release └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | PodSixNet.egg-info 3 | *.pyc 4 | *.sublime-project 5 | *.sublime-workspace 6 | MANIFEST 7 | dist 8 | version.py 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.7-dev" 6 | install: 7 | - echo "__version__='0.0.0-test'" > PodSixNet/version.py 8 | - pip install -e . 9 | script: 10 | - python PodSixNet/test.py 11 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /PodSixNet/Channel.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | from PodSixNet.asyncwrapper import asynchat 5 | from PodSixNet.rencode import loads, dumps 6 | 7 | class Channel(asynchat.async_chat): 8 | endchars = '\0---\0' 9 | def __init__(self, conn=None, addr=(), server=None, map=None): 10 | asynchat.async_chat.__init__(self, getattr(conn, "socket", conn), map) 11 | self.addr = addr 12 | self._server = server 13 | self._ibuffer = b"" 14 | self.set_terminator(self.endchars.encode()) 15 | self.sendqueue = [] 16 | 17 | def collect_incoming_data(self, data): 18 | self._ibuffer += data 19 | 20 | def found_terminator(self): 21 | data = loads(self._ibuffer) 22 | self._ibuffer = b"" 23 | 24 | if type(dict()) == type(data) and 'action' in data: 25 | [getattr(self, n)(data) for n in ('Network_' + data['action'], 'Network') if hasattr(self, n)] 26 | else: 27 | print("OOB data:", data) 28 | 29 | def Pump(self): 30 | [asynchat.async_chat.push(self, d) for d in self.sendqueue] 31 | self.sendqueue = [] 32 | 33 | def Send(self, data): 34 | """Returns the number of bytes sent after enoding.""" 35 | outgoing = dumps(data) + self.endchars.encode() 36 | self.sendqueue.append(outgoing) 37 | return len(outgoing) 38 | 39 | def handle_connect(self): 40 | if hasattr(self, "Connected"): 41 | self.Connected() 42 | else: 43 | print("Unhandled Connected()") 44 | 45 | def handle_error(self): 46 | try: 47 | self.close() 48 | except: 49 | pass 50 | if hasattr(self, "Error"): 51 | self.Error(sys.exc_info()[1]) 52 | else: 53 | asynchat.async_chat.handle_error(self) 54 | 55 | def handle_expt(self): 56 | pass 57 | 58 | def handle_close(self): 59 | if hasattr(self, "Close"): 60 | self.Close() 61 | asynchat.async_chat.handle_close(self) 62 | -------------------------------------------------------------------------------- /PodSixNet/Connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | A client's connection to the server. 3 | 4 | This module contains two components: a singleton called 'connection' and a class called 'ConnectionListener'. 5 | 6 | 'connection' is a singleton instantiation of an EndPoint which will be connected to the server at the other end. It's a singleton because each client should only need one of these in most multiplayer scenarios. (If a client needs more than one connection to the server, a more complex architecture can be built out of instantiated EndPoint()s.) The connection is based on Python's asyncore and so it should have it's polling loop run periodically, probably once per gameloop. This just means putting "from Connection import connection; connection.Pump()" somewhere in your top level gameloop. 7 | 8 | Subclass ConnectionListener in order to have an object that will receive network events. For example, you might have a GUI element which is a label saying how many players there are online. You would declare it like 'class NumPlayersLabel(ConnectionListener, ...):' Later you'd instantitate it 'n = NumPlayersLabel()' and then somewhere in your loop you'd have 'n.Pump()' which asks the connection singleton if there are any new messages from the network, and calls the 'Network_' callbacks for each bit of new data from the server. So you'd implement a method like "def Network_players(self, data):" which would be called whenever a message from the server arrived which looked like {"action": "players", "number": 5}. 9 | """ 10 | 11 | from __future__ import print_function 12 | 13 | from PodSixNet.EndPoint import EndPoint 14 | 15 | connection = EndPoint() 16 | 17 | class ConnectionListener: 18 | """ 19 | Looks at incoming data and calls "Network_" methods in self, based on what messages come in. 20 | Subclass this to have your own classes monitor incoming network messages. 21 | For example, a method called "Network_players(self, data)" will be called when a message arrives like: 22 | {"action": "players", "number": 5, ....} 23 | """ 24 | def Connect(self, *args, **kwargs): 25 | connection.DoConnect(*args, **kwargs) 26 | # check for connection errors: 27 | self.Pump() 28 | 29 | def Pump(self): 30 | for data in connection.GetQueue(): 31 | [getattr(self, n)(data) for n in ("Network_" + data['action'], "Network") if hasattr(self, n)] 32 | 33 | def Send(self, data): 34 | """ Convenience method to allow this listener to appear to send network data, whilst actually using connection. """ 35 | connection.Send(data) 36 | 37 | if __name__ == "__main__": 38 | from time import sleep 39 | from sys import exit 40 | class ConnectionTest(ConnectionListener): 41 | def Network(self, data): 42 | print("Network:", data) 43 | 44 | def Network_error(self, error): 45 | print("error:", error['error']) 46 | print("Did you start a server?") 47 | exit(-1) 48 | 49 | def Network_connected(self, data): 50 | print("connection test Connected") 51 | 52 | c = ConnectionTest() 53 | 54 | c.Connect() 55 | while 1: 56 | connection.Pump() 57 | c.Pump() 58 | sleep(0.001) 59 | -------------------------------------------------------------------------------- /PodSixNet/EndPoint.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import socket 3 | 4 | from PodSixNet.asyncwrapper import poll 5 | from PodSixNet.Channel import Channel 6 | 7 | class EndPoint(Channel): 8 | """ 9 | The endpoint queues up all network events for other classes to read. 10 | """ 11 | def __init__(self, address=("127.0.0.1", 31425), map=None): 12 | self.address = address 13 | self.isConnected = False 14 | self.queue = [] 15 | if map is None: 16 | self._map = {} 17 | else: 18 | self._map = map 19 | 20 | def DoConnect(self, address=None): 21 | if address: 22 | self.address = address 23 | try: 24 | Channel.__init__(self, map=self._map) 25 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 26 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 27 | self.connect(self.address) 28 | except socket.gaierror as e: 29 | self.queue.append({"action": "error", "error": e.args}) 30 | except socket.error as e: 31 | self.queue.append({"action": "error", "error": e.args}) 32 | 33 | def GetQueue(self): 34 | return self.queue 35 | 36 | def Pump(self): 37 | Channel.Pump(self) 38 | self.queue = [] 39 | poll(map=self._map) 40 | 41 | # methods to add network data to the queue depending on network events 42 | 43 | def Close(self): 44 | self.isConnected = False 45 | self.close() 46 | self.queue.append({"action": "disconnected"}) 47 | 48 | def Connected(self): 49 | self.queue.append({"action": "socketConnect"}) 50 | 51 | def Network_connected(self, data): 52 | self.isConnected = True 53 | 54 | def Network(self, data): 55 | self.queue.append(data) 56 | 57 | def Error(self, error): 58 | self.queue.append({"action": "error", "error": error}) 59 | 60 | def ConnectionError(self): 61 | self.isConnected = False 62 | self.queue.append({"action": "error", "error": (-1, "Connection error")}) 63 | 64 | -------------------------------------------------------------------------------- /PodSixNet/Server.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import socket 3 | 4 | from PodSixNet.asyncwrapper import poll, asyncore 5 | from PodSixNet.Channel import Channel 6 | 7 | class Server(asyncore.dispatcher): 8 | channelClass = Channel 9 | 10 | def __init__(self, channelClass=None, localaddr=("127.0.0.1", 5071), listeners=5): 11 | if channelClass: 12 | self.channelClass = channelClass 13 | self._map = {} 14 | self.channels = [] 15 | asyncore.dispatcher.__init__(self, map=self._map) 16 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 17 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 18 | self.set_reuse_addr() 19 | self.bind(localaddr) 20 | self.listen(listeners) 21 | 22 | def handle_accept(self): 23 | try: 24 | conn, addr = self.accept() 25 | except socket.error: 26 | print('warning: server accept() threw an exception') 27 | return 28 | except TypeError: 29 | print('warning: server accept() threw EWOULDBLOCK') 30 | return 31 | print("connection") 32 | self.channels.append(self.channelClass(conn, addr, self, self._map)) 33 | self.channels[-1].Send({"action": "connected"}) 34 | if hasattr(self, "Connected"): 35 | self.Connected(self.channels[-1], addr) 36 | 37 | def Pump(self): 38 | [c.Pump() for c in self.channels] 39 | poll(map=self._map) 40 | 41 | -------------------------------------------------------------------------------- /PodSixNet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/PodSixNet/3d7e67fb6f2921665e2a18aceff051314350e1c5/PodSixNet/__init__.py -------------------------------------------------------------------------------- /PodSixNet/asyncwrapper.py: -------------------------------------------------------------------------------- 1 | """ monkey patched version of asynchat to allow map argument on all version of Python, and the best version of the poll function. """ 2 | from sys import version 3 | 4 | import asynchat 5 | import asyncore 6 | 7 | if float(version[:3]) < 2.5: 8 | from asyncore import poll2 as poll 9 | else: 10 | from asyncore import poll 11 | 12 | # monkey patch older versions to support maps in asynchat. Yuck. 13 | if float(version[:3]) < 2.6: 14 | def asynchat_monkey_init(self, conn=None, map=None): 15 | self.ac_in_buffer = b'' 16 | self.ac_out_buffer = b'' 17 | self.producer_fifo = asynchat.fifo() 18 | asyncore.dispatcher.__init__ (self, sock=conn, map=map) 19 | 20 | #asynchat.async_chat.__init__ = asynchat_monkey_init 21 | -------------------------------------------------------------------------------- /PodSixNet/rencode.py: -------------------------------------------------------------------------------- 1 | # https://github.com/aresch/rencode 2 | # 3 | # This is tweaked to add native tuple type 4 | # 5 | # Original bencode module by Petru Paler, et al. 6 | # 7 | # Modifications by Connelly Barnes: 8 | # 9 | # - Added support for floats (sent as 32-bit or 64-bit in network 10 | # order), bools, None. 11 | # - Allowed dict keys to be of any serializable type. 12 | # - Lists/tuples are always decoded as tuples (thus, tuples can be 13 | # used as dict keys). 14 | # - Embedded extra information in the 'typecodes' to save some space. 15 | # - Added a restriction on integer length, so that malicious hosts 16 | # cannot pass us large integers which take a long time to decode. 17 | # 18 | # Licensed by Bram Cohen under the "MIT license": 19 | # 20 | # "Copyright (C) 2001-2002 Bram Cohen 21 | # 22 | # Permission is hereby granted, free of charge, to any person 23 | # obtaining a copy of this software and associated documentation files 24 | # (the "Software"), to deal in the Software without restriction, 25 | # including without limitation the rights to use, copy, modify, merge, 26 | # publish, distribute, sublicense, and/or sell copies of the Software, 27 | # and to permit persons to whom the Software is furnished to do so, 28 | # subject to the following conditions: 29 | # 30 | # The above copyright notice and this permission notice shall be 31 | # included in all copies or substantial portions of the Software. 32 | # 33 | # The Software is provided "AS IS", without warranty of any kind, 34 | # express or implied, including but not limited to the warranties of 35 | # merchantability, fitness for a particular purpose and 36 | # noninfringement. In no event shall the authors or copyright holders 37 | # be liable for any claim, damages or other liability, whether in an 38 | # action of contract, tort or otherwise, arising from, out of or in 39 | # connection with the Software or the use or other dealings in the 40 | # Software." 41 | # 42 | # (The rencode module is licensed under the above license as well). 43 | # 44 | # pylint: disable=redefined-builtin 45 | 46 | """ 47 | rencode -- Web safe object pickling/unpickling. 48 | 49 | Public domain, Connelly Barnes 2006-2007. 50 | 51 | The rencode module is a modified version of bencode from the 52 | BitTorrent project. For complex, heterogeneous data structures with 53 | many small elements, r-encodings take up significantly less space than 54 | b-encodings: 55 | 56 | >>> len(rencode.dumps({'a':0, 'b':[1,2], 'c':99})) 57 | 13 58 | >>> len(bencode.bencode({'a':0, 'b':[1,2], 'c':99})) 59 | 26 60 | 61 | The rencode format is not standardized, and may change with different 62 | rencode module versions, so you should check that you are using the 63 | same rencode version throughout your project. 64 | """ 65 | 66 | import struct 67 | import sys 68 | from threading import Lock 69 | 70 | __version__ = ("Python", 1, 0, 5) 71 | __all__ = ('dumps', 'loads') 72 | 73 | py3 = sys.version_info[0] >= 3 74 | if py3: 75 | long = int 76 | unicode = str 77 | 78 | def int2byte(c): 79 | return bytes([c]) 80 | else: 81 | def int2byte(c): 82 | return chr(c) 83 | 84 | # Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()). 85 | DEFAULT_FLOAT_BITS = 32 86 | 87 | # Maximum length of integer when written as base 10 string. 88 | MAX_INT_LENGTH = 64 89 | 90 | # The bencode 'typecodes' such as i, d, etc have been extended and 91 | # relocated on the base-256 character set. 92 | CHR_TUPLE = int2byte(58) 93 | CHR_LIST = int2byte(59) 94 | CHR_DICT = int2byte(60) 95 | CHR_INT = int2byte(61) 96 | CHR_INT1 = int2byte(62) 97 | CHR_INT2 = int2byte(63) 98 | CHR_INT4 = int2byte(64) 99 | CHR_INT8 = int2byte(65) 100 | CHR_FLOAT32 = int2byte(66) 101 | CHR_FLOAT64 = int2byte(44) 102 | CHR_TRUE = int2byte(67) 103 | CHR_FALSE = int2byte(68) 104 | CHR_NONE = int2byte(69) 105 | CHR_TERM = int2byte(127) 106 | 107 | # Positive integers with value embedded in typecode. 108 | INT_POS_FIXED_START = 0 109 | INT_POS_FIXED_COUNT = 44 110 | 111 | # Dictionaries with length embedded in typecode. 112 | DICT_FIXED_START = 102 113 | DICT_FIXED_COUNT = 25 114 | 115 | # Negative integers with value embedded in typecode. 116 | INT_NEG_FIXED_START = 70 117 | INT_NEG_FIXED_COUNT = 32 118 | 119 | # Strings with length embedded in typecode. 120 | STR_FIXED_START = 128 121 | STR_FIXED_COUNT = 64 122 | 123 | # Lists with length embedded in typecode. 124 | LIST_FIXED_START = STR_FIXED_START + STR_FIXED_COUNT 125 | LIST_FIXED_COUNT = 32 126 | 127 | # Tuples with length embedded in typecode. 128 | TUPLE_FIXED_START = LIST_FIXED_START + LIST_FIXED_COUNT 129 | TUPLE_FIXED_COUNT = 32 130 | 131 | # Whether strings should be decoded when loading 132 | _decode_utf8 = True 133 | 134 | 135 | def decode_int(x, f): 136 | f += 1 137 | newf = x.index(CHR_TERM, f) 138 | if newf - f >= MAX_INT_LENGTH: 139 | raise ValueError('overflow') 140 | try: 141 | n = int(x[f:newf]) 142 | except (OverflowError, ValueError): 143 | n = long(x[f:newf]) 144 | if x[f:f + 1] == '-': 145 | if x[f + 1:f + 2] == '0': 146 | raise ValueError 147 | elif x[f:f + 1] == '0' and newf != f + 1: 148 | raise ValueError 149 | return (n, newf + 1) 150 | 151 | 152 | def decode_intb(x, f): 153 | f += 1 154 | return (struct.unpack('!b', x[f:f + 1])[0], f + 1) 155 | 156 | 157 | def decode_inth(x, f): 158 | f += 1 159 | return (struct.unpack('!h', x[f:f + 2])[0], f + 2) 160 | 161 | 162 | def decode_intl(x, f): 163 | f += 1 164 | 165 | return (struct.unpack('!l', x[f:f + 4])[0], f + 4) 166 | 167 | 168 | def decode_intq(x, f): 169 | f += 1 170 | return (struct.unpack('!q', x[f:f + 8])[0], f + 8) 171 | 172 | 173 | def decode_float32(x, f): 174 | f += 1 175 | n = struct.unpack('!f', x[f:f + 4])[0] 176 | return (n, f + 4) 177 | 178 | 179 | def decode_float64(x, f): 180 | f += 1 181 | n = struct.unpack('!d', x[f:f + 8])[0] 182 | return (n, f + 8) 183 | 184 | 185 | def decode_string(x, f): 186 | colon = x.index(b':', f) 187 | try: 188 | n = int(x[f:colon]) 189 | except (OverflowError, ValueError): 190 | n = long(x[f:colon]) 191 | if x[f] == '0' and colon != f + 1: 192 | raise ValueError 193 | colon += 1 194 | s = x[colon:colon + n] 195 | if _decode_utf8: 196 | s = s.decode('utf8') 197 | return (s, colon + n) 198 | 199 | 200 | def decode_list(x, f): 201 | r, f = [], f + 1 202 | while x[f:f + 1] != CHR_TERM: 203 | v, f = decode_func[x[f:f + 1]](x, f) 204 | r.append(v) 205 | return (r, f + 1) 206 | 207 | def decode_tuple(x, f): 208 | r, f = [], f + 1 209 | while x[f:f + 1] != CHR_TERM: 210 | v, f = decode_func[x[f:f + 1]](x, f) 211 | r.append(v) 212 | return (tuple(r), f + 1) 213 | 214 | def decode_dict(x, f): 215 | r, f = {}, f + 1 216 | while x[f:f + 1] != CHR_TERM: 217 | k, f = decode_func[x[f:f + 1]](x, f) 218 | r[k], f = decode_func[x[f:f + 1]](x, f) 219 | return (r, f + 1) 220 | 221 | 222 | def decode_true(x, f): 223 | return (True, f + 1) 224 | 225 | 226 | def decode_false(x, f): 227 | return (False, f + 1) 228 | 229 | 230 | def decode_none(x, f): 231 | return (None, f + 1) 232 | 233 | decode_func = {} 234 | decode_func[b'0'] = decode_string 235 | decode_func[b'1'] = decode_string 236 | decode_func[b'2'] = decode_string 237 | decode_func[b'3'] = decode_string 238 | decode_func[b'4'] = decode_string 239 | decode_func[b'5'] = decode_string 240 | decode_func[b'6'] = decode_string 241 | decode_func[b'7'] = decode_string 242 | decode_func[b'8'] = decode_string 243 | decode_func[b'9'] = decode_string 244 | decode_func[CHR_TUPLE] = decode_tuple 245 | decode_func[CHR_LIST] = decode_list 246 | decode_func[CHR_DICT] = decode_dict 247 | decode_func[CHR_INT] = decode_int 248 | decode_func[CHR_INT1] = decode_intb 249 | decode_func[CHR_INT2] = decode_inth 250 | decode_func[CHR_INT4] = decode_intl 251 | decode_func[CHR_INT8] = decode_intq 252 | decode_func[CHR_FLOAT32] = decode_float32 253 | decode_func[CHR_FLOAT64] = decode_float64 254 | decode_func[CHR_TRUE] = decode_true 255 | decode_func[CHR_FALSE] = decode_false 256 | decode_func[CHR_NONE] = decode_none 257 | 258 | 259 | def make_fixed_length_string_decoders(): 260 | def make_decoder(slen): 261 | def f(x, f): 262 | s = x[f + 1:f + 1 + slen] 263 | if _decode_utf8: 264 | s = s.decode("utf8") 265 | return (s, f + 1 + slen) 266 | return f 267 | for i in range(STR_FIXED_COUNT): 268 | decode_func[int2byte(STR_FIXED_START + i)] = make_decoder(i) 269 | 270 | make_fixed_length_string_decoders() 271 | 272 | 273 | def make_fixed_length_list_decoders(): 274 | def make_decoder(slen): 275 | def f(x, f): 276 | r, f = [], f + 1 277 | for _ in range(slen): 278 | v, f = decode_func[x[f:f + 1]](x, f) 279 | r.append(v) 280 | return (list(r), f) 281 | return f 282 | for i in range(LIST_FIXED_COUNT): 283 | decode_func[int2byte(LIST_FIXED_START + i)] = make_decoder(i) 284 | 285 | make_fixed_length_list_decoders() 286 | 287 | def make_fixed_length_tuple_decoders(): 288 | def make_decoder(slen): 289 | def f(x, f): 290 | r, f = [], f + 1 291 | for _ in range(slen): 292 | v, f = decode_func[x[f:f + 1]](x, f) 293 | r.append(v) 294 | return (tuple(r), f) 295 | return f 296 | for i in range(TUPLE_FIXED_COUNT): 297 | decode_func[int2byte(TUPLE_FIXED_START + i)] = make_decoder(i) 298 | 299 | make_fixed_length_tuple_decoders() 300 | 301 | def make_fixed_length_int_decoders(): 302 | def make_decoder(j): 303 | def f(x, f): 304 | return (j, f + 1) 305 | return f 306 | for i in range(INT_POS_FIXED_COUNT): 307 | decode_func[int2byte(INT_POS_FIXED_START + i)] = make_decoder(i) 308 | for i in range(INT_NEG_FIXED_COUNT): 309 | decode_func[int2byte(INT_NEG_FIXED_START + i)] = make_decoder(-1 - i) 310 | 311 | make_fixed_length_int_decoders() 312 | 313 | 314 | def make_fixed_length_dict_decoders(): 315 | def make_decoder(slen): 316 | def f(x, f): 317 | r, f = {}, f + 1 318 | for _ in range(slen): 319 | k, f = decode_func[x[f:f + 1]](x, f) 320 | r[k], f = decode_func[x[f:f + 1]](x, f) 321 | return (r, f) 322 | return f 323 | for i in range(DICT_FIXED_COUNT): 324 | decode_func[int2byte(DICT_FIXED_START + i)] = make_decoder(i) 325 | 326 | make_fixed_length_dict_decoders() 327 | 328 | 329 | def loads(x, decode_utf8=True): 330 | global _decode_utf8 331 | _decode_utf8 = decode_utf8 332 | try: 333 | r, l = decode_func[x[0:1]](x, 0) 334 | except (IndexError, KeyError): 335 | raise ValueError 336 | if l != len(x): 337 | raise ValueError 338 | return r 339 | 340 | 341 | def encode_int(x, r): 342 | if 0 <= x < INT_POS_FIXED_COUNT: 343 | r.append(int2byte(INT_POS_FIXED_START + x)) 344 | elif -INT_NEG_FIXED_COUNT <= x < 0: 345 | r.append(int2byte(INT_NEG_FIXED_START - 1 - x)) 346 | elif -128 <= x < 128: 347 | r.extend((CHR_INT1, struct.pack('!b', x))) 348 | elif -32768 <= x < 32768: 349 | r.extend((CHR_INT2, struct.pack('!h', x))) 350 | elif -2147483648 <= x < 2147483648: 351 | r.extend((CHR_INT4, struct.pack('!l', x))) 352 | elif -9223372036854775808 <= x < 9223372036854775808: 353 | r.extend((CHR_INT8, struct.pack('!q', x))) 354 | else: 355 | s = str(x) 356 | if py3: 357 | s = bytes(s, "ascii") 358 | 359 | if len(s) >= MAX_INT_LENGTH: 360 | raise ValueError('overflow') 361 | r.extend((CHR_INT, s, CHR_TERM)) 362 | 363 | 364 | def encode_float32(x, r): 365 | r.extend((CHR_FLOAT32, struct.pack('!f', x))) 366 | 367 | 368 | def encode_float64(x, r): 369 | r.extend((CHR_FLOAT64, struct.pack('!d', x))) 370 | 371 | 372 | def encode_bool(x, r): 373 | r.append({False: CHR_FALSE, True: CHR_TRUE}[bool(x)]) 374 | 375 | 376 | def encode_none(x, r): 377 | r.append(CHR_NONE) 378 | 379 | 380 | def encode_string(x, r): 381 | if len(x) < STR_FIXED_COUNT: 382 | r.extend((int2byte(STR_FIXED_START + len(x)), x)) 383 | else: 384 | s = str(len(x)) 385 | if py3: 386 | s = bytes(s, "ascii") 387 | r.extend((s, b':', x)) 388 | 389 | 390 | def encode_unicode(x, r): 391 | encode_string(x.encode("utf8"), r) 392 | 393 | 394 | def encode_list(x, r): 395 | if len(x) < LIST_FIXED_COUNT: 396 | r.append(int2byte(LIST_FIXED_START + len(x))) 397 | for i in x: 398 | encode_func[type(i)](i, r) 399 | else: 400 | r.append(CHR_LIST) 401 | for i in x: 402 | encode_func[type(i)](i, r) 403 | r.append(CHR_TERM) 404 | 405 | def encode_tuple(x, r): 406 | if len(x) < TUPLE_FIXED_COUNT: 407 | r.append(int2byte(TUPLE_FIXED_START + len(x))) 408 | for i in x: 409 | encode_func[type(i)](i, r) 410 | else: 411 | r.append(CHR_TUPLE) 412 | for i in x: 413 | encode_func[type(i)](i, r) 414 | r.append(CHR_TERM) 415 | 416 | def encode_dict(x, r): 417 | if len(x) < DICT_FIXED_COUNT: 418 | r.append(int2byte(DICT_FIXED_START + len(x))) 419 | for k, v in x.items(): 420 | encode_func[type(k)](k, r) 421 | encode_func[type(v)](v, r) 422 | else: 423 | r.append(CHR_DICT) 424 | for k, v in x.items(): 425 | encode_func[type(k)](k, r) 426 | encode_func[type(v)](v, r) 427 | r.append(CHR_TERM) 428 | 429 | encode_func = {} 430 | encode_func[int] = encode_int 431 | encode_func[long] = encode_int 432 | encode_func[bytes] = encode_string 433 | encode_func[list] = encode_list 434 | encode_func[tuple] = encode_tuple 435 | encode_func[dict] = encode_dict 436 | encode_func[type(None)] = encode_none 437 | encode_func[unicode] = encode_unicode 438 | encode_func[bool] = encode_bool 439 | 440 | lock = Lock() 441 | 442 | 443 | def dumps(x, float_bits=DEFAULT_FLOAT_BITS): 444 | """ 445 | Dump data structure to str. 446 | 447 | Here float_bits is either 32 or 64. 448 | """ 449 | with lock: 450 | if float_bits == 32: 451 | encode_func[float] = encode_float32 452 | elif float_bits == 64: 453 | encode_func[float] = encode_float64 454 | else: 455 | raise ValueError('Float bits (%d) is not 32 or 64' % float_bits) 456 | r = [] 457 | encode_func[type(x)](x, r) 458 | return b''.join(r) 459 | 460 | 461 | def test(): 462 | f1 = struct.unpack('!f', struct.pack('!f', 25.5))[0] 463 | f2 = struct.unpack('!f', struct.pack('!f', 29.3))[0] 464 | f3 = struct.unpack('!f', struct.pack('!f', -0.6))[0] 465 | ld = (({b'a': 15, b'bb': f1, b'ccc': f2, b'': (f3, (), False, True, b'')}, (b'a', 10**20), 466 | tuple(range(-100000, 100000)), b'b' * 31, b'b' * 62, b'b' * 64, 2**30, 2**33, 2**62, 467 | 2**64, 2**30, 2**33, 2**62, 2**64, False, False, True, -1, 2, 0),) 468 | assert loads(dumps(ld)) == ld 469 | d = dict(zip(range(-100000, 100000), range(-100000, 100000))) 470 | d.update({b'a': 20, 20: 40, 40: 41, f1: f2, f2: f3, f3: False, False: True, True: False}) 471 | ld = (d, {}, {5: 6}, {7: 7, True: 8}, {9: 10, 22: 39, 49: 50, 44: b''}) 472 | assert loads(dumps(ld)) == ld 473 | ld = (b'', b'a' * 10, b'a' * 100, b'a' * 1000, b'a' * 10000, b'a' * 100000, b'a' * 1000000, b'a' * 10000000) 474 | assert loads(dumps(ld)) == ld 475 | ld = tuple([dict(zip(range(n), range(n))) for n in range(100)]) + (b'b',) 476 | assert loads(dumps(ld)) == ld 477 | ld = tuple([dict(zip(range(n), range(-n, 0))) for n in range(100)]) + (b'b',) 478 | assert loads(dumps(ld)) == ld 479 | ld = tuple([tuple(range(n)) for n in range(100)]) + (b'b',) 480 | assert loads(dumps(ld)) == ld 481 | ld = tuple([b'a' * n for n in range(1000)]) + (b'b',) 482 | assert loads(dumps(ld)) == ld 483 | ld = tuple([b'a' * n for n in range(1000)]) + (None, True, None) 484 | assert loads(dumps(ld)) == ld 485 | assert loads(dumps(None)) is None 486 | assert loads(dumps({None: None})) == {None: None} 487 | assert 1e-10 < abs(loads(dumps(1.1)) - 1.1) < 1e-6 488 | assert 1e-10 < abs(loads(dumps(1.1, 32)) - 1.1) < 1e-6 489 | assert abs(loads(dumps(1.1, 64)) - 1.1) < 1e-12 490 | assert loads(dumps("Hello World!!"), decode_utf8=True) 491 | try: 492 | import psyco 493 | psyco.bind(dumps) 494 | psyco.bind(loads) 495 | except ImportError: 496 | pass 497 | 498 | 499 | if __name__ == '__main__': 500 | test() 501 | -------------------------------------------------------------------------------- /PodSixNet/test.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | 4 | import unittest 5 | import sys 6 | from time import sleep, time 7 | import socket 8 | 9 | from PodSixNet.asyncwrapper import poll, asyncore 10 | from PodSixNet.Server import Server 11 | from PodSixNet.Channel import Channel 12 | from PodSixNet.EndPoint import EndPoint 13 | 14 | class FailEndPointTestCase(unittest.TestCase): 15 | def setUp(self): 16 | 17 | class FailEndPoint(EndPoint): 18 | def __init__(self): 19 | EndPoint.__init__(self, ("localhost", 31429)) 20 | self.result = "" 21 | 22 | def Error(self, error): 23 | self.result = error 24 | 25 | def Test(self): 26 | self.DoConnect() 27 | start = time() 28 | while not self.result and time() - start < 10: 29 | self.Pump() 30 | sleep(0.001) 31 | 32 | self.endpoint_bad = FailEndPoint() 33 | 34 | def runTest(self): 35 | self.endpoint_bad.Test() 36 | want = "[Errno 111] Connection refused" 37 | self.assertEqual(str(self.endpoint_bad.result), str(want), "Socket got %s instead of %s" % (str(self.endpoint_bad.result), str(want))) 38 | 39 | def tearDown(self): 40 | del self.endpoint_bad 41 | 42 | class EndPointTestCase(unittest.TestCase): 43 | def setUp(self): 44 | self.outgoing = [ 45 | {"action": "hello", "data": {"a": 321, "b": [2, 3, 4], "c": ["afw", "wafF", "aa", "weEEW", "w234r"], "d": ["x"] * 256}}, 46 | {"action": "hello", "data": [454, 35, 43, 543, "aabv"]}, 47 | {"action": "hello", "data": [10] * 512}, 48 | #{"action": "hello", "data": [10] * 512, "otherstuff": "hello\0---\0goodbye", "x": [0, "---", 0], "y": "zäö"}, 49 | ] 50 | self.count = len(self.outgoing) 51 | self.lengths = [len(data['data']) for data in self.outgoing] 52 | 53 | class ServerChannel(Channel): 54 | def Network_hello(self, data): 55 | self._server.received.append(data) 56 | self._server.count += 1 57 | self.Send({"action": "gotit", "data": "Yeah, we got it: " + str(len(data['data'])) + " elements"}) 58 | 59 | class TestEndPoint(EndPoint): 60 | received = [] 61 | connected = False 62 | count = 0 63 | 64 | def Network_connected(self, data): 65 | self.connected = True 66 | 67 | def Network_gotit(self, data): 68 | self.received.append(data) 69 | self.count += 1 70 | 71 | 72 | class TestServer(Server): 73 | connected = False 74 | received = [] 75 | count = 0 76 | 77 | def Connected(self, channel, addr): 78 | self.connected = True 79 | 80 | self.server = TestServer(channelClass=ServerChannel, localaddr=("127.0.0.1", 31426)) 81 | self.endpoint = TestEndPoint(("127.0.0.1", 31426)) 82 | 83 | def runTest(self): 84 | self.endpoint.DoConnect() 85 | for o in self.outgoing: 86 | self.endpoint.Send(o) 87 | 88 | 89 | for x in range(50): 90 | self.server.Pump() 91 | self.endpoint.Pump() 92 | 93 | # see if what we receive from the server is what we expect 94 | for r in self.server.received: 95 | self.assertTrue(r == self.outgoing.pop(0)) 96 | self.server.received = [] 97 | 98 | # see if what we receive from the client is what we expect 99 | for r in self.endpoint.received: 100 | self.assertTrue(r['data'] == "Yeah, we got it: %d elements" % self.lengths.pop(0)) 101 | self.endpoint.received = [] 102 | 103 | sleep(0.001) 104 | 105 | self.assertTrue(self.server.connected, "Server is not connected") 106 | self.assertTrue(self.endpoint.connected, "Endpoint is not connected") 107 | 108 | self.assertTrue(self.server.count == self.count, "Didn't receive the right number of messages") 109 | self.assertTrue(self.endpoint.count == self.count, "Didn't receive the right number of messages") 110 | 111 | self.endpoint.Close() 112 | 113 | 114 | def tearDown(self): 115 | del self.server 116 | del self.endpoint 117 | 118 | class ServerTestCase(unittest.TestCase): 119 | testdata = {"action": "hello", "data": {"a": 321, "b": [2, 3, 4], "c": ["afw", "wafF", "aa", "weEEW", "w234r"], "d": ["x"] * 256}} 120 | def setUp(self): 121 | print("ServerTestCase") 122 | print("--------------") 123 | 124 | class ServerChannel(Channel): 125 | def Network_hello(self, data): 126 | print("*Server* ran test method for 'hello' action") 127 | print("*Server* received:", data) 128 | self._server.received = data 129 | 130 | class EndPointChannel(Channel): 131 | connected = False 132 | def Connected(self): 133 | print("*EndPoint* Connected()") 134 | 135 | def Network_connected(self, data): 136 | self.connected = True 137 | print("*EndPoint* Network_connected(", data, ")") 138 | print("*EndPoint* initiating send") 139 | self.Send(ServerTestCase.testdata) 140 | 141 | class TestServer(Server): 142 | connected = False 143 | received = None 144 | def Connected(self, channel, addr): 145 | self.connected = True 146 | print("*Server* Connected() ", channel, "connected on", addr) 147 | 148 | self.server = TestServer(channelClass=ServerChannel, localaddr=("127.0.0.1", 31427)) 149 | 150 | sender = asyncore.dispatcher(map=self.server._map) 151 | sender.create_socket(socket.AF_INET, socket.SOCK_STREAM) 152 | sender.connect(("127.0.0.1", 31427)) 153 | self.outgoing = EndPointChannel(sender, map=self.server._map) 154 | 155 | def runTest(self): 156 | from time import sleep 157 | print("*** polling for half a second") 158 | for x in range(250): 159 | self.server.Pump() 160 | self.outgoing.Pump() 161 | if self.server.received: 162 | self.assertTrue(self.server.received == self.testdata) 163 | self.server.received = None 164 | sleep(0.001) 165 | self.assertTrue(self.server.connected == True, "Server is not connected") 166 | self.assertTrue(self.outgoing.connected == True, "Outgoing socket is not connected") 167 | 168 | def tearDown(self): 169 | pass 170 | del self.server 171 | del self.outgoing 172 | 173 | 174 | if __name__ == "__main__": 175 | unittest.main() 176 | 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *Note:* this project uses deprecated libraries ([#46](https://github.com/chr15m/PodSixNet/issues/46)) and isn't actively maintained any more. 2 | 3 | PodSixNet - lightweight multiplayer networking library for Python games 4 | ----------------------------------------------------------------------- 5 | 6 | PodSixNet is a lightweight network layer designed to make it easy to write multiplayer games in Python. It uses Python's built in asyncore library and rencode.py (included) to asynchronously serialise network events and arbitrary data structures, and deliver them to your high level classes through simple callback methods. 7 | 8 | Each class within your game client which wants to receive network events, subclasses the ConnectionListener class and then implements `Network_*` methods to catch specific user-defined events from the server. You don't have to wait for buffers to fill, or check sockets for waiting data or anything like that, just do `connection.Pump()` once per game loop and the library will handle everything else for you, passing off events to all classes that are listening. Sending data back to the server is just as easy, using `connection.Send(mydata)`. Likewise on the server side, events are propagated to `Network_*` method callbacks and data is sent back to clients with the `client.Send(mydata)` method. 9 | 10 | The [PodSixNet mailing list](http://groups.google.com/group/podsixnet) is good for getting help from other users. 11 | 12 | For users of the Construct game making environment for Windows, there is a tutorial on doing multiplayer networking with PodSixNet, [here](http://www.scirra.com/forum/viewtopic.php?f=8&t=6299). Thanks to Dave Chabo for contributing this tutorial. 13 | 14 | Here is [another tutorial by Julian Meyer](http://www.raywenderlich.com/38732/multiplayer-game-programming-for-teens-with-python). 15 | 16 | Install 17 | ------- 18 | 19 | `pip install PodSixNet` 20 | 21 | or 22 | 23 | `easy_install PodSixNet` 24 | 25 | 26 | ### From source 27 | 28 | First make sure you have [Python](http://python.org/) 2.4 or greater installed (python 3 also works). 29 | 30 | Next you'll want to get the PodSixNet source. 31 | 32 | The module is found inside a subdirectory called PodSixNet within the top level folder. There's an `__init__.py` inside there, so you can just copy or symlink the PodSixNet sub-directory into your own project and then do `import PodSixNet`, or else you can run `sudo python setup.py install` to install PodSixNet into your Python path. Use `sudo python setup.py develop` if you want to stay up to date with the cutting edge and still be able to svn/bzr up every now and then. 33 | 34 | By default PodSixNet uses a binary encoder to transfer data over the network, but it can optionally use the [JSON](http://json.org/) format or other formats supported by a serialiser which has 'dumps' and 'loads' methods. If you want to serialise your data using JSON you can change the first line of Channel.py to 'from simplejson import dumps, loads' or use the built-in json library in Python 2.6 or higher. This will allow you to write game clients in languages that can't read the 'rencode' binary format, such as Javascript. 35 | 36 | Examples 37 | -------- 38 | 39 | Chat example: 40 | 41 | * `python examples/ChatServer.py` 42 | * and a couple of instances of `python examples/ChatClient.py` 43 | 44 | Whiteboard example: 45 | 46 | * `python examples/WhiteboardServer.py` 47 | * and a couple of instances of `python examples/WhiteboardClient.py` 48 | 49 | LagTime example (measures round-trip time from the server to the client): 50 | 51 | * `python examples/LagTimeServer.py` 52 | * and a couple of instances of `python examples/LagTimeClient.py` 53 | 54 | Quick start - Server 55 | -------------------- 56 | 57 | You will need to subclass two classes in order to make your own server. Each time a client connects, a new Channel based class will be created, so you should subclass Channel to make your own server-representation-of-a-client class like this: 58 | 59 | ```python 60 | from PodSixNet.Channel import Channel 61 | 62 | class ClientChannel(Channel): 63 | 64 | def Network(self, data): 65 | print data 66 | 67 | def Network_myaction(self, data): 68 | print "myaction:", data 69 | ``` 70 | 71 | Whenever the client does `connection.Send(mydata)`, the `Network()` method will be called. The method `Network_myaction()` will only be called if your data has a key called 'action' with a value of "myaction". In other words if it looks something like this: 72 | 73 | ```python 74 | data = {"action": "myaction", "blah": 123, ... } 75 | ``` 76 | 77 | Next you need to subclass the Server class like this: 78 | 79 | ```python 80 | from PodSixNet.Server import Server 81 | 82 | class MyServer(Server): 83 | 84 | channelClass = ClientChannel 85 | 86 | def Connected(self, channel, addr): 87 | print 'new connection:', channel 88 | ``` 89 | 90 | Set `channelClass` to the channel class that you created above. The method `Connected()` will be called whenever a new client connects to your server. See the example servers for an idea of what you might do each time a client connects. You need to call `Server.Pump()` every now and then, probably once per game loop. For example: 91 | 92 | ```python 93 | myserver = MyServer() 94 | while True: 95 | myserver.Pump() 96 | sleep(0.0001) 97 | ``` 98 | 99 | When you want to send data to a specific client/channel, use the Send method of the Channel class: 100 | 101 | ```python 102 | channel.Send({"action": "hello", "message": "hello client!"}) 103 | ``` 104 | 105 | Quick start - Client 106 | -------------------- 107 | 108 | To have a client connect to your new server, you should use the Connection module. See `pydoc Connection` for more details, but here's a summary: 109 | 110 | `Connection.connection` is a singleton Channel which connects to the server. You'll only have one of these in your game code, and you'll use it to connect to the server and send messages to the server. 111 | 112 | ```python 113 | from PodSixNet.Connection import connection 114 | 115 | # connect to the server - optionally pass hostname and port like: ("mccormick.cx", 31425) 116 | connection.Connect() 117 | 118 | connection.Send({"action": "myaction", "blah": 123, "things": [3, 4, 3, 4, 7]}) 119 | ``` 120 | 121 | You'll also need to put the following code once somewhere in your game loop: 122 | 123 | ```python 124 | connection.Pump() 125 | ``` 126 | 127 | Any time you have an object in your game which you want to receive messages from the server, subclass `ConnectionListener`. For example: 128 | 129 | ```python 130 | from PodSixNet.Connection import ConnectionListener 131 | 132 | class MyNetworkListener(ConnectionListener): 133 | 134 | def Network(self, data): 135 | print 'network data:', data 136 | 137 | def Network_connected(self, data): 138 | print "connected to the server" 139 | 140 | def Network_error(self, data): 141 | print "error:", data['error'][1] 142 | 143 | def Network_disconnected(self, data): 144 | print "disconnected from the server" 145 | 146 | def Network_myaction(data): 147 | print "myaction:", data 148 | ``` 149 | 150 | Just like in the server case, the network events are received by `Network_*` callback methods, where you should replace '*' with the value in the 'action' key you want to catch. You can implement as many or as few of the above as you like. For example, NetworkGUI would probably only want to listen for the `_connected`, `_disconnected`, and `_error` network events. The data for `_error` always comes in the form of network exceptions, like (111, 'Connection refused') - these are passed straight from the socket layer and are standard socket errors. 151 | 152 | Another class might implement custom methods like `Network_myaction()`, which will receive any data that gets sent from the server with an 'action' key that has the name 'myaction'. For example, the server might send a message with the number of players currently connected like so: 153 | 154 | ```python 155 | channel.Send({"action": "numplayers", "players": 10}) 156 | ``` 157 | 158 | And the listener would look like this: 159 | 160 | ```python 161 | from PodSixNet.Connection import ConnectionListener 162 | 163 | class MyPlayerListener(ConnectionListener): 164 | 165 | def Network_numplayers(data): 166 | # update gui element displaying the number of currently connected players 167 | print data['players'] 168 | ``` 169 | 170 | You can subclass `ConnectionListener` as many times as you like in your application, and every class you make which subclasses it will receive the network events via named Network callbacks. You should call the `Pump()` method on each object you instantiate once per game loop: 171 | 172 | ```python 173 | gui = MyPlayerListener() 174 | while 1: 175 | connection.Pump() 176 | gui.Pump() 177 | ``` 178 | 179 | License 180 | ------- 181 | 182 | Copyright [Chris McCormick](http://mccormick.cx/), 2009-2015. 183 | 184 | PodSixNet is licensed under the terms of the LGPL v3.0 or higher. See the file called [COPYING](COPYING) for details. 185 | 186 | This basically means that you can use it in most types of projects (commercial or otherwise), but if you make changes to the PodSixNet code you must make the modified code available with the distribution of your software. Hopefully you'll tell us about it so we can incorporate your changes. I am not a lawyer, so please read the license carefully to understand your rights with respect to this code. 187 | 188 | Why not use Twisted instead? 189 | --------------------------- 190 | 191 | Twisted is a fantastic library for writing robust network code. I have used it in several projects in the past, and it was quite nice to work with. That said, Twisted: 192 | 193 | * wants to steal the mainloop 194 | * is bloated not KISS (it implements many many different protocols) 195 | * has a weird template launching language when Python should do just fine 196 | * is not written 100% for the specfic use-case of multiplayer games 197 | 198 | These are some of the reasons why I decided to write a library that is lightweight, has no dependencies except Python, and is dedicated 100% to the task of multiplayer game networking. 199 | 200 | -------------------------------------------------------------------------------- /examples/ChatClient.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | from time import sleep 5 | from sys import stdin, exit 6 | 7 | from PodSixNet.Connection import connection, ConnectionListener 8 | 9 | # This example uses Python threads to manage async input from sys.stdin. 10 | # This is so that I can receive input from the console whilst running the server. 11 | # Don't ever do this - it's slow and ugly. (I'm doing it for simplicity's sake) 12 | from _thread import * 13 | 14 | class Client(ConnectionListener): 15 | def __init__(self, host, port): 16 | self.Connect((host, port)) 17 | print("Chat client started") 18 | print("Ctrl-C to exit") 19 | # get a nickname from the user before starting 20 | print("Enter your nickname: ") 21 | connection.Send({"action": "nickname", "nickname": stdin.readline().rstrip("\n")}) 22 | # launch our threaded input loop 23 | t = start_new_thread(self.InputLoop, ()) 24 | 25 | def Loop(self): 26 | connection.Pump() 27 | self.Pump() 28 | 29 | def InputLoop(self): 30 | # horrid threaded input loop 31 | # continually reads from stdin and sends whatever is typed to the server 32 | while 1: 33 | connection.Send({"action": "message", "message": stdin.readline().rstrip("\n")}) 34 | 35 | ####################################### 36 | ### Network event/message callbacks ### 37 | ####################################### 38 | 39 | def Network_players(self, data): 40 | print("*** players: " + ", ".join([p for p in data['players']])) 41 | 42 | def Network_message(self, data): 43 | print(data['who'] + ": " + data['message']) 44 | 45 | # built in stuff 46 | 47 | def Network_connected(self, data): 48 | print("You are now connected to the server") 49 | 50 | def Network_error(self, data): 51 | print('error:', data['error'][1]) 52 | connection.Close() 53 | 54 | def Network_disconnected(self, data): 55 | print('Server disconnected') 56 | exit() 57 | 58 | if __name__ == '__main__': 59 | if len(sys.argv) != 2: 60 | print("Usage:", sys.argv[0], "host:port") 61 | print("e.g.", sys.argv[0], "localhost:31425") 62 | else: 63 | host, port = sys.argv[1].split(":") 64 | c = Client(host, int(port)) 65 | while 1: 66 | c.Loop() 67 | sleep(0.001) 68 | -------------------------------------------------------------------------------- /examples/ChatServer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | from time import sleep, localtime 5 | from weakref import WeakKeyDictionary 6 | 7 | from PodSixNet.Server import Server 8 | from PodSixNet.Channel import Channel 9 | 10 | class ClientChannel(Channel): 11 | """ 12 | This is the server representation of a single connected client. 13 | """ 14 | def __init__(self, *args, **kwargs): 15 | self.nickname = "anonymous" 16 | Channel.__init__(self, *args, **kwargs) 17 | 18 | def Close(self): 19 | self._server.DelPlayer(self) 20 | 21 | ################################## 22 | ### Network specific callbacks ### 23 | ################################## 24 | 25 | def Network_message(self, data): 26 | self._server.SendToAll({"action": "message", "message": data['message'], "who": self.nickname}) 27 | 28 | def Network_nickname(self, data): 29 | self.nickname = data['nickname'] 30 | self._server.SendPlayers() 31 | 32 | class ChatServer(Server): 33 | channelClass = ClientChannel 34 | 35 | def __init__(self, *args, **kwargs): 36 | Server.__init__(self, *args, **kwargs) 37 | self.players = WeakKeyDictionary() 38 | print('Server launched') 39 | 40 | def Connected(self, channel, addr): 41 | self.AddPlayer(channel) 42 | 43 | def AddPlayer(self, player): 44 | print("New Player" + str(player.addr)) 45 | self.players[player] = True 46 | self.SendPlayers() 47 | print("players", [p for p in self.players]) 48 | 49 | def DelPlayer(self, player): 50 | print("Deleting Player" + str(player.addr)) 51 | del self.players[player] 52 | self.SendPlayers() 53 | 54 | def SendPlayers(self): 55 | self.SendToAll({"action": "players", "players": [p.nickname for p in self.players]}) 56 | 57 | def SendToAll(self, data): 58 | [p.Send(data) for p in self.players] 59 | 60 | def Launch(self): 61 | while True: 62 | self.Pump() 63 | sleep(0.0001) 64 | 65 | # get command line argument of server, port 66 | if __name__ == '__main__': 67 | if len(sys.argv) != 2: 68 | print("Usage:", sys.argv[0], "host:port") 69 | print("e.g.", sys.argv[0], "localhost:31425") 70 | else: 71 | host, port = sys.argv[1].split(":") 72 | s = ChatServer(localaddr=(host, int(port))) 73 | s.Launch() 74 | 75 | -------------------------------------------------------------------------------- /examples/LagTimeClient.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | from time import sleep 5 | from sys import stdin, exit 6 | 7 | from PodSixNet.Connection import connection, ConnectionListener 8 | 9 | class LagTimeClient(ConnectionListener): 10 | """ This example client connects to a LagTimeServer which then sends pings back. 11 | This client responds to 10 pings, and the server measures the round-trip time of each ping and outputs it.""" 12 | def __init__(self, host, port): 13 | self.Connect((host, port)) 14 | print("LagTimeClient started") 15 | 16 | ####################################### 17 | ### Network event/message callbacks ### 18 | ####################################### 19 | 20 | def Network_ping(self, data): 21 | print("got:", data) 22 | if data["count"] == 10: 23 | connection.Close() 24 | else: 25 | connection.Send(data) 26 | 27 | # built in stuff 28 | 29 | def Network_connected(self, data): 30 | print("Connected to the server") 31 | 32 | def Network_error(self, data): 33 | print('error:', data['error'][1]) 34 | connection.Close() 35 | 36 | def Network_disconnected(self, data): 37 | print('Server disconnected') 38 | exit() 39 | 40 | if __name__ == __main__: 41 | if len(sys.argv) != 2: 42 | print("Usage:", sys.argv[0], "host:port") 43 | print("e.g.", sys.argv[0], "localhost:31425") 44 | else: 45 | host, port = sys.argv[1].split(":") 46 | c = LagTimeClient(host, int(port)) 47 | while 1: 48 | connection.Pump() 49 | c.Pump() 50 | sleep(0.001) 51 | -------------------------------------------------------------------------------- /examples/LagTimeServer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from time import sleep, localtime 4 | from weakref import WeakKeyDictionary 5 | from time import time 6 | import sys 7 | 8 | from PodSixNet.Server import Server 9 | from PodSixNet.Channel import Channel 10 | 11 | class LagTimeChannel(Channel): 12 | """ 13 | This is the server representation of a single connected client. 14 | """ 15 | def __init__(self, *args, **kwargs): 16 | Channel.__init__(self, *args, **kwargs) 17 | self.count = 0 18 | self.times = [] 19 | 20 | def Close(self): 21 | print(self, 'Client disconnected') 22 | 23 | ################################## 24 | ### Network specific callbacks ### 25 | ################################## 26 | 27 | def Network_ping(self, data): 28 | print(self, "ping %d round trip time was %f" % (data["count"], time() - self.times[data["count"]])) 29 | self.Ping() 30 | 31 | def Ping(self): 32 | print(self, "Ping:", self.count) 33 | self.times.append(time()) 34 | self.Send({"action": "ping", "count": self.count}) 35 | self.count += 1 36 | 37 | class LagTimeServer(Server): 38 | channelClass = LagTimeChannel 39 | 40 | def __init__(self, *args, **kwargs): 41 | Server.__init__(self, *args, **kwargs) 42 | print('Server launched') 43 | 44 | def Connected(self, channel, addr): 45 | print(channel, "Channel connected") 46 | channel.Ping() 47 | 48 | def Launch(self): 49 | while True: 50 | self.Pump() 51 | sleep(0.0001) 52 | 53 | # get command line argument of server, port 54 | if __name__ == '__main__': 55 | if len(sys.argv) != 2: 56 | print("Usage:", sys.argv[0], "host:port") 57 | print("e.g.", sys.argv[0], "localhost:31425") 58 | else: 59 | host, port = sys.argv[1].split(":") 60 | s = LagTimeServer(localaddr=(host, int(port))) 61 | s.Launch() 62 | 63 | -------------------------------------------------------------------------------- /examples/Whiteboard.py: -------------------------------------------------------------------------------- 1 | from sys import exit 2 | from os import environ 3 | import pygame 4 | 5 | SCREENSIZE = (640, 480) 6 | 7 | environ['SDL_VIDEO_CENTERED'] = '1' 8 | pygame.init() 9 | screen = pygame.display.set_mode(SCREENSIZE) 10 | 11 | pygame.font.init() 12 | fnt = pygame.font.SysFont("Arial", 14) 13 | txtpos = (100, 90) 14 | 15 | class Whiteboard: 16 | def __init__(self): 17 | self.statusLabel = "connecting" 18 | self.playersLabel = "0 players" 19 | self.frame = 0 20 | self.down = False 21 | 22 | def Events(self): 23 | for event in pygame.event.get(): 24 | if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == 27): 25 | exit() 26 | 27 | if event.type == pygame.MOUSEBUTTONDOWN: 28 | self.down = True 29 | self.PenDown(event) 30 | 31 | if event.type == pygame.MOUSEMOTION and self.down: 32 | self.PenMove(event) 33 | 34 | if event.type == pygame.MOUSEBUTTONUP: 35 | self.down = False 36 | self.PenUp(event) 37 | 38 | def Draw(self, linesets): 39 | screen.fill([255, 255, 255]) 40 | txt = fnt.render(self.statusLabel, 1, (0, 0, 0)) 41 | screen.blit(fnt.render(self.statusLabel, 1, (0, 0, 0)), [10, 10]) 42 | txt = fnt.render(self.playersLabel, 1, (0, 0, 0)) 43 | screen.blit(fnt.render(self.playersLabel, 1, (0, 0, 0)), [10, 20]) 44 | [[pygame.draw.aalines(screen, c, False, l) for l in lines if len(l) > 1] for c, lines in linesets] 45 | pygame.display.flip() 46 | self.frame += 1 47 | -------------------------------------------------------------------------------- /examples/WhiteboardClient.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | from time import sleep 5 | 6 | from PodSixNet.Connection import connection, ConnectionListener 7 | from Whiteboard import Whiteboard 8 | 9 | class Client(ConnectionListener, Whiteboard): 10 | def __init__(self, host, port): 11 | self.Connect((host, port)) 12 | self.players = {} 13 | Whiteboard.__init__(self) 14 | 15 | def Loop(self): 16 | self.Pump() 17 | connection.Pump() 18 | self.Events() 19 | self.Draw([(self.players[p]['color'], self.players[p]['lines']) for p in self.players]) 20 | 21 | if "connecting" in self.statusLabel: 22 | self.statusLabel = "connecting" + "".join(["." for s in range(int(self.frame / 30) % 4)]) 23 | 24 | ####################### 25 | ### Event callbacks ### 26 | ####################### 27 | #def PenDraw(self, e): 28 | # connection.Send({"action": "draw", "point": e.pos}) 29 | 30 | def PenDown(self, e): 31 | connection.Send({"action": "startline", "point": e.pos}) 32 | 33 | def PenMove(self, e): 34 | connection.Send({"action": "drawpoint", "point": e.pos}) 35 | 36 | def PenUp(self, e): 37 | connection.Send({"action": "drawpoint", "point": e.pos}) 38 | 39 | ############################### 40 | ### Network event callbacks ### 41 | ############################### 42 | 43 | def Network_initial(self, data): 44 | self.players = data['lines'] 45 | 46 | def Network_drawpoint(self, data): 47 | self.players[data['id']]['lines'][-1].append(data['point']) 48 | 49 | def Network_startline(self, data): 50 | self.players[data['id']]['lines'].append([data['point']]) 51 | 52 | def Network_players(self, data): 53 | self.playersLabel = str(len(data['players'])) + " players" 54 | mark = [] 55 | 56 | for i in data['players']: 57 | if not i in self.players: 58 | self.players[i] = {'color': data['players'][i], 'lines': []} 59 | 60 | for i in self.players: 61 | if not i in data['players'].keys(): 62 | mark.append(i) 63 | 64 | for m in mark: 65 | del self.players[m] 66 | 67 | def Network(self, data): 68 | #print('network:', data) 69 | pass 70 | 71 | def Network_connected(self, data): 72 | self.statusLabel = "connected" 73 | 74 | def Network_error(self, data): 75 | print(data) 76 | import traceback 77 | traceback.print_exc() 78 | self.statusLabel = data['error'][1] 79 | connection.Close() 80 | 81 | def Network_disconnected(self, data): 82 | self.statusLabel += " - disconnected" 83 | 84 | if __name__ == '__main__': 85 | if len(sys.argv) != 2: 86 | print("Usage:", sys.argv[0], "host:port") 87 | print("e.g.", sys.argv[0], "localhost:31425") 88 | else: 89 | host, port = sys.argv[1].split(":") 90 | c = Client(host, int(port)) 91 | while 1: 92 | c.Loop() 93 | sleep(0.001) 94 | 95 | -------------------------------------------------------------------------------- /examples/WhiteboardServer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | from time import sleep, localtime 5 | from random import randint 6 | from weakref import WeakKeyDictionary 7 | 8 | from PodSixNet.Server import Server 9 | from PodSixNet.Channel import Channel 10 | 11 | class ServerChannel(Channel): 12 | """ 13 | This is the server representation of a single connected client. 14 | """ 15 | def __init__(self, *args, **kwargs): 16 | Channel.__init__(self, *args, **kwargs) 17 | self.id = str(self._server.NextId()) 18 | intid = int(self.id) 19 | self.color = [(intid + 1) % 3 * 84, (intid + 2) % 3 * 84, (intid + 3) % 3 * 84] #tuple([randint(0, 127) for r in range(3)]) 20 | self.lines = [] 21 | 22 | def PassOn(self, data): 23 | # pass on what we received to all connected clients 24 | data.update({"id": self.id}) 25 | self._server.SendToAll(data) 26 | 27 | def Close(self): 28 | self._server.DelPlayer(self) 29 | 30 | ################################## 31 | ### Network specific callbacks ### 32 | ################################## 33 | 34 | def Network_startline(self, data): 35 | self.lines.append([data['point']]) 36 | self.PassOn(data) 37 | 38 | def Network_drawpoint(self, data): 39 | self.lines[-1].append(data['point']) 40 | self.PassOn(data) 41 | 42 | class WhiteboardServer(Server): 43 | channelClass = ServerChannel 44 | 45 | def __init__(self, *args, **kwargs): 46 | self.id = 0 47 | Server.__init__(self, *args, **kwargs) 48 | self.players = WeakKeyDictionary() 49 | print('Server launched') 50 | 51 | def NextId(self): 52 | self.id += 1 53 | return self.id 54 | 55 | def Connected(self, channel, addr): 56 | self.AddPlayer(channel) 57 | 58 | def AddPlayer(self, player): 59 | print("New Player" + str(player.addr)) 60 | self.players[player] = True 61 | player.Send({"action": "initial", "lines": dict([(p.id, {"color": p.color, "lines": p.lines}) for p in self.players])}) 62 | self.SendPlayers() 63 | 64 | def DelPlayer(self, player): 65 | print("Deleting Player" + str(player.addr)) 66 | del self.players[player] 67 | self.SendPlayers() 68 | 69 | def SendPlayers(self): 70 | self.SendToAll({"action": "players", "players": dict([(p.id, p.color) for p in self.players])}) 71 | 72 | def SendToAll(self, data): 73 | [p.Send(data) for p in self.players] 74 | 75 | def Launch(self): 76 | while True: 77 | self.Pump() 78 | sleep(0.0001) 79 | 80 | # get command line argument of server, port 81 | if __name__ == '__main__': 82 | if len(sys.argv) != 2: 83 | print("Usage:", sys.argv[0], "host:port") 84 | print("e.g.", sys.argv[0], "localhost:31425") 85 | else: 86 | host, port = sys.argv[1].split(":") 87 | s = WhiteboardServer(localaddr=(host, int(port))) 88 | s.Launch() 89 | 90 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "__version__='`git describe --tag --always`'" > PodSixNet/version.py 4 | python setup.py sdist 5 | twine upload dist/* 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from distutils.core import setup 5 | import subprocess 6 | 7 | # convert readme to rst format 8 | try: 9 | import pypandoc 10 | long_description = pypandoc.convert('README.md', 'rst') 11 | except: 12 | long_description = '' 13 | 14 | versionfile = "PodSixNet/version.py" 15 | if not os.path.isfile(versionfile): 16 | # assume git checkout 17 | __version__ = str(subprocess.check_output(["git", "describe", "--tag", "--always"])).strip("\n") 18 | else: 19 | # created by pip 20 | exec(open(versionfile).read()) 21 | 22 | setup( 23 | version=__version__, 24 | name='PodSixNet', 25 | description='Multiplayer networking library for games', 26 | long_description=long_description, 27 | author='Chris McCormick', 28 | author_email='chris@mccormick.cx', 29 | url='https://github.com/chr15m/PodSixNet', 30 | packages=['PodSixNet'], 31 | ) 32 | --------------------------------------------------------------------------------