이번 post에는 소켓이 뭔지 간략하게 설명하고 파이썬에서의 소켓 모듈을 확인한 후 마지막으로 thread 기법을 적용하지 않은 서버와 클라이언트 1:1 채팅 프로그램을 간략하게 구현한 내용을 작성해보겠습니당ㅎㅎ…
내가 대학교에서 네트워크 전공 강의를 들을 때 배운 소켓의 정의는 아래이다.
서로 떨어진 두 대의 컴퓨터 사이에서 TCP/IP 네트워크를 통해 상호 통신이 가능하도록 운영체제에서 해당 자원을 할당하고, 처리해 주는 방식
나는 위 소켓 정의에서 중요한 키워드는 TCP/IP 와 상호 통신 이라고 생각한다. 그럼 각각의 키워드가 소켓의 정의에서 무엇을 의미하는지 간단하게 살펴보자.
TCP/IP는 인터넷 프로토콜 스위트(Internet Protocol Suite)로 OSI 7 Layer 중에서 Layer3, 4를 다루는 프로토콜로 패킷 통신 방식의 인터넷 프로토콜인 IP(Internet Protocol)와 전송 조절 프로토콜인 TCP(Transmisson Control Protocol)를 합쳐서 부르는 용어다.
즉 네트워크 상 안전하고 효율적인 데이터 전송을 위한 필수 요건들을 정의하고 있다. 아래는 TCP/IP의 몇 가지 특징을 설명한 것이다.
→ 송신자가 수신자에게 IP 주소를 사용해 데이터를 전달하고 그 데이터가 제대로 전송됐는지?, 너무 빠르거나 느리지는 않는지? 등 데이터 전송을 보증해주는 것을 의미
소켓은 HTTP 통신과 달리 두 프로그램이 서로 실시간으로 데이터를 송수신 가능하게 생성되는 통신 창구라고 생각할 수 있다. 클라이언트에서 서버로만 데이터를 요청하는 것이 아닌 서버도 언제든지 클라이언트 측으로 데이터를 요청 가능한 양방향 통신이라고 할 수 있다.
하지만, 소켓 통신은 HTTP 통신과 달리 서버와 클라이언트가 실시간으로 계속 연결이 되고 있어야 하므로 실시간 채팅, 스트리밍 서비스에 이용되며 비교적 많은 네트워크 비용, 리소스가 발생된다.
맨 위의 그림을 보면 응용 프로그램 Layer와 트랜스포트 Layer 사이에 소켓 층이 존재하는데 이는 응용 프로그램에서 데이터를 송수신 하기 위해서는 트랜스포트 Layer에서 소켓을 거쳐야 가능하다고 볼 수 있다. 소켓은 IP와 Port 번호로 정의되며 같은 IP를 사용하는 응용 프로그램이라도 port 번호로 구분된 통신 창구를 가질 수 있다.
→ 소켓은 OS에서 제공하는 응용 Layer와 트랜스포트 Layer 사이의 인터페이스 역할
위에서 설명한 것처럼 소켓은 연결을 요청하는 클라이언트와 연결을 수락하는 서버로 나뉘어지는데 서버와 클라이언트는 서로 조금씩 다르게 구현 과정이 필요하다.
아까 위에서 소켓을 OS에서 제공하는 인터페이스라고 설명한 듯이 소켓 통신을 구현하는 것은 언어에 크게 상관없이 구현 방식은 비슷하다고 볼 수 있다. 아래의 그림을 보면 이해하기 쉽다.
서버에 데이터를 주고 받기 위해 소켓 연결 요청을 하는 측으로 host의 IP와 port 번호로 서버에 연결을 요청한다.
클라이언트 측으로부터 소켓 연결 요청을 대기하는 측으로 연결 요청 시 수락하여 요청된 각 클라이언트 별로 소켓을 생성해 통신을 가능하게 한다.
채팅 프로그램에서 server의 역할은 binding 할 서버의 ip와 port를 지정해 클라이언트가 서버에 접속할 수 있도록 하는 것이 중요하다.
아래의 글부터는 파이썬으로 구현한 아주 간단한 서버와 소켓 1:1 방식의 채팅 프로그램을 구현한 코드이다.
단순히 while 문을 통해 클라이언트 프로그램에서 메세지를 계속 보낼 수 있고 서버는 그 메세지 내용 그대로 다시 클라이언트로 전송해 채팅이 정상적으로 이뤄지는지 확인할 수 있도록 구현했다.
import socket
server_host = 'localhost'
server_port = 55555
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#서버와 연결
client_sock.connect((server_host, server_port))
print(f'Server: {server_host}, {server_port}와 정상적으로 연결')
while True:
message = input(">>> ")
#send vs sendaall: sendall이 버퍼에 있는 데이터를 다 보냈음을 보장함!!
client_sock.sendall(message.encode('utf-8'))
message = client_sock.recv(1024)
print(f"server: {message.decode()}")
client_sock.close()
import socket
host = 'localhost'
port = 55555
parent_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
parent_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
parent_sock.bind((host, port))
parent_sock.listen(5)
print(f'Server IP: {host}, Port Num: {port}로 서버 실행 중...')
child_sock, child_addr = parent_sock.accept()
print(f'{child_addr}에서 접속')
while True:
message = child_sock.recv(1024)
print(f'{child_addr}: {message.decode()}')
child_sock.sendall(message)
child_sock.close()
parent_sock.close()
두 코드다 위에서 한번 각각의 소켓 함수들이 무엇을 의미하는지 설명했으니 파이썬 언어를 조금 안다면 크게 이해하는데 어려움이 없을 거라고 생각한다. 그래서 언급하지 않은 몇몇 코드들을 설명하면 아래와 같다.
socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
마지막으로 프로그램의 실행 결과를 나타낸 그림으로 이번 post를 마무리하겠당~~