# ChatTCPServer.py
import socket
# IPv4를 따르는 TCP 소켓을 생성한다.
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Address already in use 오류 해결
serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 소켓을 localhost(127.0.0.1) 주소의 20825번 포트에 할당하고 활성화한다.
serverName = "localhost"
serverPort = 20825
serverSocket.bind((serverName, serverPort))
serverSocket.listen(1)
# 소켓이 생성된 곳의 주소와 포트 번호를 출력
print("The server socket is listening on", serverSocket.getsockname())
socket.socket()
으로 소켓을 생성할 수 있다. 1번째 인자에는 IP 주소 버전을 명시하는데 socket.AF_INET
는 IPv4를 의미하고, socket.AF_INET6
은 IPv6을 의미한다. 2번째 인자는 소켓의 종류를 설정할 수 있는데 socket.SOCK_STREAM
은 TCP를 의미하고, socket.SOCK_DGRAM
은 UDP를 의미한다.
소켓은 클라이언트와 연결이 끊어지고 나면 네트워크 지연 때문에 미처 도달하지 못한 패킷이 있을 수도 있기 때문에 TIME_WAIT
상태에 들어간다. 이 상태에선 해당 포트에 새로운 소켓을 할당(bind
)할 수 없다. 운영체제마다 다르지만 TIME_WAIT
은 보통 1분 동안 지속된다. 따라서 프로그램을 테스트할 때마다 기다리지 않기 위해 setsockopt
함수로 소켓 설정을 바꿔준다.
그리고 해당 주소, 포트에 서버 소켓을 할당하고 활성화(listen
)시킨다.
# ChatTCPServer.py
import threading
# 클라이언트 이름, 주소, 소켓을 저장한 클래스
# 모든 클라이언트 연결 스레드(connectionThread)에서 공유한다.
class ClientsInfo:
def __init__(self):
# 구조 : { nickname: [IP, port, connectionSocket], ... }
self.clientsInfo = {}
# clientsInfo를 위한 lock
self.lock = threading.Lock()
def addClientInfo(self, nickname, IP, port, connectionSocket):
self.lock.acquire()
self.clientsInfo[nickname] = [IP, port, connectionSocket]
self.lock.release()
def deleteClientInfo(self, nickname):
self.lock.acquire()
del self.clientsInfo[nickname]
self.lock.release()
# 클라이언트의 정보를 담고 있다.
clientsInfo = ClientsInfo()
# 서버의 메인 스레드는 클라이언트의 연결 요청을 처리한다.
try:
while True:
# 클라이언트의 연결 요청이 들어오면 새로운 소켓이 생성된다.
(connectionSocket, clientAddress) = serverSocket.accept()
# 이 클라이언트 소켓과 주소를 통해 클라이언트와 정보를 교환한다.
connectionThread = threading.Thread(
target=connection,
args=(clientsInfo, connectionSocket, clientAddress)
)
# 서버의 메인 스레드가 종료되면 이 스레드도 종료된다.
connectionThread.daemon = True
# 스레드를 실행한다.
connectionThread.start()
# 서버에서 Ctrl+C 가 눌러졌을 때
except KeyboardInterrupt:
pass
accept()
함수를 통해 클라이언트의 연결 요청을 기다린다. 만약 요청이 오면 해당 클라이언트와 정보를 주고 받을 수 있는 소켓과 해당 클라이언트 주소가 반환된다.
이렇게 반환된 소켓과 주소를 통해 클라이언트와 정보를 교환하는 스레드를 생성한다. 따라서 서버의 메인 스레드는 단순히 클라이언트의 접속 요청만 처리하고, 서버와 클라이언트의 정보 교환은 새로 생성된 스레드에서 이뤄진다.
그리고 만약 서버에서 Ctrl+C
가 눌러져 KeyboardInterrupt Exception
이 발생하면 반복문이 끝난다.
# ChatTCPServer.py
# 클라이언트와 연결된 모든 소켓과 서버 소켓을 닫는다.
for clientInfo in clientsInfo.clientsInfo.values():
clientInfo[2].close()
serverSocket.close()
print("\nBye bye~")
서버 프로세스가 종료되기 전에는 서버에 생성된 모든 소켓을 닫아주는 것이 좋다. 그래서 모든 클라이언트 연결 소켓과 서버 소켓을 닫아주고 서버 프로세스를 종료한다.
# ChatTCPServer.py
import json
# command list
MAX_USER_IN_CHATROOM = 10
SERVER_VERSION = "1"
CLIENT_VERSION = "1"
CONNECT = "0"
CONNECT_DUPLICATE_NICKNAME = "01"
CONNECT_CHATROOM_FULL = "02"
CONNECT_SUCCESS = "03"
USERS = "1"
USERS_RESPONSE = "10"
WHISPER = "2"
WHISPER_NO_SUCH_USER = "20"
WHISPER_MYSELF = "21"
WHISPER_SUCCESS = "22"
EXIT = "3"
EXIT_BROADCAST = "30"
VERSION = "4"
VERSION_RESPONSE = "40"
RENAME = "5"
RENAME_DUPLICATE_NICKNAME = "50"
RENAME_SUCCESS = "51"
RTT = "6"
RTT_RESPONSE = "60"
CHAT = "7"
CHAT_RESPONSE = "70"
NEW_USER_ENTERS = "8"
# check whether message has banned words
def isValidMessage(message, bannedWords):
for bannedWord in bannedWords:
if message.lower().find(bannedWord) >= 0:
return False
return True
# function for a connection thread
def connection(clientsInfo, connectionSocket, clientAddress):
nickname = ""
bannedWords = ["i hate professor"]
try:
request = json.loads(connectionSocket.recv(1024).decode("utf-8"))
cmd = request["cmd"]
response = {}
# receive a request from the client. there are 1 kinds of request.
# - CONNECT : a client want to connect to the server. (want to enter to a chatroom)
if cmd == CONNECT:
nickname = request["nickname"]
# send a response to the client. there are 3 kinds of response.
# - CONNECT_DUPLICATE_NICKNAME : The nickname is already used by someone else.
# - CONNECT_CHATROOM_FULL : The chatroom is full.
# - CONNECT_SUCCESS : The client has successfully connected to the server
if nickname in clientsInfo.clientsInfo:
response["cmd"] = CONNECT_DUPLICATE_NICKNAME
connectionSocket.send(json.dumps(response).encode("utf-8"))
connectionSocket.close()
return
elif len(clientsInfo.clientsInfo) == MAX_USER_IN_CHATROOM:
response["cmd"] = CONNECT_CHATROOM_FULL
connectionSocket.send(json.dumps(response).encode("utf-8"))
connectionSocket.close()
return
else:
userCount = len(clientsInfo.clientsInfo) + 1
responseToEveryone = {}
responseToEveryone["cmd"] = NEW_USER_ENTERS
responseToEveryone["nickname"] = nickname
responseToEveryone["userCount"] = userCount
responseToEveryoneByte = json.dumps(responseToEveryone).encode("utf-8")
for clientInfo in clientsInfo.clientsInfo.values():
clientInfo[2].send(responseToEveryoneByte)
clientsInfo.addClientInfo(nickname, clientAddress[0], clientAddress[1], connectionSocket)
response["cmd"] = CONNECT_SUCCESS
response["serverAddress"] = serverSocket.getsockname()
response["userCount"] = userCount
print(nickname, "joined. There are", userCount, "users connected.")
connectionSocket.send(json.dumps(response).encode("utf-8"))
else:
print("Invalid request command. Command:", cmd)
connectionSocket.close()
return
while True:
# receive a request from the client. there are 7 kinds of requests.
# - USERS : show the <nickname, IP, port> list of all users
# - WHISPER : whisper to <nickname>
# - EXIT : disconnect from server, and quit client process
# - VERSION : show server's software version (and client software version)
# - RENAME : change client nickname
# - RTT : show RTT(Round Trip Time) from the client to the server and back
# - CHAT : send a chat to all users in the chatroom
requestString = connectionSocket.recv(1024).decode("utf-8")
# if the client disconnects server, or connection to the client has been lost
if requestString == "":
print("The TCP connection to the clinet(nickname=" + nickname + ") has been lost.")
break
request = json.loads(requestString)
cmd = request["cmd"]
print("Command", cmd)
response = {}
# send a response to the client. there are 1 kinds of response.
# - USERS_RESPONSE : send the <nickname, IP, port> list of all users
if cmd == USERS:
users = [] # [[clientNickname, clientIP, clientPort], ...]
for clientNickname, clientInfo in clientsInfo.clientsInfo.items():
users.append([clientNickname, clientInfo[0], clientInfo[1]])
response["cmd"] = USERS_RESPONSE
response["users"] = users
connectionSocket.send(json.dumps(response).encode("utf-8"))
# send a response to the client. there are 3 kinds of response.
# - WHISPER_NO_SUCH_USER : there is no user 'whispering to'
# - WHISPER_MYSELF : cannot whisper myself
# - WHISPER_SUCCESS : succeed to whisper
elif cmd == WHISPER:
to = request["to"]
msg = request["msg"]
if to not in clientsInfo.clientsInfo:
response["cmd"] = WHISPER_NO_SUCH_USER
elif clientsInfo.clientsInfo[to][2] == connectionSocket:
response["cmd"] = WHISPER_MYSELF
elif not isValidMessage(msg, bannedWords):
break
else:
responseToReceiver = {}
responseToReceiver["cmd"] = CHAT_RESPONSE
responseToReceiver["from"] = nickname
responseToReceiver["msg"] = msg
clientsInfo.clientsInfo[to][2].send(json.dumps(responseToReceiver).encode("utf-8"))
response["cmd"] = WHISPER_SUCCESS
connectionSocket.send(json.dumps(response).encode("utf-8"))
# send a response to the client. there are 3 kinds of response.
# - EXIT_BROADCAST : broadcast the fact that a user left the chatroom to all users
elif cmd == EXIT:
break
# send a response to the client. there are 3 kinds of response.
# - VERSION_RESPONSE : send server version to the client
elif cmd == VERSION:
response["cmd"] = VERSION_RESPONSE
response["serverVersion"] = SERVER_VERSION
connectionSocket.send(json.dumps(response).encode("utf-8"))
# send a response to the client. there are 3 kinds of response.
# - RENAME_DUPLICATE_NICKNAME : cannot change to the <nickname> because the <nickname> is userd by another user
# - RENAME_SUCCESS : change client nickname
elif cmd == RENAME:
newNickname = request["newNickname"]
if newNickname in clientsInfo.clientsInfo:
response["cmd"] = RENAME_DUPLICATE_NICKNAME
else:
clientsInfo.deleteClientInfo(nickname)
clientsInfo.addClientInfo(newNickname, clientAddress[0], clientAddress[1], connectionSocket)
nickname = newNickname
response["cmd"] = RENAME_SUCCESS
response["newNickname"] = newNickname
connectionSocket.send(json.dumps(response).encode("utf-8"))
# send a response to the client. there are 3 kinds of response.
# - RTT_RESPONSE : send back a 'beginTime' to the client as it is
elif cmd == RTT:
response["cmd"] = RTT_RESPONSE
response["beginTime"] = request["beginTime"]
connectionSocket.send(json.dumps(response).encode("utf-8"))
# send a response to the client. there are 3 kinds of response.
# - CHAT_RESPONSE : send back a 'msg' to all users in the chatroom as it is
# - EXIT_BROADCAST : disconnects the client if message from the client has banned words
elif cmd == CHAT:
msg = request["msg"]
if not isValidMessage(msg, bannedWords):
break
else:
response["cmd"] = CHAT_RESPONSE
response["from"] = nickname
response["msg"] = msg
responseByte = json.dumps(response).encode("utf-8")
for clientInfo in clientsInfo.clientsInfo.values():
if clientInfo[2] != connectionSocket:
clientInfo[2].send(responseByte)
# Undefined request comnmand
else:
print("Invalid request command. Command:", cmd)
except Exception as e:
print(e)
# disconnect the client and broadcast the fact that a user left the chatroom to all users
connectionSocket.close()
clientsInfo.deleteClientInfo(nickname)
response = {}
response["cmd"] = EXIT_BROADCAST
response["nickname"] = nickname
response["userCount"] = len(clientsInfo.clientsInfo)
responseByte = json.dumps(response).encode("utf-8")
for clientInfo in clientsInfo.clientsInfo.values():
clientInfo[2].send(responseByte)