my-psb/openclassrooms-trainings/roboc/connectsocket.py

341 lines
10 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Author: freezed <freezed@users.noreply.github.com> 2018-03-06
Version: 0.1
Licence: `GNU GPL v3` GNU GPL v3: http://www.gnu.org/licenses/
This file is part or roboc project. View `readme.md`
"""
import socket
import select
class ConnectSocket:
"""
ConnectSocket
=======
Provide network connection and management methods
:Example:
>>> c0 = ConnectSocket()
Server is running, listening on port 5555
>>> c0.list_sockets(False, False)
[]
>>> c0.list_sockets()
''
>>> c0.count_clients()
0
>>> c0.close()
Server stop
"""
# default connection parameters
_HOST = 'localhost'
_PORT = 5555
_BUFFER = 1024
_MAIN_CONNECT = "MAIN_CONNECT"
# Template messages
_BROADCAST_MSG = "{}~ {}\n"
_MSG_DISCONNECTED = "<gone away to infinity, and beyond>"
_MSG_SALUTE = "Hi, {}, wait for other players\n"
_MSG_SERVER_STOP = "Server stop"
_MSG_START_SERVER = "Server is running, listening on port {}"
_MSG_USER_IN = "<entered the network>"
_MSG_WELCOME = "Welcome. First do something usefull and type your name: "
_MSG_UNWELCOME = "Sorry, no more place here.\n"
_MSG_UNSUPPORTED = "Unsupported data:«{}»{}"
_SERVER_LOG = "{}:{}|{name}|{msg}"
# Others const
_MAX_CLIENT_NAME_LEN = 8
_MIN_CLIENT_NAME_LEN = 3
_MAX_CLIENT_NB = 5
def __init__(self, host=_HOST, port=_PORT):
"""
Set up the server connection using a socket object
:param str _HOST: domain or IPV4 address
:param int _PORT: port number
:return obj: socket
"""
# Setting up the connection
self._main_sckt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._main_sckt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._main_sckt.bind((host, port))
self._main_sckt.listen(5)
# Init connection list
self._inputs = []
self._inputs.append(self._main_sckt)
# Init username list, to keep match between inputs & name lists
self._user_name = []
self._user_name.append(self._MAIN_CONNECT)
# Logging server's activity
self.server_log(self._MSG_START_SERVER.format(port))
def broadcast(self, sender, message):
"""
Send a message to all named-clients but the sender
TODO should replace sckt.send() too
:param obj sender: sender socket (or 'server' str() for server)
:param str message: message to send
"""
# Define senders name
if sender == 'server':
name = 'server'.upper()
else:
idx = self._inputs.index(sender)
name = self._user_name[idx].upper()
message = self._BROADCAST_MSG.format(name, message)
recipients = self.list_sockets(False, False)
for sckt in recipients:
if sckt != sender:
sckt.send(message.encode())
# NOTE I remove a `try/except` here because I do not
# encountered any Error, I cannot choose the correct
# exception type
# sckt.close() \ self._inputs.remove(sckt)
def close(self):
""" Cleanly closes each socket (clients) of the network """
self._main_sckt = self._inputs.pop(0)
i = 1
for sckt in self._inputs:
self.server_log(self._SERVER_LOG.format(
*sckt.getpeername(),
name=self._user_name[i],
msg="closed client socket")
)
sckt.close()
i += 1
self._inputs.clear()
self._main_sckt.close()
self.server_log(self._MSG_SERVER_STOP)
def count_clients(self):
"""
Count connected and named clients
(to avoid playing with a unamed player)
"""
return len(self.list_sockets(False, False))
def data_filter(self, data, s_idx):
"""
Filters the data basically:
- Alphanumeric char only
- non-alphanum char replaced by 'x'
- 'xxx' for string < MIN
- Troncated after the MAX char
- trailed with index number if same name exists
"""
# alphanumeric
if str(data).isalnum() is False:
f_data = ''
for char in data:
if char.isalnum():
f_data += char
else:
f_data += 'x'
data = f_data
# minimum length
if len(data) < self._MIN_CLIENT_NAME_LEN:
data += 'xxx'
# maximum length
data = data[0:self._MAX_CLIENT_NAME_LEN]
# name already used
if data in self._user_name:
data += str(s_idx)
return data
def list_sockets(self, print_string=True, user_name=True):
"""
List connected sockets
:param bool print_string: return txt formated socket list
:param bool user_name: return user_name
"""
if user_name:
client_list = [
name for name in self._user_name
if name != self._MAIN_CONNECT and name is not False
]
else:
client_list = [
sckt for (idx, sckt) in enumerate(self._inputs)
if sckt != self._main_sckt
and self._user_name[idx] is not False
]
# NOTE maybe there is a better way for the next condition: when
# client connects it has not yet his name filled in the
# _user_name attribut, then this method returns only clients
# with a filled name
if print_string and len(client_list) == 0:
client_list = ""
elif print_string:
client_list = ", ".join(client_list)
return client_list
def listen(self):
"""
Listen sockets activity
Get the list of sockets which are ready to be read and apply:
- connect a new client
- return data sended by client
This object only processes data specific to network connections,
other data (username and message sent) are stored in `u_name` &
`message` attributes to be used by parent script
:return str, str: user_name, data
"""
self.u_name = ""
self.message = ""
# listennig…
rlist, [], [] = select.select(self._inputs, [], [], 0.05)
for sckt in rlist:
# logging, broadcasting & sending (LBS) params
logging = True
broadcasting = True
sending = True
# Listen for new client connection
if sckt == self._main_sckt:
sckt_object, sckt_addr = sckt.accept()
# Maximum connection number is not reached: accepting
if len(self._inputs) <= self._MAX_CLIENT_NB:
self._inputs.append(sckt_object)
self._user_name.append(False)
# LBS params override
log_msg = self._SERVER_LOG.format(
*sckt_addr, name="unknown", msg="connected"
)
sckt_object.send(self._MSG_WELCOME.encode())
broadcasting = False
sending = False
# Refusing
else:
sckt_object.send(self._MSG_UNWELCOME.encode())
self.server_log(self._SERVER_LOG.format(
*sckt_addr, name="unknow", msg="rejected"
))
sckt_object.close()
break
else: # receiving data
data = sckt.recv(self._BUFFER).decode().strip()
s_idx = self._inputs.index(sckt)
if self._user_name[s_idx] is False: # setting username
# saving name
self._user_name[s_idx] = self.data_filter(data, s_idx)
# LBS params override
log_msg = self._SERVER_LOG.format(
*sckt.getpeername(),
name=self._user_name[s_idx],
msg="set user name"
)
bdcst_msg = self._MSG_USER_IN
send_msg = self._MSG_SALUTE.format(self._user_name[s_idx])
elif data.upper() == "QUIT": # client quit network
# LBS params override
log_msg = self._SERVER_LOG.format(
*sckt.getpeername(),
name=self._user_name[s_idx],
msg="disconnected"
)
# broadcasting params
bdcst_msg = self._MSG_DISCONNECTED
self.broadcast(sckt, bdcst_msg)
broadcasting = False
sending = False
self._inputs.remove(sckt)
self._user_name.pop(s_idx)
sckt.close()
elif data:
self.u_name, self.message = self._user_name[s_idx], data
# LBS params override
log_msg = self._SERVER_LOG.format(
*sckt.getpeername(),
name=self._user_name[s_idx],
msg=data
)
broadcasting = False
sending = False
else:
# LBS params override
log_msg = self._SERVER_LOG.format(
*sckt.getpeername(),
name=self._user_name[s_idx],
msg=self._MSG_UNSUPPORTED.format(data)
)
send_msg = self._MSG_UNSUPPORTED.format(data, "\n")
broadcasting = False
if logging:
self.server_log(log_msg)
if broadcasting:
self.broadcast(sckt, bdcst_msg)
if sending:
sckt.send(send_msg.encode())
@staticmethod
def server_log(msg):
""" Log activity on server-side"""
print(msg)
# writes in a logfile here TODO19
# adds a timestamp TODO20
if __name__ == "__main__":
""" Starting doctests """
import doctest
doctest.testmod()