Welcome, guest | Sign In | My Account | Store | Cart

This recipe demos how to write a simple command line chat server & client using multiplexing using select.select. The server uses select call to multiplex multiple clients and the client uses it to multiplex command line & socket I/O.

Python, 251 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# First the server

#!/usr/bin/env python
#!/usr/bin/env python

"""
A basic, multiclient 'chat server' using Python's select module
with interrupt handling.

Entering any line of input at the terminal will exit the server.
"""

import select
import socket
import sys
import signal
from communication import send, receive

BUFSIZ = 1024


class ChatServer(object):
    """ Simple chat server using select """
    
    def __init__(self, port=3490, backlog=5):
        self.clients = 0
        # Client map
        self.clientmap = {}
        # Output socket list
        self.outputs = []
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server.bind(('',port))
        print 'Listening to port',port,'...'
        self.server.listen(backlog)
        # Trap keyboard interrupts
        signal.signal(signal.SIGINT, self.sighandler)
        
    def sighandler(self, signum, frame):
        # Close the server
        print 'Shutting down server...'
        # Close existing client sockets
        for o in self.outputs:
            o.close()
            
        self.server.close()

    def getname(self, client):

        # Return the printable name of the
        # client, given its socket...
        info = self.clientmap[client]
        host, name = info[0][0], info[1]
        return '@'.join((name, host))
        
    def serve(self):
        
        inputs = [self.server,sys.stdin]
        self.outputs = []

        running = 1

        while running:

            try:
                inputready,outputready,exceptready = select.select(inputs, self.outputs, [])
            except select.error, e:
                break
            except socket.error, e:
                break

            for s in inputready:

                if s == self.server:
                    # handle the server socket
                    client, address = self.server.accept()
                    print 'chatserver: got connection %d from %s' % (client.fileno(), address)
                    # Read the login name
                    cname = receive(client).split('NAME: ')[1]
                    
                    # Compute client name and send back
                    self.clients += 1
                    send(client, 'CLIENT: ' + str(address[0]))
                    inputs.append(client)

                    self.clientmap[client] = (address, cname)
                    # Send joining information to other clients
                    msg = '\n(Connected: New client (%d) from %s)' % (self.clients, self.getname(client))
                    for o in self.outputs:
                        # o.send(msg)
                        send(o, msg)
                    
                    self.outputs.append(client)

                elif s == sys.stdin:
                    # handle standard input
                    junk = sys.stdin.readline()
                    running = 0
                else:
                    # handle all other sockets
                    try:
                        # data = s.recv(BUFSIZ)
                        data = receive(s)
                        if data:
                            # Send as new client's message...
                            msg = '\n#[' + self.getname(s) + ']>> ' + data
                            # Send data to all except ourselves
                            for o in self.outputs:
                                if o != s:
                                    # o.send(msg)
                                    send(o, msg)
                        else:
                            print 'chatserver: %d hung up' % s.fileno()
                            self.clients -= 1
                            s.close()
                            inputs.remove(s)
                            self.outputs.remove(s)

                            # Send client leaving information to others
                            msg = '\n(Hung up: Client from %s)' % self.getname(s)
                            for o in self.outputs:
                                # o.send(msg)
                                send(o, msg)
                                
                    except socket.error, e:
                        # Remove
                        inputs.remove(s)
                        self.outputs.remove(s)
                        


        self.server.close()

if __name__ == "__main__":
    ChatServer().serve()

#############################################################################
# The chat client
#############################################################################
#! /usr/bin/env python

"""
Simple chat client for the chat server. Defines
a simple protocol to be used with chatserver.

"""

import socket
import sys
import select
from communication import send, receive

BUFSIZ = 1024

class ChatClient(object):
    """ A simple command line chat client using select """

    def __init__(self, name, host='127.0.0.1', port=3490):
        self.name = name
        # Quit flag
        self.flag = False
        self.port = int(port)
        self.host = host
        # Initial prompt
        self.prompt='[' + '@'.join((name, socket.gethostname().split('.')[0])) + ']> '
        # Connect to server at port
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((host, self.port))
            print 'Connected to chat server@%d' % self.port
            # Send my name...
            send(self.sock,'NAME: ' + self.name) 
            data = receive(self.sock)
            # Contains client address, set it
            addr = data.split('CLIENT: ')[1]
            self.prompt = '[' + '@'.join((self.name, addr)) + ']> '
        except socket.error, e:
            print 'Could not connect to chat server @%d' % self.port
            sys.exit(1)

    def cmdloop(self):

        while not self.flag:
            try:
                sys.stdout.write(self.prompt)
                sys.stdout.flush()

                # Wait for input from stdin & socket
                inputready, outputready,exceptrdy = select.select([0, self.sock], [],[])
                
                for i in inputready:
                    if i == 0:
                        data = sys.stdin.readline().strip()
                        if data: send(self.sock, data)
                    elif i == self.sock:
                        data = receive(self.sock)
                        if not data:
                            print 'Shutting down.'
                            self.flag = True
                            break
                        else:
                            sys.stdout.write(data + '\n')
                            sys.stdout.flush()
                            
            except KeyboardInterrupt:
                print 'Interrupted.'
                self.sock.close()
                break
            
            
if __name__ == "__main__":
    import sys

    if len(sys.argv)<3:
        sys.exit('Usage: %s chatid host portno' % sys.argv[0])
        
    client = ChatClient(sys.argv[1],sys.argv[2], int(sys.argv[3]))
    client.cmdloop()

###############################################################################
# The communication module (communication.py)
###############################################################################
import cPickle
import socket
import struct

marshall = cPickle.dumps
unmarshall = cPickle.loads

def send(channel, *args):
    buf = marshall(args)
    value = socket.htonl(len(buf))
    size = struct.pack("L",value)
    channel.send(size)
    channel.send(buf)

def receive(channel):

    size = struct.calcsize("L")
    size = channel.recv(size)
    try:
        size = socket.ntohl(struct.unpack("L", size)[0])
    except struct.error, e:
        return ''
    
    buf = ""

    while len(buf) < size:
        buf = channel.recv(size - len(buf))

    return unmarshall(buf)[0]

This demonstrates the power of select.select call which can be used as a very efficient I/O multiplexer for any I/O stream such as sockets, file streams etc.

Here is a sample session with the chat server/client.

Starting the server

$ python chatserver.py &

Starting the client

[anand@localhost python]$ python chatclient.py anand 3490 Connected to chat server@3490 [anand@127.0.0.1]> (Connected: New client (2) from appy@127.0.0.1) [anand@127.0.0.1]> hi appy [anand@127.0.0.1]> (Hung up: Client from appy@127.0.0.1) [anand@127.0.0.1]> ^C [anand@127.0.0.1]> Interrupted. [anand@localhost python]$

asyncore & asynchat modules provide wrappers for event handling using select/poll in Python. Twistedmatrix provides an advanced reactor pattern using select.

[28/09/07] - Changed to work across machines by using network byte addressing. The communication module has been borrowed from recipe #457669.

12 comments

Marco Antonio Islas Cruz 16 years, 4 months ago  # | flag

One problem using socket.ntohl. There is one horrible problem using socket.ntohl:

Just try to send a string with at least 230 chars. It's a bit weird the fact that sending something with 530 chars will work fine.

Sam Wang 14 years, 11 months ago  # | flag

How to resolve the CPU problem? when i started the server and connected as a client, CPU almost 60%...

Please help

seham_yam 14 years, 10 months ago  # | flag

very thanks for this code >>> but there is a problem i really faced for along time.

Traceback (most recent call last): File "C:/Users/TOSHIBA/Desktop/v.py", line 17, in <module> from cmmunication import send, receive ImportError: No module named cmmunication

how can i solve this would somebody help me

seham_yam 14 years, 10 months ago  # | flag

i received this error

Traceback (most recent call last): File "C:\Users\TOSHIBA\Desktop\recipe-531824-1.py", line 17, in <module> from communication import send, receive ImportError: No module named communication

how can i solve this is this code work in windows and symbian mobiles ?? if not how can i chang it?? thanks

Myles Gallagher 14 years, 5 months ago  # | flag

Well... Program is intersting, i have been trying to understand network programming for a bit. since I am teaching my self with tutorials and things like analyzing premade code, this is helpful. I do keep getting an error however at line 54 of the server module:

def serve(self):

    inputs = [self.server,sys.stdin]
    self.outputs = []

    running = 1

    while running:

        try:

54>>> inputready,outputready,exceptready = select.select(inputs, self.outputs, []) except select.error, e: break except socket.error, e: break

the error is:

File "C:/Users/Owner/Desktop/server.py", line 54, in serve inputready,outputready,exceptready = select.select(inputs, self.outputs, []) TypeError: argument must be an int, or have a fileno() method.

I am not sure where the select module is and it would probably be helpful if i could see the code for the select module, but i cant.

One of the arguments is obviosly not an integer when it is put into the function, but i am not sure how to fix this. My first attemp (which was probably pointless) i put int(*) around the arguments, and tested it on one or the other.

Each time it came out saying the argument for int must be a string or a number. I am not sure what either of the arguments in line 54.

If any one could help me with this it would be greatly appreciated

Jesse 14 years, 4 months ago  # | flag

@Myles:

Select.select expects a file-like handle, including sockets or integers representing file descriptors. The error you got, "argument must be an int, or have a fileno() method", means that one of the inputs you provided did not implement a file-like interface (usually because it is an undefined variable).

Is self.server a socket? Did you make sure it is defined?

See the select module documentation for more information.

Anand B Pillai 13 years, 9 months ago  # | flag

The select.select on sys.stdin will work only on POSIX systems (Linux/Unix etc). It won't work on Windows. So this code cannot execute on windows as it is. Perhaps there is a way around it, but I haven't tried it yet.

Leonhard Weber 13 years, 7 months ago  # | flag

One problem using socket.ntohl. There is one horrible problem using socket.ntohl:

Just try to send a string with at least 230 chars. It's a bit weird the fact that sending something with 530 chars will work fine....

Thinking about this particular issue... Is it possible to actually skip the socket.htonl call by just pushing that responsibility onto struct.pack('>L', actual_lenght)? I mean, by getting the integer as Big-Endian... the '>' before the 'L' or alternatively an '!'. As described in http://docs.python.org/release/2.6.6/library/struct.html#struct-alignment

Would it help?

Aaron Hanson 13 years ago  # | flag

wonderful recipe but it has a serious bug with respect to multi-packet messages; it locks up if the message is bigger than the MTU. Fix:

--- recipe-531824-1.py.orig 2011-03-18 14:04:04.087992789 -0700 +++ recipe-531824-1.py 2011-03-18 14:04:30.481874871 -0700 @@ -246,6 +246,6 @@ buf = ""

 while len(buf) < size:

- buf = channel.recv(size - len(buf)) + buf += channel.recv(size - len(buf))

 return unmarshall(buf)[0]
Aaron Hanson 13 years ago  # | flag

let me try that patch one more time; I'm unfamiliar with the 'Markdown syntax' for this discussion board:

--- recipe-531824-1.py.orig 2011-03-18 14:04:04.087992789 -0700
+++ recipe-531824-1.py  2011-03-18 14:04:30.481874871 -0700
@@ -246,6 +246,6 @@
     buf = ""

     while len(buf) < size:
-        buf = channel.recv(size - len(buf))
+        buf += channel.recv(size - len(buf))

     return unmarshall(buf)[0]
shahab 8 years, 9 months ago  # | flag

i have a problem with it


from communication import send, receive ImportError: No module named 'communication'


how can i solve it in python3?

J Storch 8 years, 3 months ago  # | flag

shahab. The code expecting communication to be in a separate module or file. Just create a new file names "communication.py" and place everything below and including ############################################################################### # The communication module (communication.py) ############################################################################### in the new file.