Converted to Twisted from asyncore. Not positive if this is just my local machine, but it seems like this backend is a bit faster.

This commit is contained in:
Greg Taylor 2007-05-21 20:52:05 +00:00
parent 82f46a2b69
commit 97cf1213e6
8 changed files with 113 additions and 145 deletions

View file

@ -1,6 +1,7 @@
Requirements Requirements
------------ ------------
* Python 2.5 strongly recommended, although 2.3 or 2.4 may work just fine. * Python 2.5 strongly recommended, although 2.3 or 2.4 may work just fine.
* Twisted -- http://twistedmatrix.com/
* PySqlite2 (If you're using the default SQLite driver) * PySqlite2 (If you're using the default SQLite driver)
* Django (Latest trunk from Subversion recommended) * Django (Latest trunk from Subversion recommended)
* Optional: Apache2 or equivalent webserver with a Python interpreter * Optional: Apache2 or equivalent webserver with a Python interpreter

View file

@ -1,17 +1,21 @@
import time
from twisted.internet import protocol, reactor, defer
import session_mgr import session_mgr
""" """
Holds the events scheduled in scheduler.py. Holds the events scheduled in scheduler.py.
""" """
# Dictionary of events with a list in the form of: [<interval>, <lastrantime>]
schedule = { schedule = {
'check_sessions': 60, 'check_sessions': [60, None]
} }
lastrun = {}
def check_sessions(): def check_sessions():
""" """
Check all of the connected sessions. Event: Check all of the connected sessions.
""" """
session_mgr.check_all_sessions() session_mgr.check_all_sessions()
schedule['check_sessions'][1] = time.time()
reactor.callLater(schedule['check_sessions'][0], check_sessions)

View file

@ -108,7 +108,7 @@ def announce_all(message, with_ann_prefix=True, with_nl=True):
newline = '' newline = ''
for session in session_mgr.get_session_list(): for session in session_mgr.get_session_list():
session.msg_no_nl('%s %s%s' % (prefix, message,newline,)) session.msg('%s %s%s' % (prefix, message,newline,))
def word_wrap(text, width=78): def word_wrap(text, width=78):
""" """

View file

@ -1,5 +1,5 @@
import time
import events import events
from twisted.internet import protocol, reactor, defer
""" """
A really simple scheduler. We can probably get a lot fancier with this A really simple scheduler. We can probably get a lot fancier with this
in the future, but it'll do for now. in the future, but it'll do for now.
@ -11,24 +11,12 @@ ADDING AN EVENT:
""" """
# The timer method to be triggered by the main server loop. # The timer method to be triggered by the main server loop.
def heartbeat(): def start_events():
""" """
Handle one tic/heartbeat. Handle one tic/heartbeat.
""" """
tictime = time.time()
for event in events.schedule: for event in events.schedule:
try: event_func = getattr(events, event)
events.lastrun[event]
except:
events.lastrun[event] = time.time()
diff = tictime - events.lastrun[event]
if diff >= events.schedule[event]: if callable(event_func):
event_func = getattr(events, event) reactor.callLater(events.schedule[event][0], event_func)
if callable(event_func):
event_func()
# We'll get a new reading for time for accuracy.
events.lastrun[event] = time.time()

View file

@ -1,12 +1,15 @@
from traceback import format_exc from traceback import format_exc
from asyncore import dispatcher import time
from asynchat import async_chat import sys
import socket, asyncore, time
from twisted.application import internet, service
from twisted.internet import protocol, reactor, defer
from django.db import models from django.db import models
from django.db import connection from django.db import connection
from apps.config.models import CommandAlias from apps.config.models import CommandAlias
import sys from session import SessionProtocol
import scheduler import scheduler
import functions_general import functions_general
import session_mgr import session_mgr
@ -15,22 +18,20 @@ import settings
import cmdtable import cmdtable
import initial_setup import initial_setup
class Server(dispatcher): class EvenniaService(service.Service):
"""
The main server class from which everything branches. def __init__(self, filename="blah"):
"""
def __init__(self):
self.cmd_alias_list = {} self.cmd_alias_list = {}
self.game_running = True self.game_running = True
# Database-specific startup optimizations. # Database-specific startup optimizations.
if settings.DATABASE_ENGINE == "sqlite3": if settings.DATABASE_ENGINE == "sqlite3":
self.sqlite3_prep() self.sqlite3_prep()
# Wipe our temporary flags on all of the objects. # Wipe our temporary flags on all of the objects.
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("UPDATE objects_object SET nosave_flags=''") cursor.execute("UPDATE objects_object SET nosave_flags=''")
print '-'*50 print '-'*50
# Load command aliases into memory for easy/quick access. # Load command aliases into memory for easy/quick access.
self.load_cmd_aliases() self.load_cmd_aliases()
@ -40,21 +41,15 @@ class Server(dispatcher):
print ' Game started for the first time, setting defaults.' print ' Game started for the first time, setting defaults.'
initial_setup.handle_setup() initial_setup.handle_setup()
# Start accepting connections.
dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind(('', int(self.port)))
self.listen(100)
self.start_time = time.time() self.start_time = time.time()
print ' %s started on port %s.' % (gameconf.get_configvalue('site_name'), self.port,) print ' %s started on port %s.' % (gameconf.get_configvalue('site_name'), self.port,)
print '-'*50 print '-'*50
scheduler.start_events()
""" """
BEGIN SERVER STARTUP METHODS BEGIN SERVER STARTUP METHODS
""" """
def load_cmd_aliases(self): def load_cmd_aliases(self):
""" """
Load up our command aliases. Load up our command aliases.
@ -63,17 +58,8 @@ class Server(dispatcher):
for alias in alias_list: for alias in alias_list:
self.cmd_alias_list[alias.user_input] = alias.equiv_command self.cmd_alias_list[alias.user_input] = alias.equiv_command
print ' Command Aliases Loaded: %i' % (len(self.cmd_alias_list),) print ' Command Aliases Loaded: %i' % (len(self.cmd_alias_list),)
pass
def handle_accept(self):
"""
What to do when we get a connection.
"""
conn, addr = self.accept()
session = session_mgr.new_session(self, conn, addr)
session.game_connect_screen(session)
print 'Connection:', str(session)
print 'Sessions active:', len(session_mgr.get_session_list())
def sqlite3_prep(self): def sqlite3_prep(self):
""" """
Optimize some SQLite stuff at startup since we can't save it to the Optimize some SQLite stuff at startup since we can't save it to the
@ -84,14 +70,14 @@ class Server(dispatcher):
cursor.execute("PRAGMA synchronous=OFF") cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF") cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2") cursor.execute("PRAGMA temp_store=2")
""" """
BEGIN GENERAL METHODS BEGIN GENERAL METHODS
""" """
def shutdown(self, message='The server has been shutdown. Please check back soon.'): def shutdown(self, message='The server has been shutdown. Please check back soon.'):
functions_general.announce_all(message) functions_general.announce_all(message)
self.game_running = False session_mgr.disconnect_all_sessions()
reactor.callLater(0, reactor.stop)
def command_list(self): def command_list(self):
""" """
@ -112,32 +98,26 @@ class Server(dispatcher):
'events', 'functions_db', 'functions_general', 'functions_comsys', 'events', 'functions_db', 'functions_general', 'functions_comsys',
'functions_help', 'gameconf', 'session', 'apps.objects.models', 'functions_help', 'gameconf', 'session', 'apps.objects.models',
'apps.helpsys.models', 'apps.config.models'] 'apps.helpsys.models', 'apps.config.models']
for mod in reload_list: for mod in reload_list:
reload(sys.modules[mod]) reload(sys.modules[mod])
session.msg("Modules reloaded.") session.msg("Modules reloaded.")
functions_general.log_infomsg("Modules reloaded by %s." % (session,)) functions_general.log_infomsg("Modules reloaded by %s." % (session,))
def getEvenniaServiceFactory(self):
f = protocol.ServerFactory()
f.protocol = SessionProtocol
f.server = self
return f
""" """
END Server CLASS END Server CLASS
""" """
""" application = service.Application('Evennia')
BEGIN MAIN APPLICATION LOGIC mud_service = EvenniaService('Evennia Server')
"""
if __name__ == '__main__':
server = Server()
try:
while server.game_running:
asyncore.loop(timeout=5, count=1)
scheduler.heartbeat()
except KeyboardInterrupt:
server.shutdown()
print '--> Server killed by keystroke.'
except: # Sheet sheet, fire ze missiles!
server.shutdown(message="The server has encountered a fatal error and has been shut down. Please check back soon.") serviceCollection = service.IServiceCollection(application)
functions_general.log_errmsg("Untrapped error: %s" % internet.TCPServer(4000, mud_service.getEvenniaServiceFactory()).setServiceParent(serviceCollection)
(format_exc()))

View file

@ -1,7 +1,8 @@
from asyncore import dispatcher import time, sys
from asynchat import async_chat
import socket, asyncore, time, sys
import cPickle as pickle import cPickle as pickle
from twisted.conch.telnet import StatefulTelnetProtocol
import cmdhandler import cmdhandler
from apps.objects.models import Object from apps.objects.models import Object
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -10,20 +11,34 @@ import functions_db
import functions_general import functions_general
import session_mgr import session_mgr
class PlayerSession(async_chat): class SessionProtocol(StatefulTelnetProtocol):
""" """
This class represents a player's sesssion. From here we branch down into This class represents a player's sesssion. From here we branch down into
other various classes, please try to keep this one tidy! other various classes, please try to keep this one tidy!
""" """
def __init__(self, server, sock, addr):
async_chat.__init__(self, sock) def connectionMade(self):
self.server = server """
self.address = addr What to do when we get a connection.
self.set_terminator("\n") """
session_mgr.add_session(self)
self.game_connect_screen()
self.prep_session()
print 'Connection:', self
print 'Sessions active:', len(session_mgr.get_session_list())
def getClientAddress(self):
"""
Returns the client's address and port in a tuple. For example
('127.0.0.1', 41917)
"""
return self.transport.client
def prep_session(self):
#self.server = server
self.address = self.getClientAddress()
self.name = None self.name = None
self.data = []
self.uid = None self.uid = None
self.sock = sock
self.logged_in = False self.logged_in = False
# The time the user last issued a command. # The time the user last issued a command.
self.cmd_last = time.time() self.cmd_last = time.time()
@ -35,6 +50,18 @@ class PlayerSession(async_chat):
self.conn_time = time.time() self.conn_time = time.time()
self.channels_subscribed = {} self.channels_subscribed = {}
def disconnectClient(self):
"""
Manually disconnect the client.
"""
self.transport.loseConnection()
def connectionLost(self, reason):
"""
Execute this when a client abruplty loses their connection.
"""
print "DISCONNECT:", reason.getErrorMessage()
def has_user_channel(self, cname, alias_search=False, return_muted=False): def has_user_channel(self, cname, alias_search=False, return_muted=False):
""" """
Is this session subscribed to the named channel? Is this session subscribed to the named channel?
@ -93,24 +120,17 @@ class PlayerSession(async_chat):
if chan_list: if chan_list:
self.channels_subscribed = pickle.loads(chan_list) self.channels_subscribed = pickle.loads(chan_list)
def collect_incoming_data(self, data): def lineReceived(self, data):
"""
Stuff any incoming data into our buffer, self.data
"""
self.data.append(data)
def found_terminator(self):
""" """
Any line return indicates a command for the purpose of a MUD. So we take Any line return indicates a command for the purpose of a MUD. So we take
the user input and pass it to our command handler. the user input and pass it to our command handler.
""" """
line = (''.join(self.data)) line = (''.join(data))
line = line.strip('\r') line = line.strip('\r')
uinput = line uinput = line
self.data = []
# Stuff anything we need to pass in this dictionary. # Stuff anything we need to pass in this dictionary.
cdat = {"server": self.server, "uinput": uinput, "session": self} cdat = {"server": self.factory.server, "uinput": uinput, "session": self}
cmdhandler.handle(cdat) cmdhandler.handle(cdat)
def handle_close(self): def handle_close(self):
@ -122,7 +142,7 @@ class PlayerSession(async_chat):
pobject.set_flag("CONNECTED", False) pobject.set_flag("CONNECTED", False)
pobject.get_location().emit_to_contents("%s has disconnected." % (pobject.get_name(show_dbref=False),), exclude=pobject) pobject.get_location().emit_to_contents("%s has disconnected." % (pobject.get_name(show_dbref=False),), exclude=pobject)
async_chat.handle_close(self) self.disconnectClient()
self.logged_in = False self.logged_in = False
session_mgr.remove_session(self) session_mgr.remove_session(self)
print 'Sessions active:', len(session_mgr.get_session_list()) print 'Sessions active:', len(session_mgr.get_session_list())
@ -137,7 +157,7 @@ class PlayerSession(async_chat):
except: except:
return False return False
def game_connect_screen(self, session): def game_connect_screen(self):
""" """
Show the banner screen. Show the banner screen.
""" """
@ -148,7 +168,7 @@ class PlayerSession(async_chat):
connect <email> <password>\n\r connect <email> <password>\n\r
create \"<username>\" <email> <password>\n\r""" create \"<username>\" <email> <password>\n\r"""
buffer += '-'*50 buffer += '-'*50
session.msg(buffer) self.msg(buffer)
def login(self, user): def login(self, user):
""" """
@ -163,27 +183,19 @@ class PlayerSession(async_chat):
self.msg("You are now logged in as %s." % (self.name,)) self.msg("You are now logged in as %s." % (self.name,))
pobject.get_location().emit_to_contents("%s has connected." % (pobject.get_name(),), exclude=pobject) pobject.get_location().emit_to_contents("%s has connected." % (pobject.get_name(),), exclude=pobject)
cdat = {"session": self, "uinput":'look', "server": self.server} cdat = {"session": self, "uinput":'look', "server": self.factory.server}
cmdhandler.handle(cdat) cmdhandler.handle(cdat)
functions_general.log_infomsg("Login: %s" % (self,)) functions_general.log_infomsg("Login: %s" % (self,))
pobject.set_attribute("Last", "%s" % (time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),)) pobject.set_attribute("Last", "%s" % (time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),))
pobject.set_attribute("Lastsite", "%s" % (self.address[0],)) pobject.set_attribute("Lastsite", "%s" % (self.address,))
self.load_user_channels() self.load_user_channels()
def msg(self, message): def msg(self, message):
""" """
Sends a message with the newline/return included. Use this instead of Sends a message to the session.
directly calling push().
""" """
self.push("%s\n\r" % (message,)) self.sendLine("%s" % (message,))
def msg_no_nl(self, message):
"""
Sends a message without the newline/return included. Use this instead of
directly calling push().
"""
self.push("%s" % (message,))
def __str__(self): def __str__(self):
""" """
String representation of the user session class. We use String representation of the user session class. We use
@ -194,6 +206,3 @@ class PlayerSession(async_chat):
else: else:
symbol = '?' symbol = '?'
return "<%s> %s@%s" % (symbol, self.name, self.address,) return "<%s> %s@%s" % (symbol, self.name, self.address,)
# def handle_error(self):
# self.handle_close()

View file

@ -1,5 +1,4 @@
import time import time
from session import PlayerSession
import gameconf import gameconf
""" """
@ -8,13 +7,12 @@ Session manager, handles connected players.
# Our list of connected sessions. # Our list of connected sessions.
session_list = [] session_list = []
def new_session(server, conn, addr): def add_session(session):
""" """
Create and return a new session. Adds a session to the session list.
""" """
session = PlayerSession(server, conn, addr)
session_list.insert(0, session) session_list.insert(0, session)
return session print 'Sessions active:', len(get_session_list())
def get_session_list(): def get_session_list():
""" """
@ -22,6 +20,13 @@ def get_session_list():
""" """
return session_list return session_list
def disconnect_all_sessions():
"""
Cleanly disconnect all of the connected sessions.
"""
for sess in get_session_list():
sess.handle_close()
def check_all_sessions(): def check_all_sessions():
""" """
Check all currently connected sessions and see if any are dead. Check all currently connected sessions and see if any are dead.
@ -38,14 +43,7 @@ def check_all_sessions():
if (time.time() - sess.cmd_last) > idle_timeout: if (time.time() - sess.cmd_last) > idle_timeout:
sess.msg("Idle timeout exceeded, disconnecting.") sess.msg("Idle timeout exceeded, disconnecting.")
sess.handle_close() sess.handle_close()
## This doesn't seem to provide an accurate indication of timed out
## sessions.
#if not sess.writable() or not sess.readable():
# print 'Problematic Session:'
# print 'Readable ', sess.readable()
# print 'Writable ', sess.writable()
def remove_session(session): def remove_session(session):
""" """
Removes a session from the session list. Removes a session from the session list.

View file

@ -1,25 +1,13 @@
#!/bin/bash #!/bin/bash
export DJANGO_SETTINGS_MODULE="settings" export DJANGO_SETTINGS_MODULE="settings"
## Uncomment whichever python binary you'd like to use to run the game.
## Evennia is developed on 2.5 but should be compatible with 2.4.
# PYTHON_BIN="python"
# PYTHON_BIN="python2.4"
PYTHON_BIN="python2.5"
## The name of your logfile.
LOGNAME="logs/evennia.log"
## Where to put the last log file from the game's last running
## on next startup.
LOGNAME_OLD="logs/evennia.log.old"
mv $LOGNAME $LOGNAME_OLD
## There are several different ways you can run the server, read the ## There are several different ways you can run the server, read the
## description for each and uncomment the desired mode. ## description for each and uncomment the desired mode.
## Generate profile data for use with cProfile. ## TODO: Make this accept a command line argument to use interactive
# $PYTHON_BIN -m cProfile -o profiler.log -s time server.py ## mode instead of having to uncomment crap.
## Interactive mode. Good for development and debugging. ## Interactive mode. Good for development and debugging.
#$PYTHON_BIN server.py #twistd -noy twistd -ny server.py
## Stand-alone mode. Good for running games. ## Stand-alone mode. Good for running games.
nohup $PYTHON_BIN server.py > $LOGNAME & twistd -ny server.py