天天看點

基于socketserver的python多線程聊天室

在python3中,socketserver提供的讀寫是基于byte位元組的。故發送字元資料需要先編碼。接受到的資料需要解碼。

而Python2.x 中。沒有位元組的概念。隻有兩種字元串:表示 str 和 unicode 。故py2不必encode/decode

#Talk is cheep, show you the code.

server:

"""everytime before you send msg, encode it
after you rec msg, decode it!
"""

import socketserver
import re
import socket

class ClientError(Exception):
	"An exception thrown because the client gave bad input to the server."
	pass

class PythonChatServer(socketserver.ThreadingTCPServer):
	"the server class"
	
	def __init__(self, server_address, RequestHandlerClass):
		"""Set up an initially empty mapping between a user' s nickname
		and the file-like object used to send data to that user."""
		socketserver.ThreadingTCPServer.__init__(self, server_address, RequestHandlerClass)
		self.users = {}

class RequestHandler(socketserver.StreamRequestHandler):
	"""Handles the life cycle of a user's connection to the chat server: connecting,
	chatting, running server commands, and disconnecting."""
	
	NICKNAME = re.compile('^[A-Za-z0-9_-]+$') #regex for a valid nickname
	
	def handle(self):
		"""Handles a connection: gets the user's nickname, then
		processes input from the user until they quit or drop the
		connection."""
		self.nickname = None
		
		self.privateMessage("Who are you?")
		nickname=self._readline()
		done = False
		try:
			self.nickCommand(nickname)
			self.privateMessage('Hello %s, welcome to the Python Chat Server.' % nickname)
			self.broadcast('%s has joined the chat.' %nickname,False)
			#print('%s has joined the chat.' %nickname) #print in server
		except (ClientError) as error:
			self.privateMessage(error.args[0])
			done = True
		except socket.error:
			done = True
		
		#Now they're logged in, let them chat
		while not done:
			try:
				done = self.processInput()
			except (ClientError) as error: #wrong:ClientError(error)
				self.privateMessage(str(error))
			except socket.error:
				done = True
				
	def finish(self):
		"""Automatically called when handle() is done!!!"""
		if self.nickname:
			#The user successfully connected before disconnecting.
			#Broadcast that they're quitting to everyone else.
			message = '%s has quit.' % self.nickname
			if hasattr(self, 'partingWords'):
				message = '%s has quit: %s' % (self.nickname, self.partingWords)
			self.broadcast(message, False)
			
			#Remove the user from the list so we don't keep trying
			#to send them messages.
			if self.server.users.get(self.nickname):
				del(self.server.users[self.nickname])
		self.request.shutdown(2)
		self.request.close()
		
	def processInput(self):
		"""Reads a line from the socket input and either runs it as a
		command, or broadcasts it as chat text."""
		done = False
		l = self._readline()
		command, arg = self._parseCommand(l)
		if command:
			done = command(arg)
		else:
			l = '<%s> %s\n' % (self.nickname, l)
			self.broadcast(l)
		return done
		
	def nickCommand(self,nickname):
		"Attempts to change a user's nickname."
		if not nickname:
			raise ClientError('No nickname provided.')
		if not self.NICKNAME.match(nickname):
			raise ClientError('Invalid nickname: %s' % nickname)
		if nickname == self.nickname:
			raise ClientError('You\'re already known as %s.' % nickname)
		if self.server.users.get(nickname,None):
			raise ClientError('There\'s already a user named "%s" here.' %nickname)
		oldNickname = None
		if self.nickname:
			oldNickname=self.nickname
			del(self.server.users[self.nickname])
		self.server.users[nickname]=self.wfile
		self.nickname=nickname
		if oldNickname:
			self.broadcast('%s is now known as %s' % (oldNickname, self.nickname))
		
	def quitCommand(self, partingWords):
		"""Tells the other users that this user has quit, then makes
		sure the handler will close this connection."""
		if partingWords:
			self.partingWords = partingWords
		#Returning True makes sure the user will be disconnected.
		return True
		
	def namesCommand(self, ignored):
		"Returns a list of the users in this chat room."
		self.privateMessage(', '.join(self.server.users.keys()))
		
	#Below are helper methods
	
	def broadcast(self, message, includeThisUser=True):
		"""Send a message to every connected user, possibly exempting the
		user who's the cause of the message."""
		message = self._ensureNewline(message)
		for user, output in self.server.users.items():
			if includeThisUser or user != self.nickname:
				output.write(message.encode('utf-8'))
	
	def privateMessage(self, message):
		"Send a private message to this user."
		self.wfile.write(self._ensureNewline(message).encode('utf-8')) #must encode before send
		
	def _readline(self):
		"Reads a line, removing any whitespace."
		return self.rfile.readline().strip().decode('utf-8') #must decode after rec
		
	def _ensureNewline(self, s):
		"Makes sure a string ends in a newline."
		if s and s[-1] !='\n':
			s += '\r\n'
		return s
		
	def _parseCommand(self, input):
		"""Try to parse a string as a command to the server.If it's an
		implemented command, run the corresponding method.
		input /xxx, runs xxxCommand"""
		commandMethod, arg = None, None
		if input and input[0]=='/':
			if len(input) < 2:
				raise ClientError('Invalid command: "%s"' % input)
			commandAndArg = input[1:].split(' ',1)
			if len(commandAndArg) == 2:
				command, arg = commandAndArg
			else:
				command = commandAndArg[0] #can not without [],otherwise it's list ,which just has one member.
			commandMethod = getattr(self, command + 'Command', None)
			if not commandMethod:
				raise ClientError('No such command: "%s"' %command)
		#if input[0]!='/', which means input is not a command
		#then commandMethod will be None
		return commandMethod, arg
		
if __name__ == '__main__':
	import sys
	if len(sys.argv) < 3:
		print('Usage: %s [hostname] [port number]' %sys.argv[0])
		sys.exit(1)
	hostname = sys.argv[1]
	port = int(sys.argv[2])
	PythonChatServer((hostname,port),RequestHandler).serve_forever()
           

client:

import socket
import select
import sys
import os
from threading import Thread

class ChatClient:

    def __init__(self, host, port, nickname):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((host, port))
        self.input = self.socket.makefile('rb', 0)
        self.output = self.socket.makefile('wb', 0)

        #Send the given nickname to the server.
        authenticationDemand = self.input.readline().decode('utf-8')
        if not authenticationDemand.startswith("Who are you?"):
            raise Exception ("This doesn't seem to be a Python Chat Server.")
        self.output.write((nickname + '\r\n').encode('utf-8'))
        response = self.input.readline().strip().decode('utf-8')
        if not response.startswith("Hello"):
            raise Exception (response)
        print(response)

        #Start out by printing out the list of members.
        self.output.write(('/names\r\n').encode('utf-8'))
        print("Currently in the chat room:", self.input.readline().decode('utf-8').strip())

        self.run()

    def run(self):
        """Start a separate thread to gather the input from the
        keyboard even as we wait for messages to come over the
        network. This makes it possible for the user to simultaneously
        send and receive chat text."""
        
        propagateStandardInput = self.PropagateStandardInput(self.output)
        propagateStandardInput.start()

        #Read from the network and print everything received to standard
        #output. Once data stops coming in from the network, it means
        #we've disconnected.
        inputText = True
        while inputText:
            inputText = self.input.readline().decode('utf-8')
            if inputText:
                print (inputText.strip())
        propagateStandardInput.done = True

    class PropagateStandardInput(Thread):
        """A class that mirrors standard input to the chat server
        until it's told to stop."""

        def __init__(self, output):
            """Make this thread a daemon thread, so that if the Python
            interpreter needs to quit it won't be held up waiting for this
            thread to die."""
            Thread.__init__(self)
            self.setDaemon(True)
            self.output = output
            self.done = False

        def run(self):
            "Echo standard input to the chat server until told to stop."
            while not self.done:
                inputText = sys.stdin.readline().strip() #no need to decode when read from stdin
                if inputText:
                    self.output.write((inputText + '\r\n').encode('utf-8'))

if __name__ == '__main__':
    import sys
    #See if the user has an OS-provided 'username' we can use as a default 
    #chat nickname. If not, they have to specify a nickname.
    try:
        import pwd
        defaultNickname = pwd.getpwuid(os.getuid())[0]
    except ImportError:
        defaultNickname = None

    if len(sys.argv) < 3 or not defaultNickname and len(sys.argv) < 4:
        print('Usage: %s [hostname] [port number] [username]' % sys.argv[0])
        sys.exit(1)

    hostname = sys.argv[1]
    port = int(sys.argv[2])

    if len(sys.argv) > 3:
        nickname = sys.argv[3]
    else:
        #We must be on a system with usernames, or we would have
        #exited earlier.
        nickname = defaultNickname

    ChatClient(hostname, port, nickname)