본문 바로가기
Python

[Python] 소켓 통신하여 채팅 하기

by teamnova 2021. 9. 11.

안녕하세요!

오늘은 서버와 소켓 통신 하여 클라이언트 간 채팅을 구현해보도록 하겠습니다.

 

테스트 환경

- Ubuntu 18.04.5 LTS

- Python 3.6.9

 

1. 소켓(Socket)이란?

 소켓(Socket)이란 네트워크상에서 동작하는 프로그램 간 통신의 종착점(Endpoint)입니다. 즉, 프로그램이 네트워크에서 데이터를 통신할 수 있도록 연결해주는 연결부라고 할 수 있습니다.

채팅이나 게임 등 클라이언트와 서버 간 양방향 통신이 필요한 프로그램에 사용되고 있습니다.

 

 

2. 소켓 프로그래밍의 흐름

 소켓 프로그램의 흐름은 아래 그림과 같이 진행이 됩니다.

 

 

 

 

3. 구현

소켓에 구현 시 IP, Port, Protocol 등 옵션을 설정 해줄 수 있습니다.

본 글에서는 간단히 집고 넘어가니 자세히 알고 싶은 부분이 있다면 아래 링크를 참고해주세요.

https://docs.python.org/ko/3/library/socket.html

 

socket — 저수준 네트워킹 인터페이스 — Python 3.9.6 문서

이 모듈은 BSD socket 인터페이스에 대한 액세스를 제공합니다. 모든 현대 유닉스 시스템, 윈도우, MacOS, 그리고 아마 추가 플랫폼에서 사용할 수 있습니다. 파이썬 인터페이스는 유닉스 시스템 호

docs.python.org

 

3-1. 서버 소켓

파이썬 라이브러리 중 socket, _thread 라이브러리를 추가해준 뒤

 

- 소켓 생성(socket(family, type))

  • family : IP4v, IP6v...
  • type: 소켓 타입 / raw 소켓, 스트림소켓, 데이터그램 소켓...

 

- 소켓 옵션 설정(setsockopt(socket, level, optname, optval, optlen))

  • socket : 소켓의 번호
  • level : 옵션의 종류. 보통 SOL_SOCKET와 IPPROTO_TCP 중 하나를 사용
  • optname : 설정을 위한 소켓 옵션의 번호
  • optval : 설정 값이 저장된 주소값.
  • optlen : optval 버퍼의 크기

 

- IP/Port 할당(bind(host, port))

  • host : IP 주소
  • port : 열어줄 포트 번호

- 연결 대기(listen())

 

코드를 작성해줍니다.

import socket
from _thread import *

client_sockets = [] # 서버에 접속한 클라이언트 목록

# 서버 IP 및 열어줄 포트
HOST = '127.0.0.1'
PORT = 9999

# 서버 소켓 생성
print('>> Server Start')
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen()

추가로 클라이언트 간 채팅이 가능하기 위해 전역 변수로 클라이언트 소켓 정보를 담을 리스트(client_sockets)를 만들어 줍니다.

 

클라이언트가 연결 요청 후 새로운 스레드를 생성해주고 해당 클라이언트와 통신을 담당할 소켓을 배정하고

위에서 만든 소켓 정보 리스트(client_sockets)에 추가해 줍니다.

try:
    while True:
        print('>> Wait')

        client_socket, addr = server_socket.accept()
        client_sockets.append(client_socket)
        start_new_thread(threaded, (client_socket, addr))
        print("참가자 수 : ", len(client_sockets))
        
except Exception as e :
    print ('에러는? : ',e)

finally:
    server_socket.close()

 

담당 소켓의 코드는 다음과 같이 구성합니다.

# 쓰레드에서 실행되는 코드입니다.
# 접속한 클라이언트마다 새로운 쓰레드가 생성되어 통신을 하게 됩니다.
def threaded(client_socket, addr):
    print('>> Connected by :', addr[0], ':', addr[1])

    # 클라이언트가 접속을 끊을 때 까지 반복합니다.
    while True:

        try:

            # 데이터가 수신되면 클라이언트에 다시 전송합니다.(에코)
            data = client_socket.recv(1024)

            if not data:
                print('>> Disconnected by ' + addr[0], ':', addr[1])
                break

            print('>> Received from ' + addr[0], ':', addr[1], data.decode())

            # 서버에 접속한 클라이언트들에게 채팅 보내기
            # 메세지를 보낸 본인을 제외한 서버에 접속한 클라이언트에게 메세지 보내기
            for client in client_sockets :
                if client != client_socket :
                    client.send(data)

        except ConnectionResetError as e:
            print('>> Disconnected by ' + addr[0], ':', addr[1])
            break

    if client_socket in client_sockets :
        client_sockets.remove(client_socket)
        print('remove client list : ',len(client_sockets))

    client_socket.close()

 

담당 클라이언트에게 받은 데이터를 다른 클라이언트에게 보내기 위해 소켓 정보 리스트를 가져와 자신 외 다른 클라이언트 소켓으로 데이터를 전송 하게 끔 구현 해줍니다.

클라이언트와 지속적으로 정보를 주고 받아야하기에 recv()/send() 부분을 반복문으로 반복시켜줍니다.

 

 

3-2. 클라이언트 소켓

소켓 생성 후, connect() 메소드에 접속하고자 하는 서버의 IP와 포트 번호를 적어줍니다.

import socket
from _thread import *

HOST = '127.0.0.1'
PORT = 9999

client_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))

 

서버에서 보내오는 데이터에 대한 메소드를 만든 후

send()와 recv() 가 별개로 작동하도록 받는 부분을 스레드로 작동 시켜줍니다.

# 서버로부터 메세지를 받는 메소드
# 스레드로 구동 시켜, 메세지를 보내는 코드와 별개로 작동하도록 처리
def recv_data(client_socket) :
    while True :
        data = client_socket.recv(1024)

        print("recive : ",repr(data.decode()))
start_new_thread(recv_data, (client_socket,))
print ('>> Connect Server')

while True:
    message = input('')
    if message == 'quit':
        close_data = message
        break

    client_socket.send(message.encode())


client_socket.close()

서버와 동일하게 접속이 유지 되는 동안에는 코드가 끝나지 않도록 반복문을 통해 계속 구동하도록 유지 시켜줍니다.

 

 

4. 실행

실행 창 구분

서버 / 클라이언트1 / 클라이언트2

 

 

참고 포스팅

* 파이썬 소켓 통신 채팅 예제

https://stickode.com/detail.html?no=2428 

 

스틱코드

 

stickode.com