2018-03-08 11:06:22 +00:00
|
|
|
|
#!/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
|
2018-03-08 17:02:13 +00:00
|
|
|
|
import select
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConnectSocket:
|
|
|
|
|
"""
|
|
|
|
|
ConnectSocket
|
|
|
|
|
=======
|
|
|
|
|
|
|
|
|
|
Provide network connection and management methods
|
|
|
|
|
|
|
|
|
|
:Example:
|
2018-03-09 14:17:08 +00:00
|
|
|
|
>>> c0 = ConnectSocket()
|
2018-03-08 11:06:22 +00:00
|
|
|
|
Server is running, listening on port 5555
|
|
|
|
|
|
2018-03-09 14:17:08 +00:00
|
|
|
|
>>> c0.list_sockets(False, False)
|
2018-03-10 22:12:01 +00:00
|
|
|
|
[]
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
|
|
|
|
>>> c0.list_sockets()
|
2018-03-10 22:12:01 +00:00
|
|
|
|
''
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
|
|
|
|
>>> c0.count_clients()
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
|
>>> c0.close()
|
|
|
|
|
Server stop
|
|
|
|
|
"""
|
2018-03-08 17:08:43 +00:00
|
|
|
|
# default connection parameters
|
2018-03-08 11:06:22 +00:00
|
|
|
|
_HOST = 'localhost'
|
|
|
|
|
_PORT = 5555
|
|
|
|
|
_BUFFER = 1024
|
2018-03-10 22:12:01 +00:00
|
|
|
|
_MAIN_CONNECT = "MAIN_CONNECT"
|
2018-03-08 17:08:43 +00:00
|
|
|
|
|
|
|
|
|
# Template messages
|
2018-03-09 13:10:14 +00:00
|
|
|
|
_BROADCAST_MSG = "{}~ {}\n"
|
2018-03-08 11:06:22 +00:00
|
|
|
|
_MSG_DISCONNECTED = "<gone away to infinity, and beyond>"
|
2018-03-08 17:08:43 +00:00
|
|
|
|
_MSG_SALUTE = "Hi, {}, wait for other players\n"
|
2018-03-08 11:06:22 +00:00
|
|
|
|
_MSG_SERVER_STOP = "Server stop"
|
|
|
|
|
_MSG_START_SERVER = "Server is running, listening on port {}"
|
2018-03-08 17:08:43 +00:00
|
|
|
|
_MSG_USER_IN = "<entered the network>"
|
|
|
|
|
_MSG_WELCOME = "Welcome. First do something usefull and type your name: "
|
2018-03-11 21:43:19 +00:00
|
|
|
|
_MSG_UNWELCOME = "Sorry, no more place here.\n"
|
2018-03-12 16:51:21 +00:00
|
|
|
|
_MSG_UNSUPPORTED = "Unsupported data:«{}»{}"
|
2018-03-12 14:47:35 +00:00
|
|
|
|
_SERVER_LOG = "{}:{}|{name}|{msg}"
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
2018-03-11 07:58:44 +00:00
|
|
|
|
# Others const
|
|
|
|
|
_MAX_CLIENT_NAME_LEN = 8
|
2018-03-12 22:14:44 +00:00
|
|
|
|
_MIN_CLIENT_NAME_LEN = 3
|
2018-03-11 21:43:19 +00:00
|
|
|
|
_MAX_CLIENT_NB = 5
|
2018-03-11 07:58:44 +00:00
|
|
|
|
|
2018-03-08 17:20:57 +00:00
|
|
|
|
def __init__(self, host=_HOST, port=_PORT):
|
|
|
|
|
"""
|
|
|
|
|
Set up the server connection using a socket object
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
2018-03-08 17:20:57 +00:00
|
|
|
|
:param str _HOST: domain or IPV4 address
|
|
|
|
|
:param int _PORT: port number
|
|
|
|
|
:return obj: socket
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Setting up the connection
|
2018-03-12 16:51:21 +00:00
|
|
|
|
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)
|
2018-03-08 17:20:57 +00:00
|
|
|
|
|
2018-03-10 22:12:01 +00:00
|
|
|
|
# Init connection list
|
2018-03-08 17:20:57 +00:00
|
|
|
|
self._inputs = []
|
2018-03-12 16:51:21 +00:00
|
|
|
|
self._inputs.append(self._main_sckt)
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
2018-03-09 14:17:08 +00:00
|
|
|
|
# Init username list, to keep match between inputs & name lists
|
2018-03-08 17:20:57 +00:00
|
|
|
|
self._user_name = []
|
2018-03-10 22:12:01 +00:00
|
|
|
|
self._user_name.append(self._MAIN_CONNECT)
|
2018-03-08 17:20:57 +00:00
|
|
|
|
|
2018-03-12 14:47:35 +00:00
|
|
|
|
# Logging server's activity
|
|
|
|
|
self.server_log(self._MSG_START_SERVER.format(port))
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
2018-03-10 23:49:05 +00:00
|
|
|
|
def broadcast(self, sender, message):
|
2018-03-08 17:02:13 +00:00
|
|
|
|
"""
|
2018-03-10 23:49:05 +00:00
|
|
|
|
Send a message to all named-clients but the sender
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
2018-03-10 23:49:05 +00:00
|
|
|
|
TODO should replace sckt.send() too
|
|
|
|
|
|
|
|
|
|
:param obj sender: sender socket (or 'server' str() for server)
|
2018-03-09 13:10:14 +00:00
|
|
|
|
:param str message: message to send
|
2018-03-08 17:02:13 +00:00
|
|
|
|
"""
|
2018-03-10 23:49:05 +00:00
|
|
|
|
# Define senders name
|
|
|
|
|
if sender == 'server':
|
|
|
|
|
name = 'server'.upper()
|
|
|
|
|
else:
|
|
|
|
|
idx = self._inputs.index(sender)
|
|
|
|
|
name = self._user_name[idx].upper()
|
|
|
|
|
|
2018-03-09 13:10:14 +00:00
|
|
|
|
message = self._BROADCAST_MSG.format(name, message)
|
2018-03-10 23:49:05 +00:00
|
|
|
|
recipients = self.list_sockets(False, False)
|
|
|
|
|
|
|
|
|
|
for sckt in recipients:
|
|
|
|
|
if sckt != sender:
|
2018-03-12 16:51:21 +00:00
|
|
|
|
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)
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
2018-03-08 11:06:22 +00:00
|
|
|
|
def close(self):
|
|
|
|
|
""" Cleanly closes each socket (clients) of the network """
|
2018-03-12 16:51:21 +00:00
|
|
|
|
self._main_sckt = self._inputs.pop(0)
|
2018-03-08 11:06:22 +00:00
|
|
|
|
i = 1
|
|
|
|
|
for sckt in self._inputs:
|
2018-03-12 14:47:35 +00:00
|
|
|
|
self.server_log(self._SERVER_LOG.format(
|
|
|
|
|
*sckt.getpeername(),
|
|
|
|
|
name=self._user_name[i],
|
|
|
|
|
msg="closed client socket")
|
2018-03-12 16:51:21 +00:00
|
|
|
|
)
|
2018-03-08 11:06:22 +00:00
|
|
|
|
sckt.close()
|
|
|
|
|
i += 1
|
|
|
|
|
self._inputs.clear()
|
2018-03-12 16:51:21 +00:00
|
|
|
|
self._main_sckt.close()
|
2018-03-12 14:47:35 +00:00
|
|
|
|
self.server_log(self._MSG_SERVER_STOP)
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
2018-03-08 17:08:43 +00:00
|
|
|
|
def count_clients(self):
|
2018-03-08 23:01:54 +00:00
|
|
|
|
"""
|
|
|
|
|
Count connected and named clients
|
|
|
|
|
|
|
|
|
|
(to avoid playing with a unamed player)
|
|
|
|
|
"""
|
2018-03-10 22:12:01 +00:00
|
|
|
|
return len(self.list_sockets(False, False))
|
2018-03-08 17:08:43 +00:00
|
|
|
|
|
2018-03-12 22:14:44 +00:00
|
|
|
|
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
|
|
|
|
|
|
2018-03-09 14:17:08 +00:00
|
|
|
|
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:
|
2018-03-10 22:12:01 +00:00
|
|
|
|
client_list = [
|
|
|
|
|
name for name in self._user_name
|
|
|
|
|
if name != self._MAIN_CONNECT and name is not False
|
|
|
|
|
]
|
2018-03-08 11:06:22 +00:00
|
|
|
|
else:
|
2018-03-10 22:12:01 +00:00
|
|
|
|
client_list = [
|
|
|
|
|
sckt for (idx, sckt) in enumerate(self._inputs)
|
2018-03-12 16:51:21 +00:00
|
|
|
|
if sckt != self._main_sckt
|
2018-03-10 22:12:01 +00:00
|
|
|
|
and self._user_name[idx] is not False
|
|
|
|
|
]
|
|
|
|
|
|
2018-03-12 16:51:21 +00:00
|
|
|
|
# NOTE maybe there is a better way for the next condition: when
|
2018-03-10 22:12:01 +00:00
|
|
|
|
# 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:
|
2018-03-09 14:17:08 +00:00
|
|
|
|
client_list = ", ".join(client_list)
|
|
|
|
|
|
|
|
|
|
return client_list
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
2018-03-08 17:02:13 +00:00
|
|
|
|
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
|
|
|
|
|
|
2018-03-09 13:44:08 +00:00
|
|
|
|
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
|
|
|
|
|
|
2018-03-08 17:02:13 +00:00
|
|
|
|
:return str, str: user_name, data
|
|
|
|
|
"""
|
2018-03-09 13:44:08 +00:00
|
|
|
|
self.u_name = ""
|
|
|
|
|
self.message = ""
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
2018-03-09 13:44:08 +00:00
|
|
|
|
# listennig…
|
|
|
|
|
rlist, [], [] = select.select(self._inputs, [], [], 0.05)
|
2018-03-08 17:02:13 +00:00
|
|
|
|
for sckt in rlist:
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
# logging, broadcasting & sending (LBS) params
|
|
|
|
|
logging = True
|
|
|
|
|
broadcasting = True
|
|
|
|
|
sending = True
|
|
|
|
|
|
2018-03-08 17:02:13 +00:00
|
|
|
|
# Listen for new client connection
|
2018-03-12 16:51:21 +00:00
|
|
|
|
if sckt == self._main_sckt:
|
2018-03-12 13:07:38 +00:00
|
|
|
|
sckt_object, sckt_addr = sckt.accept()
|
2018-03-11 21:43:19 +00:00
|
|
|
|
|
2018-03-12 13:07:38 +00:00
|
|
|
|
# Maximum connection number is not reached: accepting
|
|
|
|
|
if len(self._inputs) <= self._MAX_CLIENT_NB:
|
2018-03-11 21:43:19 +00:00
|
|
|
|
self._inputs.append(sckt_object)
|
|
|
|
|
self._user_name.append(False)
|
|
|
|
|
|
2018-03-12 13:07:38 +00:00
|
|
|
|
# LBS params override
|
2018-03-12 14:47:35 +00:00
|
|
|
|
log_msg = self._SERVER_LOG.format(
|
|
|
|
|
*sckt_addr, name="unknown", msg="connected"
|
|
|
|
|
)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
sckt_object.send(self._MSG_WELCOME.encode())
|
|
|
|
|
broadcasting = False
|
|
|
|
|
sending = False
|
2018-03-11 21:43:19 +00:00
|
|
|
|
|
2018-03-12 13:07:38 +00:00
|
|
|
|
# Refusing
|
2018-03-11 21:43:19 +00:00
|
|
|
|
else:
|
|
|
|
|
sckt_object.send(self._MSG_UNWELCOME.encode())
|
2018-03-12 14:47:35 +00:00
|
|
|
|
self.server_log(self._SERVER_LOG.format(
|
|
|
|
|
*sckt_addr, name="unknow", msg="rejected"
|
|
|
|
|
))
|
2018-03-11 21:43:19 +00:00
|
|
|
|
sckt_object.close()
|
|
|
|
|
break
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
|
|
|
|
else: # receiving data
|
|
|
|
|
data = sckt.recv(self._BUFFER).decode().strip()
|
|
|
|
|
s_idx = self._inputs.index(sckt)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
2018-03-08 17:02:13 +00:00
|
|
|
|
if self._user_name[s_idx] is False: # setting username
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
2018-03-12 22:14:44 +00:00
|
|
|
|
# saving name
|
|
|
|
|
self._user_name[s_idx] = self.data_filter(data, s_idx)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
# LBS params override
|
2018-03-12 14:47:35 +00:00
|
|
|
|
log_msg = self._SERVER_LOG.format(
|
|
|
|
|
*sckt.getpeername(),
|
2018-03-12 22:14:44 +00:00
|
|
|
|
name=self._user_name[s_idx],
|
2018-03-12 14:47:35 +00:00
|
|
|
|
msg="set user name"
|
|
|
|
|
)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
bdcst_msg = self._MSG_USER_IN
|
|
|
|
|
|
2018-03-12 22:14:44 +00:00
|
|
|
|
send_msg = self._MSG_SALUTE.format(self._user_name[s_idx])
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
|
|
|
|
elif data.upper() == "QUIT": # client quit network
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
# LBS params override
|
2018-03-12 14:47:35 +00:00
|
|
|
|
log_msg = self._SERVER_LOG.format(
|
|
|
|
|
*sckt.getpeername(),
|
|
|
|
|
name=self._user_name[s_idx],
|
|
|
|
|
msg="disconnected"
|
|
|
|
|
)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
# broadcasting params
|
|
|
|
|
bdcst_msg = self._MSG_DISCONNECTED
|
|
|
|
|
self.broadcast(sckt, bdcst_msg)
|
|
|
|
|
|
|
|
|
|
broadcasting = False
|
|
|
|
|
sending = False
|
|
|
|
|
|
2018-03-08 17:02:13 +00:00
|
|
|
|
self._inputs.remove(sckt)
|
|
|
|
|
self._user_name.pop(s_idx)
|
|
|
|
|
sckt.close()
|
|
|
|
|
|
|
|
|
|
elif data:
|
2018-03-12 14:47:35 +00:00
|
|
|
|
self.u_name, self.message = self._user_name[s_idx], data
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
# LBS params override
|
2018-03-12 14:47:35 +00:00
|
|
|
|
log_msg = self._SERVER_LOG.format(
|
|
|
|
|
*sckt.getpeername(),
|
|
|
|
|
name=self._user_name[s_idx],
|
|
|
|
|
msg=data
|
|
|
|
|
)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
broadcasting = False
|
|
|
|
|
sending = False
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
|
|
|
|
else:
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
# LBS params override
|
2018-03-12 14:47:35 +00:00
|
|
|
|
log_msg = self._SERVER_LOG.format(
|
|
|
|
|
*sckt.getpeername(),
|
|
|
|
|
name=self._user_name[s_idx],
|
|
|
|
|
msg=self._MSG_UNSUPPORTED.format(data)
|
|
|
|
|
)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
2018-03-12 16:51:21 +00:00
|
|
|
|
send_msg = self._MSG_UNSUPPORTED.format(data, "\n")
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
broadcasting = False
|
|
|
|
|
|
|
|
|
|
if logging:
|
2018-03-12 14:47:35 +00:00
|
|
|
|
self.server_log(log_msg)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
|
|
|
|
|
if broadcasting:
|
|
|
|
|
self.broadcast(sckt, bdcst_msg)
|
|
|
|
|
|
|
|
|
|
if sending:
|
|
|
|
|
sckt.send(send_msg.encode())
|
|
|
|
|
|
2018-03-12 16:51:21 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def server_log(msg):
|
2018-03-12 13:07:38 +00:00
|
|
|
|
""" Log activity on server-side"""
|
2018-03-12 14:47:35 +00:00
|
|
|
|
print(msg)
|
2018-03-12 13:07:38 +00:00
|
|
|
|
# writes in a logfile here TODO19
|
2018-03-12 14:47:35 +00:00
|
|
|
|
# adds a timestamp TODO20
|
2018-03-08 17:02:13 +00:00
|
|
|
|
|
2018-03-08 11:06:22 +00:00
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
""" Starting doctests """
|
|
|
|
|
|
|
|
|
|
import doctest
|
|
|
|
|
doctest.testmod()
|