소켓 프로그래밍

김경윤·2025년 5월 16일

정보보안

목록 보기
8/10

소켓의 개념 1

• 전화 통신과 소켓 통신 비교


소켓의 개념 2


소켓의 개념 3

  • 소켓의 정의
    • 소켓은 1982년 BSD(Berkeley Software Distribution) UNIX 4.1에서 처음 소개되었고, 현재 널리
      사용되는 것은 1986년 BSD UNIX 4.3에서 개정된 것
    • 소켓은 소프트웨어로 작성된 통신 접속점
    • 소켓은 응용프로그램에서 TCP/IP를 이용하는 창구(인터페이스) 역할
    • 주로 TCP 상에서 동작하는 소켓을 사용해서 “TCP 소켓”
      또는 “TCP/IP 소켓” 이라고 함
    • 물론, UDP 소켓도 가능


Socket vs WebSocket 비교표

특징SocketWebSocket
프로토콜 레벨TCP/IP 저수준HTTP 기반 고수준
라이브러리파이썬 기본 내장 (socket)외부 라이브러리 필요 (websockets)
연결 방식양방향 통신 (Full-duplex)양방향 통신 (Full-duplex)
주요 용도일반 네트워크 통신, 서버-서버 통신웹 기반 실시간 통신
구현 방식동기/비동기 모두 가능주로 비동기(async/await)
보안기본적인 TCP/IP 보안TLS/SSL 지원 (WSS)
브라우저 지원직접 사용 불가대부분의 현대 브라우저 지원
초기 연결TCP 핸드셰이크HTTP 업그레이드 (WS 핸드셰이크)
사용 사례- 데이터베이스 연결
- 네트워크 서비스
- 시스템 간 통신
- 웹 채팅
- 실시간 게임
- 실시간 데이터 스트리밍
설치 필요성불필요 (내장)필요 (pip install websockets)
포트 사용모든 포트 사용 가능주로 80(WS)/443(WSS) 포트 사용
방화벽 통과추가 설정 필요할 수 있음일반적으로 용이 (HTTP 기반)
에러 처리수동 구현 필요내장 에러 처리 메커니즘

소켓을 통한 google.com 접속


google.com 접속 (실습)

import socket  # socket 모듈 임포트 (저수준 네트워크 통신을 위한 표준 라이브러리)

#### 1번째 방법 ####

# 소켓 객체 생성 (IPv4 주소 체계, TCP 통신 방식)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 구글 서버의 80번 포트에 연결 (HTTP 기본 포트)
sock.connect(("www.google.com", 80))

# HTTP GET 요청 전송 (간단히 "GET \n"만 보냄 — 실제 요청은 더 복잡함)
sock.send("GET \n".encode())

# 응답 데이터 1024바이트 수신
data = sock.recv(1024)

# 수신한 데이터 출력 (바이트 -> 문자열로 디코딩)
print(data.decode())

# 소켓 연결 종료
sock.close()


#### 2번째 방법 ####

# with문을 사용하면 블록이 끝날 때 소켓이 자동으로 닫힘 (자원 해제 자동 처리)
with socket.socket() as sock:
    # 서버 주소 및 포트 정의
    addr = ("www.google.com", 80)

    # 서버 연결
    sock.connect(addr)

    # 요청 전송
    sock.send("GET \n".encode())

    # 응답 수신
    data = sock.recv(1024)

    # 응답 출력
    print(data.decode())

🔹 소켓 상수

  • socket.AF_INET : IPv4
  • socket.AF_INET6 : IPv6
  • socket.SOCK_STREAM : TCP
  • socket.SOCK_DGRAM : UDP

🔹 with 문

  • with 블록을 벗어나면 자동으로 close() 실행
  • with socket.socket() as socksock = socket.socket() + sock.close()와 동일 효과

🔹 encode() 메소드

  • 문자열을 byte로 변환 (전송 시 필수)
  • 바이트 형식: b"..." 또는 b'...'
  • bytes("ABC", "utf-8")"ABC".encode()
  • str(문자열)과 bytes(바이트)는 다름

소켓 프로그래밍 구조


소켓 프로그래밍 - 서버

아래는 업로드한 코드를 타이핑하고, 각 단계별로 주석을 추가한 버전입니다:

import socket  # socket 모듈 임포트

# 1. 소켓 생성 (IPv4, TCP)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 바인딩 (호스트 주소는 공란으로 모든 인터페이스 허용, 포트는 9999로 설정)
sock.bind(("", 9999))

# 3. 클라이언트 접속 대기
sock.listen()
print("The echo server start.....")

# 4. 접속 수락 (클라이언트가 접속하면 conn과 addr에 저장됨)
conn, addr = sock.accept()

# 5. 데이터 수신 (최대 1024바이트 읽기)
read_data = conn.recv(1024)
print("recv >>>>>>>> {}".format(read_data))

# 6. 접속 종료
conn.close()  # 클라이언트 연결 종료
sock.close()  # 서버 소켓 종료

  • bind((IP, PORT))

    • 서버의 IP와 포트를 지정
    • IP에 "" 또는 "0.0.0.0"을 입력하면 모든 네트워크 인터페이스에서 접속 수신 가능
  • listen(1)

    • 서버가 동시 접속을 몇 개까지 허용할지 설정
    • 1이면 한 개의 클라이언트 접속만 허용
  • accept()

    • 클라이언트가 접속하면 **새로운 소켓 객체(conn)**와 **주소 정보(addr)**를 반환
    • conn은 해당 클라이언트와 통신할 소켓

소켓 프로그래밍 - 클라이언트

import socket  # socket 모듈 임포트

# 1. 소켓 생성 (IPv4, TCP)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 서버에 접속 시도 (127.0.0.1: 로컬호스트, 포트 9999)
sock.connect(("127.0.0.1", 9999))

# 3. 데이터 송신 (사용자 입력 → bytes 변환 후 전송)
send_data = input("입력: ")
sock.sendall(bytes(send_data, "utf-8"))

# 4. 접속 종료
print("클라이언트 종료!")
sock.close()

🔹 send() vs sendall()

  • send()

    • C언어 시스템 콜 기반 저수준 메소드
    • 데이터 일부만 전송될 가능성 있음
    • 전송된 바이트 수를 확인하고 반복 처리해야 함
  • sendall()

    • 파이썬의 고수준 메소드
    • 버퍼의 모든 데이터가 전송될 때까지 자동 처리
    • 사용자가 전송 완료 여부를 신경 쓸 필요 없음

✅ 일반적으로 sendall() 사용 권장


에코 서버 - 클라이언트


에코 서버 1 (메시지 1번 주고 받음)

import socket  # socket 모듈 임포트

# 1. 소켓 생성 (IPv4, TCP)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 바인딩 (모든 IP 허용, 포트 9999)
sock.bind(("", 9999))

# 3. 클라이언트 접속 대기
sock.listen()
print("The echo server start.....")

# 4. 클라이언트 접속 수락
conn, addr = sock.accept()

# 5-1. 데이터 수신
read_data = conn.recv(1024)
print("recv >>>>>>>> {}".format(read_data))

# 5-2. 데이터 송신 (수신한 데이터를 그대로 다시 전송 - 에코)
conn.sendall(read_data)
print("send >>>>>>>> {}".format(read_data))

# 6. 접속 종료
conn.close()
sock.close()

✅ 요약 설명:

  • 이 코드는 에코 서버로, 클라이언트가 보낸 데이터를 그대로 다시 클라이언트에 돌려보냅니다.
  • conn.sendall(read_data)는 수신한 데이터를 그대로 되돌려보내는 동작입니다.
  • sendall() 사용으로 전송 누락 없이 안정적인 송신이 가능합니다.

에코 클라이언트 1 (메시지 1번 주고 받음)

import socket  # socket 모듈 임포트 (네트워크 통신 기능 제공)

# 1. 소켓 생성 (IPv4 주소 체계, TCP 프로토콜)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 접속 시도 (127.0.0.1은 로컬호스트, 포트 9999는 서버가 열어둔 포트)
sock.connect(("127.0.0.1", 9999))

# 3-1. 데이터 송신
send_data = input("입력: ")  # 사용자로부터 메시지 입력받기
sock.sendall(bytes(send_data, "utf-8"))  # 입력값을 UTF-8로 인코딩하여 서버로 전송

# 3-2. 데이터 수신
recv_data = sock.recv(1024)  # 서버로부터 최대 1024바이트 데이터 수신
print(f"recv : {recv_data}")  # 수신한 데이터 출력 (바이트 형태)

# 4. 접속 종료
print("클라이언트 종료!")  # 종료 메시지 출력
sock.close()  # 소켓 종료 (연결 해제)

✅ 요약:

  • 이 코드는 에코 서버와 통신하는 클라이언트입니다.
  • 사용자가 입력한 데이터를 서버에 보내고, 서버로부터 그대로 다시 받은 데이터를 출력한 후 종료합니다.
  • sendall()recv()는 각각 안전한 송신과 수신을 위한 기본 메소드입니다.

에코 서버 2 (메시지 여러 번 주고 받음)

import socket  # socket 모듈 임포트 (네트워크 통신용)

# with문 사용: 블록이 끝나면 자동으로 소켓 close됨
with socket.socket() as s:
    s.bind(("", 9999))  # 모든 IP에서 오는 연결을 9999 포트로 바인딩
    s.listen()  # 클라이언트 접속 대기 상태 진입
    print("The echo server start.....")

    conn, addr = s.accept()  # 클라이언트 접속 수락, conn은 통신용 소켓, addr은 주소

    # 무한 반복: 클라이언트 메시지를 계속 수신하고 다시 전송
    while True:
        recv_data = conn.recv(1024)  # 데이터 수신 (최대 1024바이트)
        print("recv >>>>>>>> {}".format(recv_data.decode()))  # 수신 데이터 출력

        # "end"라는 문자열이 수신되면 반복 종료
        if recv_data.decode() == "end":
            break

        conn.sendall(recv_data)  # 수신 데이터를 클라이언트에게 다시 전송 (에코)
        print("send >>>>>>>> {}".format(recv_data))  # 전송 데이터 출력

print("echo complete..")  # 서버 종료 메시지

✅ 요약 설명:

  • 클라이언트가 "end"를 보낼 때까지 에코 서버 기능 반복 수행
  • recv() → 수신, sendall() → 에코 응답
  • with문 덕분에 소켓 자동 정리
  • conn.close()는 생략되었지만, with가 끝나면 서버 소켓은 안전하게 닫힘.

에코 클라이언트 2 (메시지 여러 번 주고 받음)

import socket  # socket 모듈 임포트 (네트워크 통신 기능 제공)

# 1. 소켓 생성 (IPv4 주소 체계, TCP 프로토콜 사용)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 서버 접속 시도 (로컬호스트 127.0.0.1, 포트 9999)
sock.connect(("127.0.0.1", 9999))

# 무한 루프를 통해 사용자 입력을 반복적으로 전송
while True:
    # 3-1. 데이터 송신
    send_data = input("입력: ")  # 사용자로부터 입력받기
    sock.sendall(bytes(send_data, "utf-8"))  # 입력값을 UTF-8로 인코딩하여 서버로 전송

    # 사용자가 "end"를 입력하면 루프 종료
    if send_data == "end":
        break

    # 3-2. 데이터 수신 (서버로부터 에코 응답 받기)
    recv_data = sock.recv(1024)  # 최대 1024바이트 수신
    print(f"recv : {recv_data}")  # 수신한 데이터 출력 (바이트형)

# 4. 접속 종료
print("클라이언트 종료!")  # 종료 메시지 출력
sock.close()  # 소켓 종료 (연결 해제)

✅ 동작 요약:

  • 사용자가 입력한 데이터를 서버에 보내고, 서버에서 다시 돌려준 메시지를 출력하는 에코 클라이언트.
  • "end"를 입력하면 통신 종료 및 소켓 닫힘.

다중 클라이언트 - 쓰레드 활용

🔹 송수신 처리 개요

  • 한 대의 컴퓨터에서 송수신을 동시에 처리하여 실시간 양방향 통신 가능
  • 송신 쓰레드: 사용자가 입력한 메시지를 서버로 전송
  • 수신 쓰레드: 서버에서 온 메시지를 실시간으로 받아 출력

🔹 서버에서 클라이언트 연결 처리

  • 다중 클라이언트동시에 접속 처리 가능
  • 각 클라이언트는 독립된 쓰레드에서 동작
    → 클라이언트마다 별도 통신 처리 가능

🔹 주의사항 (실무 참고용)

  • 쓰레드 동기화 필요
    → 공유 자원 접근 시 Lock 사용 권장
  • 예외 처리 필수
    → 네트워크 오류, 클라이언트 비정상 종료 등 대비

쓰레드 실습

import threading  # 파이썬 쓰레딩 모듈 임포트
import time       # 시간 지연을 위한 모듈

# 첫 번째 쓰레드가 실행할 함수
def thread1():
    for i in range(5):
        print("thread1 ++++++++")
        time.sleep(0.1)  # 0.1초 대기

# 두 번째 쓰레드가 실행할 함수
def thread2():
    for i in range(5):
        print("thread2 ========")
        time.sleep(0.1)  # 0.1초 대기

# 쓰레드 객체 생성 (각 함수 지정)
th1 = threading.Thread(target=thread1)
th2 = threading.Thread(target=thread2)

# 쓰레드 시작
th1.start()
th2.start()

# 쓰레드가 끝날 때까지 대기 (join 없으면 main END 먼저 출력될 수 있음)
th1.join()
th2.join()

# 모든 쓰레드가 종료된 후 출력
print("main END!")  # 주 쓰레드 종료 메시지

🔹 프로세스 & 쓰레드 기본 개념

  • 프로세스: 실행 중인 프로그램 (메모리에 올라가 독립적으로 동작)
  • 하나의 프로세스는 최소 하나 이상의 쓰레드를 포함
  • 멀티쓰레드: OS 스케줄러가 여러 쓰레드를 번갈아 실행해 동시 수행처럼 보이게 함
  • 실행할 때마다 스케줄링 순서가 달라져 결과도 달라질 수 있음

🔹 join() 함수 역할

  • join(): 해당 쓰레드가 완전히 끝날 때까지 메인 쓰레드가 기다리게 함
  • 이를 통해 좀비 프로세스(종료되지 않은 쓰레드) 발생 방지
  • 프로그램의 정상 종료를 보장

✅ 핵심 포인트

  • thread1()thread2()는 각각 별도의 쓰레드에서 실행됨
  • start()로 쓰레드 실행, join()으로 완료 대기
  • join()이 없으면 main END!가 먼저 출력될 수 있음
    동시성 테스트/동작 확인용 기본 예제로 적합

상속, 오버라이딩 실습

# 부모 클래스 dad 정의
class dad():
    def dad_look(self):
        print("handsome")  # 아빠 외모 출력

    def doYouKonw(self, lec):  # 일반적인 질문 응답 메소드
        print(f"I don't know {lec}")

# 부모 클래스 mom 정의
class mom():
    def mom_look(self):
        print("pretty")  # 엄마 외모 출력

# 자식 클래스 child1: dad와 mom을 다중 상속
class child1(dad, mom):  # 상속만 하고 내용 없음
    pass

# 자식 클래스 child2: dad와 mom을 다중 상속 + 메서드 오버라이딩
class child2(dad, mom):
    def doYouKonw(self, lec):  # 오버라이딩 (부모 메소드 재정의)
        print(f"I love {lec}")

# child1 객체 생성 및 부모 메소드 사용
c1 = child1()
c1.dad_look()     # → "handsome"
c1.mom_look()     # → "pretty"

# child2 객체 생성 및 오버라이딩된 메소드 호출
c2 = child2()
c2.doYouKonw("python")  # → "I love python"

✅ 요약

  • child1dadmom의 기능을 모두 상속
  • child2daddoYouKonw() 메서드를 오버라이딩하여 새로 정의
  • 다중 상속, 메서드 오버라이딩의 구조와 동작 확인 예제

쓰레드 이용한 채팅 프로그램 1

🖥️ Server (thread_server.py)

import socket
import threading

# 수신 처리 함수 (클라이언트별로 실행될 스레드 함수)
def thread_recv(client_socket, addr):
    while True:
        recv_data = client_socket.recv(1024)
        print(f"{addr}에서 받은 메시지 : {recv_data.decode()}")

        # 받은 데이터를 다시 클라이언트에게 전송 (에코)
        client_socket.sendall(recv_data)

        # "end" 수신 시 연결 종료
        if recv_data.decode() == "end":
            print(f"{addr}의 접속을 종료합니다.")
            client_socket.close()
            break

# 1. 소켓 생성 및 바인딩
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("", 9999))
sock.listen()
print("The echo server start.....")

# 2. 클라이언트 접속 대기 및 처리
while True:
    conn, addr = sock.accept()
    print(f"{addr} 연결됨")

    # 클라이언트마다 별도 스레드 생성
    recv_handler = threading.Thread(target=thread_recv, args=(conn, addr))
    recv_handler.start()

💻 Client (thread_client.py)

import socket
import threading
import time

# 송신 쓰레드 함수
def send_thread(sock):
    while True:
        send_data = input("입력: ")
        sock.sendall(bytes(send_data, "utf-8"))

        if send_data == "end":
            print("서버에 종료 요청 보냄")
            break

        time.sleep(0.3)  # 약간의 딜레이 (수신 전에 반복 방지)

# 수신 쓰레드 함수
def recv_thread(sock):
    while True:
        recv_data = sock.recv(1024)
        print(f"recv : {recv_data.decode()}")

        if recv_data.decode() == "end":
            print("서버에서 종료 응답 받음")
            break

# 1. 소켓 생성 및 서버 접속
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 9999))

# 2. 송신/수신 쓰레드 동시 실행
sth = threading.Thread(target=send_thread, args=(sock,))
rth = threading.Thread(target=recv_thread, args=(sock,))
sth.start()
rth.start()

✅ 핵심 정리

항목설명
서버접속한 클라이언트마다 thread_recv() 쓰레드 생성, 에코 처리
클라이언트송신과 수신을 각각 별도의 쓰레드로 처리해 동시 통신 가능
"end"종료 신호 역할 – 전송되면 해당 쓰레드 종료 및 소켓 닫힘

쓰레드 이용한 채팅 프로그램 2


✅ 전체 서버 코드 (thread_server2.py)

import socket
import threading
import os

numClient = 0  # 현재 접속 클라이언트 수

# 클라이언트 수신 처리용 스레드 함수
def thread_recv(client_socket, addr):
    global numClient

    while True:
        recv_data = client_socket.recv(1024)

        # 어떤 스레드(클라이언트)인지 확인
        print(f"Thread Name [{threading.current_thread().name}]에서 보낸 메시지 : {recv_data.decode()}")

        # 'end' 수신 시 연결 종료
        if recv_data.decode() == "end":
            numClient -= 1
            print(f"{addr}의 접속을 종료합니다.")
            print(f"현재 서버에 접속된 클라이언트 수 : {numClient}")
            client_socket.close()

            # 모든 클라이언트 접속 종료 시 서버도 종료
            if numClient == 0:
                print("서버를 종료합니다.")
                os._exit(0)
            break

        # 에코 메시지 전송
        client_socket.sendall(recv_data)

# =========================
# 메인 서버 코드 시작 부분
# =========================

# 1. 소켓 생성
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 바인딩
sock.bind(("", 9999))

# 3. 클라이언트 접속 대기
sock.listen()
print("The echo server start.....")

# 4. 접속 수락 및 스레드 생성 반복
while True:
    conn, addr = sock.accept()

    numClient += 1  # 접속자 수 증가
    print(f"현재 {addr}이 접속했습니다.")
    print(f"현재 서버에 접속된 클라이언트 수 : {numClient}")

    # 클라이언트마다 별도 스레드로 수신 처리
    recv_handler = threading.Thread(target=thread_recv, args=(conn, addr), name=str(addr))
    recv_handler.start()

✅ 핵심 요약

항목설명
thread_recv()각 클라이언트 통신을 담당할 쓰레드 함수
recv_handler = threading.Thread(...)클라이언트마다 독립 쓰레드 생성
name=addr스레드 이름을 접속 주소로 설정해 추적 가능
os._exit(0)모든 접속 종료 시 서버 강제 종료

라이브러리를 이용한 채팅 프로그램 – 에코 서버

아래는 업로드한 두 이미지를 통합하여 chat_server2.py 전체 코드를 완전하게 정리하고 주석을 덧붙인 버전입니다:


✅ 멀티 유저 채팅 서버 (chat_server2.py)

import socketserver  # 고수준 TCP 서버 구축용 모듈

# =============================
# ▶ 사용자 관리용 핸들러 클래스
# =============================
class MyHandler(socketserver.BaseRequestHandler):
    users = {}  # {username: (socket, addr)} 형태로 사용자 정보 저장

    # 전체 사용자에게 메시지 전송
    def broadcast(self, msg):
        for sock, addr in self.users.values():
            sock.send(msg.encode())  # 문자열 → 바이트로 전송

    # 사용자 등록
    def addUser(self, username, conn, addr):
        if username in self.users:
            conn.send("이미 등록되어 있습니다.\n".encode())
            return None
        
        self.users[username] = (conn, addr)  # 등록
        self.broadcast("★{}님이 참여했습니다.\n".format(username))
        print("채팅 참여 인원 {}명".format(len(self.users)))
        return username

    # 사용자 제거
    def delUser(self, username):
        del self.users[username]
        self.broadcast("★{}님이 퇴장했습니다.\n".format(username))
        print("채팅 참여 인원 {}명".format(len(self.users)))

    # =============================
    # ▶ 클라이언트 통신 처리 함수
    # =============================
    def handle(self):
        print(self.client_address[0])  # 접속 IP 출력

        # 사용자 이름 등록 루프
        while True:
            self.request.send("이름을 입력하세요: ".encode())
            username = self.request.recv(1024).decode()

            if self.addUser(username, self.request, self.client_address):
                break  # 등록 성공 시 탈출

        # 채팅 메시지 수신/송신 루프
        while True:
            data = self.request.recv(1024)
            print("[{}] {}".format(username, data.decode()))

            if data.decode() == "end":
                self.request.close()
                break

            self.broadcast("[{}] {}\n".format(username, data.decode()))

        print("[{}] 접속 종료".format(username))
        self.delUser(username)

# =============================
# ▶ 쓰레드 기반 TCP 서버 클래스
# =============================
class ChatServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

# =============================
# ▶ 서버 실행
# =============================
print("chat server start...")

chat_serv = ChatServer(("", 9999), MyHandler)
chat_serv.serve_forever()      # 서버 실행
chat_serv.shutdown()           # 서버 중단 요청 시
chat_serv.server_close()       # 소켓 자원 해제

✅ 작동 구조 요약

구성 요소설명
MyHandler클라이언트 요청을 처리하는 클래스
users 딕셔너리접속한 모든 유저의 소켓, 주소 정보 저장
broadcast()모든 유저에게 메시지 전송
addUser() / delUser()유저 등록 및 삭제 관리
handle()클라이언트 연결 시 실행, 이름 받고 채팅 처리
ChatServerThreadingMixIn 상속 → 다중 접속 처리 가능

0개의 댓글