server.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. #!/usr/bin/env python3
  2. import configparser
  3. import threading
  4. import socket
  5. import argparse
  6. import os
  7. import json
  8. import random
  9. import time
  10. titles = [
  11. "Python",
  12. "3",
  13. "JavaScript",
  14. "Hamburger",
  15. "Wieloryb",
  16. "Szkoła",
  17. "Uniwersytet",
  18. "Remiza",
  19. "Pomidor",
  20. "Radość",
  21. "Młot",
  22. "Baran",
  23. "Hiacynt",
  24. "Jedzenie w samolocie",
  25. "Rozpacz",
  26. "Ciasto",
  27. "Słońce",
  28. "Pantofel",
  29. "Widmo",
  30. "Dama",
  31. "Krawędź",
  32. "Japonki",
  33. "Zamek",
  34. "Szmalcownik",
  35. "Bazy danych",
  36. "Szarada",
  37. "Głaz",
  38. "Piwnica",
  39. "Pióro",
  40. "Rachunek prawdopodobieństwa",
  41. "Jabłko",
  42. "Granat",
  43. "Przystań",
  44. "Sumo",
  45. "Baba",
  46. "Tygrys",
  47. "Staw",
  48. "Oliwa",
  49. "Grzebień",
  50. "Kuba",
  51. "Piła",
  52. "Polka",
  53. "Warta",
  54. "Róża",
  55. "Kosa",
  56. "Narcyz",
  57. "Zebra",
  58. "Kapelusz",
  59. "Kule",
  60. "Mars",
  61. "Kiwi",
  62. "Mysz"
  63. ]
  64. class Server(threading.Thread):
  65. """
  66. Supports management of server connections.
  67. Attributes:
  68. connections (list): A list of ServerSocket objects representing the active connections.
  69. host (str): The IP address of the listening socket.
  70. port (int): The port number of the listening socket.
  71. """
  72. def __init__(self, host, port):
  73. super().__init__()
  74. self.config = configparser.ConfigParser()
  75. self.config.read("config.ini", "UTF8")
  76. self.connections = []
  77. self.host = host
  78. self.port = port
  79. # self.players = []
  80. self.data = {}
  81. self.data["title"] = "undefined"
  82. self.data["state"] = "wait"
  83. self.data["users"] = {}
  84. self.data["messages"] = {}
  85. self.data["scores"] = {}
  86. self.data["total_scores"] = {}
  87. self.votes = {}
  88. def reset(self):
  89. time.sleep(10)
  90. self.votes = {}
  91. self.data["scores"] = {}
  92. self.data["messages"] = {}
  93. self.data["title"] = random.choice(titles)
  94. self.data["state"] = "wait"
  95. if self.player_count() >= 3:
  96. self.data["state"] = "game"
  97. else:
  98. self.data["state"] = "wait"
  99. dump = json.dumps(self.data, ensure_ascii=False)
  100. self.broadcast_all(dump)
  101. def run(self):
  102. """
  103. Creates the listening socket. The listening socket will use the SO_REUSEADDR option to
  104. allow binding to a previously-used socket address. This is a small-scale application which
  105. only supports one waiting connection at a time.
  106. For each new connection, a ServerSocket thread is started to facilitate communications with
  107. that particular client. All ServerSocket objects are stored in the connections attribute.
  108. """
  109. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  110. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  111. sock.bind((self.host, self.port))
  112. sock.listen(1)
  113. print('Listening at', sock.getsockname())
  114. idx = 0
  115. try:
  116. while True:
  117. # Accept new connection
  118. sc, sockname = sock.accept()
  119. print('{} has joined'.format(idx))
  120. # Create new thread
  121. server_socket = ServerSocket(sc, sockname, self, idx)
  122. idx += 1
  123. # Start new thread
  124. server_socket.start()
  125. # Add thread to active connections
  126. self.connections.append(server_socket)
  127. except KeyboardInterrupt:
  128. self.quit()
  129. def quit(self):
  130. with open("config.ini", 'w') as config_file:
  131. self.config.write(config_file)
  132. os._exit(0)
  133. def broadcast(self, message, source):
  134. """
  135. Sends a message to all connected clients, except the source of the message.
  136. Args:
  137. message (str): The message to broadcast.
  138. source (tuple): The socket address of the source client.
  139. """
  140. for connection in self.connections:
  141. # Send to all connected clients except the source client
  142. if connection.sockname != source:
  143. connection.send(message)
  144. def broadcast_all(self, message):
  145. """
  146. Sends a message to all connected clients
  147. Args:
  148. message (str): The message to broadcast.
  149. """
  150. for connection in self.connections:
  151. connection.send(message)
  152. def remove_connection(self, connection):
  153. """
  154. Removes a ServerSocket thread from the connections attribute.
  155. Args:
  156. connection (ServerSocket): The ServerSocket thread to remove.
  157. """
  158. self.connections.remove(connection)
  159. def player_count(self):
  160. return len(self.connections)
  161. class ServerSocket(threading.Thread):
  162. """
  163. Supports communications with a connected client.
  164. Attributes:
  165. sc (socket.socket): The connected socket.
  166. sockname (tuple): The client socket address.
  167. server (Server): The parent thread.
  168. """
  169. def __init__(self, sc, sockname, server, id):
  170. super().__init__()
  171. self.sc = sc
  172. self.id = id
  173. self.sockname = sockname
  174. self.server = server
  175. self.username = ""
  176. def call_quit(self):
  177. print('{} has left'.format(self.id))
  178. self.server.data["users"].pop(self.id, None)
  179. self.server.data["messages"].pop(self.id, None)
  180. self.server.data["scores"].pop(self.id, None)
  181. self.server.data["total_scores"].pop(self.id, None)
  182. self.server.votes.pop(self.id, None)
  183. self.sc.close()
  184. server.remove_connection(self)
  185. if self.server.player_count() == 0:
  186. self.server.data["state"] = "wait"
  187. def run(self):
  188. """
  189. Receives data from the connected client and broadcasts the message to all other clients.
  190. If the client has left the connection, closes the connected socket and removes itself
  191. from the list of ServerSocket threads in the parent Server thread.
  192. """
  193. try:
  194. while True:
  195. message = self.sc.recv(1024).decode('utf8')
  196. if not message:
  197. self.call_quit()
  198. return
  199. elif message:
  200. if self.username == "":
  201. self.username = message
  202. self.server.data["users"][self.id] = self.username
  203. if len(self.server.data["users"]) >= 3 and self.server.data["state"] == "wait":
  204. self.server.data["title"] = random.choice(titles)
  205. self.server.data["state"] = "game"
  206. dump = json.dumps(self.server.data, ensure_ascii=False)
  207. self.server.broadcast(dump, self.sockname)
  208. data_copy = self.server.data.copy()
  209. data_copy["playerid"] = self.id
  210. dump = json.dumps(data_copy, ensure_ascii=False)
  211. self.send(dump)
  212. elif self.server.data["state"] == "game":
  213. self.server.data["messages"][self.id] = message
  214. if len(self.server.data["messages"]) == self.server.player_count():
  215. self.server.data["state"] = "vote"
  216. dump = json.dumps(self.server.data, ensure_ascii=False)
  217. self.server.broadcast_all(dump)
  218. elif self.server.data["state"] == "vote":
  219. self.server.votes[self.id] = int(message)
  220. print(self.server.votes)
  221. if len(self.server.votes) == self.server.player_count():
  222. for key in self.server.votes:
  223. player = self.server.votes[key]
  224. if player not in self.server.data["scores"]:
  225. self.server.data["scores"][player] = 0
  226. self.server.data["scores"][player] += 1
  227. for player in self.server.data["scores"]:
  228. name = self.server.data["users"][player]
  229. print(name)
  230. if player not in self.server.data["total_scores"]:
  231. if name in self.server.config["DEFAULT"]:
  232. self.server.data["total_scores"][player] = int(self.server.config["DEFAULT"][name])
  233. else:
  234. self.server.data["total_scores"][player] = 0
  235. self.server.config["DEFAULT"][name] = "0"
  236. self.server.data["total_scores"][player] += self.server.data["scores"][player]
  237. self.server.config["DEFAULT"][name] = str(int(int(self.server.config["DEFAULT"][name]) + int(self.server.data["scores"][player])))
  238. self.server.data["state"] = "display"
  239. dump = json.dumps(self.server.data, ensure_ascii=False)
  240. self.server.broadcast_all(dump)
  241. with open("config.ini", 'w') as config_file:
  242. self.server.config.write(config_file)
  243. self.server.reset()
  244. #print(self.server.data)
  245. except KeyboardInterrupt or EOFError:
  246. print("Caught keyboard interrupt, exiting")
  247. call_quit()
  248. os._exit(0)
  249. except ConnectionResetError:
  250. self.call_quit()
  251. def send(self, message):
  252. """
  253. Sends a message to the connected server.
  254. Args:
  255. message (str): The message to be sent.
  256. """
  257. self.sc.sendall(message.encode('utf8'))
  258. def quit(server):
  259. print('Closing all connections...')
  260. for connection in server.connections:
  261. connection.sc.close()
  262. print('Shutting down the server...')
  263. os._exit(0)
  264. def exit(server):
  265. """
  266. Allows the server administrator to shut down the server.
  267. Typing 'q' in the command line will close all active connections and exit the application.
  268. """
  269. try:
  270. while True:
  271. input('')
  272. except KeyboardInterrupt:
  273. quit(server)
  274. except EOFError:
  275. quit(server)
  276. if __name__ == '__main__':
  277. parser = argparse.ArgumentParser(description='Chatroom Server')
  278. parser.add_argument('host', help='Interface the server listens at')
  279. parser.add_argument('-p', metavar='PORT', type=int, default=1060,
  280. help='TCP port (default 1060)')
  281. args = parser.parse_args()
  282. # Create and start server thread
  283. server = Server(args.host, args.p)
  284. server.start()
  285. exit = threading.Thread(target = exit, args = (server,))
  286. exit.start()