본문 바로가기

보안

정보보안 10,11 주차 - 소켓 프로그래밍

1. TCP/IP 프로토콜

1) TCP/IP 4계층

  • 애플리케이션 계층
    • 예: FTP, Telnet, SMTP, HTTP
    • OSI 7계층 모델의 5, 6, 7계층
  • 전송 계층
    • 예: TCP, UDP
    • OSI 7계층 모델의 4계층
  • 네트워크 계층
    • 예: IP, ARP, ICMP
    • OSI 7계층 모델의 3계층
  • 네트워크 인터페이스 계층
    • 예: 이더넷, Token Ring
    • OSI 7계층 모델의 1, 2계층

2) TCP/IP 전송 구조

  • 캡슐화
    • 각 계층을 지나면서 헤더가 붙음
  • 4계층에서는 Port 번호가 담긴 TCP 헤더가 붙음
    • 데이터 단위: 세그먼트
  • 3계층에서는 IP 주소가 담긴 헤더가 붙음
    • 데이터 단위: 패킷

정리:

IP 주소 → 어느 컴퓨터로 보낼지 결정
포트 번호 → 컴퓨터 안의 어느 프로그램으로 보낼지 결정

2. 소켓의 개념

1) 전화 통신 vs 소켓 통신

  • 전화 통신의 전화번호
    = 소켓 통신의 IP 주소 + 포트 번호

2) 소켓이라는 인터페이스로 통신

  • 클라이언트와 서버가 통신할 때 응용 프로그램이 TCP/IP를 직접 다루는 것이 아님
  • 소켓을 활용해서 통신함

3) 정의

  • 응용 프로그램에서 TCP/IP를 이용하는 창구, 즉 인터페이스 역할

정리:

소켓은 네트워크 통신을 하기 위해 프로그램이 사용하는 통신 창구

4) 포트 번호

  • 같은 컴퓨터 안에서도 여러 프로그램이 동시에 통신 가능한 이유는 프로그램별로 포트 번호가 다르기 때문

5) Socket vs WebSocket

구분                                                Socket                                                           WebSocket

프로토콜 레벨 TCP/IP 저수준 HTTP 기반 고수준
라이브러리 파이썬 기본 내장 외부 라이브러리
연결 방식 양방향 통신 양방향 통신
주요 용도 일반 네트워크 통신 웹 기반 실시간 통신

 

3. 소켓 프로그래밍

1) 소켓 프로그래밍 구조

TCP 서버                  TCP 클라이언트

socket()                 socket()

bind()

listen()

accept()                 connect()

recv()                   send()

send()                   recv()

close()                  close()

2) 기초 및 에코 서버 코드, 숫자 맞추기 게임

echo_server2

import socket  # 소켓 통신 사용하기 위해 기본 내장 모듈 가져옴
import random  # 숫자 맞추기 게임에서 난수 생성하기 위해 사용

# 숫자 맞추기 게임용 2자리 난수 생성
num = random.randint(10, 99)

# s = socket.socket() 은 소켓 객체를 생성하는 코드
# 인자 생략시 socket.AF_INET (IPv4), socket.SOCK_STREAM(TCP)가 디폴트임
# AF_INET6 는 IPv6, SOCK_DGRAM 은 UDP
# with 구문을 사용하면 s.close()가 블록 끝날 때 자동으로 실행.
with socket.socket() as s: 
# =( s = socket.socket() , close() 명시해야함 )은 소켓 객체를 생성하는 코드

    s.bind(("", 9999))  # bind(서버가 사용할 IP주소, 포트번호)
    # bind("",9999) 9999번 포트로 들어오는 서버가 가진 
    모든 네트워크 인터페이스에서 들어오는 연결을 수신하겠다는 의미

    s.listen()  # listen() 몇개의 동시접속을 허용할건지, 1입력하면 하나의 접속만 허용

    print("The echo server start")

    conn, addr = s.accept()  # 클라이언트의 소켓 객체와, 주소 정보를 리턴
    # s는 서버가 접속을 기다리는 소켓이고 conn은 접속한 클라이언트와 통신하는 소켓
    # -> 데이터 송수신은 conn으로 함

    while True:
        recv_data = conn.recv(1024)  
        # 클라이언트가 보낸 데이터 받음, bytes 형태로 최대 1024 바이트까지를 명시

        print("recv >>>> {}".format(recv_data.decode()))  
        # 문자열 지정으로 출력, decode()는 bytes를 문자열로 바꾸는 코드

        if recv_data.decode() == "end":  # 받은 데이터가 end일 경우 연결 종료
            break

        guess = int(recv_data.decode())  
        # 클라이언트가 보낸 숫자를 문자열에서 정수형으로 변환

        if guess < num:
            send_data = "낮음"
        elif guess == num:
            send_data = "정답"
        else:
            send_data = "높음"

        conn.sendall(send_data.encode())  # 결과 문자열을 bytes로 변환해 클라이언트에 전송
        # send() 는 C언어의 시스템콜 함수를 그대로 사용하는 것으로 
        주어진 메시지를 완전히 전송하지 못할 가능성이 있는 저수준 메소드,
        # sendall() 은 파이썬 기반의 고수준 메소드로 
        버퍼 내의 모든 데이터를 전송 (전송완료 여부 확인할 필요 없음)

        print("send >>> {}".format(send_data))  # 전송한 내용 출력

        if send_data == "정답":
            break

    print("echo complete")  # while문이 끝나면 출력 -> client 연결이 끊어진것

echo_client2

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 클라이언트용 소켓 생성

sock.connect(("127.0.0.1", 9999))  # localhost (내 컴퓨터 자신을 의미하는 IP), 9999는 서버가 열어둔 포트 번호로 접속

while True:

    send_data = input("입력: ")  # 사용자 입력

    sock.sendall(bytes(send_data, "utf-8"))  # 문자열을 bytes로 바꿔 서버에 전송

    # 문자열(str)을 bytes로 바꿔 서버에 전송, 사용안할 경우 에러발생. 소켓 통신에서는 bytes형으로 보내야함
    # send_data.encode("utf-8")와 같은 의미

    # 예: send_data = "ABC"
    # send_data.encode() -> b'ABC' (= b"ABC")
    # b'ABC'.decode() -> "ABC"

    if send_data == "end":

        break

    recv_data = sock.recv(1024)  # 서버가 다시 보낸 데이터 수신

    print(f"recv : {recv_data.decode()}")  # bytes를 문자열로 바꿔서 출력

    if recv_data.decode() == "정답":

        break

print("클라이언트 종료")

sock.close()

 

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

1) 송수신 처리 부분

  • 한 대의 컴퓨터에서 송수신을 동시에 처리
    • 실시간 양방향 통신
  • 송신 쓰레드
    • 사용자가 입력한 메시지를 서버로 보냄
  • 수신 쓰레드
    • 서버로부터 오는 메시지를 받아서 화면에 표시

2) 서버가 클라이언트와 연결하는 부분

  • 다중 클라이언트 동시 접속 처리
  • 각 클라이언트 별로 독립된 쓰레드로 동작

3) 주의사항

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

4) 쓰레드 실습

import threading

import time


def thread1():

    for i in range(5):

        print("thread1 +++++"

        time.sleep(0.1)


def thread2():

    for i in range(5):

        print("thread2 +++++"

        time.sleep(0.1)


th1 = threading.Thread(target=thread1)

th2 = threading.Thread(target=thread2)


th1.start()

th2.start()


th1.join()

th2.join()


print("main END!")
  • 프로세스
    • 메모리에 올라가 실행
  • 프로세스는 최소 하나 이상의 쓰레드 가짐
  • OS 스케줄러는 쓰레드들을 스케줄링해서 동시에 동작하는 것처럼 보이게 함
    • 멀티쓰레드
  • 실행할 때마다 다른 결과
  • join()
    • 좀비 프로세스가 되지 않도록, 특정 스레드가 끝날 때까지 메인스레드를 대기시킴
    • 지울 경우, main END가 중간에 출력됨

5) 상속, 오버라이딩 실습

self는 객체 자기 자신을 가리키는 변수임.

클래스 안의 메서드는 객체를 통해 호출되기 때문에, 파이썬은 첫 번째 인자로 그 객체 자신을 자동으로 넘김. 그래서 메서드 정의할 때 첫 번째 매개변수로 self를 적어야 함.

c1 = child1()
c1.dad_look()

이렇게 호출하면 내부적으로는 대략:

child1.dad_look(c1)

처럼 동작한다고 보면 됨.
그래서 메서드 정의가 이렇게 되어야 함.

def dad_look(self):

self를 안 적으면 객체 자신을 받을 자리가 없어서 오류남.


(1). 상속, 오버라이딩 실습 코드

class dad:
    def dad_look(self):
        # self는 현재 이 메서드를 호출한 객체 자신
        print("handsome")

    def doYouKnow(self, lec):
        # lec는 외부에서 전달받는 값
        print(f"I don't know {lec}")


class mom:
    def mom_look(self):
        # mom 클래스의 메서드
        print("pretty")


class child1(dad, mom):  # dad, mom 클래스를 상속
    pass
    # pass는 아무 코드도 작성하지 않고 넘어갈 때 사용
    # child1은 dad와 mom의 메서드를 그대로 사용 가능


class child2(dad, mom):
    def doYouKnow(self, lec):  # 오버라이딩
        # 부모 dad 클래스에도 doYouKnow()가 있지만
        # child2에서 같은 이름으로 다시 정의했으므로 이 메서드가 실행됨
        print(f"I love {lec}")


c1 = child1()
c1.dad_look()  # dad 클래스에서 상속받은 메서드 사용
c1.mom_look()  # mom 클래스에서 상속받은 메서드 사용

c2 = child2()
c2.doYouKnow("python")  # child2에서 오버라이딩한 메서드 실행

실행 결과:

handsome
pretty
I love python

(2). super() 실습 코드

class vehicle:
    def __init__(self, brand):
        # self.brand는 객체 안에 brand 값을 저장하는 것
        self.brand = brand

    def info(self):
        print(f"브랜드: {self.brand}")


class Car(vehicle):
    def __init__(self, brand, doors):
        # 부모 클래스 vehicle의 __init__()을 호출
        # 즉, self.brand = brand 코드를 실행하게 함
        super().__init__(brand)

        # Car 클래스에서 추가로 doors 속성 저장
        self.doors = doors

    def info(self):
        # 부모 클래스 vehicle의 info() 호출
        # 즉, 브랜드 출력 부분 재사용
        super().info()

        # Car 클래스에서 추가한 doors 정보 출력
        print(f"문 개수: {self.doors}개")


c = Car("현대", 4)
c.info()

실행 결과:

브랜드: 현대
문 개수: 4개

 

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

# thread_server.py

def thread_recv(client_socket, addr):
    # 클라이언트 1명과 통신하는 함수
    # 이 함수가 스레드로 실행됨

    while True:
        # 클라이언트가 보낸 데이터 수신
        recv_data = client_socket.recv(1024)

        # 받은 데이터 출력
        # recv_data는 bytes라서 decode()로 문자열 변환
        print("{}에서 보낸 메시지 : {}".format(addr, recv_data.decode()))

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

        # 클라이언트가 end를 보내면 종료
        if recv_data.decode() == "end":
            print("{} 이 종료 됩니다.".format(addr))

            # 클라이언트 소켓 닫기
            client_socket.close()
            break


# 클라이언트와 통신할 스레드 생성
# target: 스레드가 실행할 함수
# args: 함수에 넘길 인자
recv_handler = threading.Thread(
    target=thread_recv,
    args=(conn, addr)
)

# 스레드 시작
recv_handler.start()
# thread_client.py

def send_thread(sock):
    # 서버로 메시지를 보내는 송신 스레드 함수

    while True:
        # 사용자 입력
        send_data = input("입력: ")

        # 문자열을 bytes로 바꿔 서버에 전송
        sock.sendall(bytes(send_data, "utf-8"))

        # end 입력 시 송신 스레드 종료
        if send_data == "end":
            print("종료 메시지 송신함")
            break

        # 3초 대기
        # recv_thread가 응답을 받을 시간 확보
        time.sleep(3)


def recv_thread(sock):
    # 서버에서 오는 메시지를 받는 수신 스레드 함수

    while True:
        # 서버가 보낸 데이터 수신
        recv_data = sock.recv(1024)

        # bytes를 문자열로 변환해서 출력
        print("recv:", recv_data.decode())

        # 서버에서 end를 받으면 종료
        if recv_data.decode() == "end":
            print("수신 종료")

            # 소켓 닫기
            sock.close()
            break


# 송신 스레드 생성
# target=send_thread: send_thread 함수를 스레드로 실행
# args=(sock,): send_thread에 sock 전달
sth = threading.Thread(target=send_thread, args=(sock,))

# 수신 스레드 생성
# target=recv_thread: recv_thread 함수를 스레드로 실행
# args=(sock,): recv_thread에 sock 전달
rth = threading.Thread(target=recv_thread, args=(sock,))

# 송신 스레드 시작
sth.start()

# 수신 스레드 시작
rth.start()

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

 

# thread_client2.py

import socket

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

# 2. 접속 시도
sock.connect(("127.0.0.1", 9999))

while True:
    # 3-1. 데이터 송신
    send_data = input("입력: ")
    sock.sendall(bytes(send_data, "utf-8"))

    # end 입력 시 클라이언트 반복 종료
    if send_data == "end":
        break

    # 3-2. 데이터 수신
    recv_data = sock.recv(1024)

    # 서버가 다시 보내준 데이터 출력
    print(f"recv : {recv_data.decode()}")

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

import socket
import threading
import os

# 현재 서버에 접속한 클라이언트 수
numClient = 0


def thread_recv(client_socket, addr):
    global numClient
    # 전역변수 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 = numClient - 1
            # 현재 접속자 수 감소

            print(f"{addr}이 종료됩니다.")
            print(f"현재 서버에 접속된 클라이언트 수 : {numClient}")

            client_socket.close()
            # 해당 클라이언트 소켓 종료

            if numClient == 0:
                print("서버 종료합니다.")
                os._exit(0)
                # 접속자가 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.....")

while True:
    # 4. 접속 수락
    conn, addr = sock.accept()
    # addr 은 튜플 형태로 (클라이언트 IP 주소, 사용한 임시 포트번호) 형태로 출력됨
    # ex ) ('127.0.0.1', 53214) , 클라이언트 포트 번호는 OS가 자동으로 배정함

    # 클라이언트가 접속할 때마다 현재 접속자 수 증가
    numClient = numClient + 1

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

    # 클라이언트마다 별도의 스레드 생성
    # target=thread_recv : 실행할 함수
    # args=(conn, addr) : thread_recv 함수에 넘길 인자
    # name=addr : 스레드 이름을 클라이언트 주소로 지정
    recv_handler = threading.Thread(
        target=thread_recv,
        args=(conn, addr),
        name=str(addr)
    )

    # 스레드 실행
    recv_handler.start()

8) 라이브러리 이용한 채팅 프로그램

# chat_server2.py

import socketserver


class MyHandler(socketserver.BaseRequestHandler):
    # 유저 관리용 딕셔너리
    # username을 key로 사용하고, (소켓, 주소)를 value로 저장
    users = {}

    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("{} 참여했습니다.".format(username))

        # 현재 채팅 참여 인원 출력
        print("채팅 참여 인원 {}".format(len(self.users)))

        return username

    def delUser(self, username):
        # 유저 삭제
        del self.users[username]

        # 전체 사용자에게 퇴장 메시지 전송
        self.broadcast("{} 퇴장했습니다.".format(username))

        # 현재 채팅 참여 인원 출력
        print("채팅 참여 인원 {}".format(len(self.users)))

    def handle(self):
        # 클라이언트가 접속하면 자동 실행되는 메서드
        print(self.client_address[0])

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

            # addUser() 성공 시 반복 종료
            if self.addUser(username, self.request, self.client_address):
                break

        # 채팅 메시지 처리
        while True:
            data = self.request.recv(1024)

            # 서버 콘솔에 메시지 출력
            print("[{}] {}".format(username, data.decode()))

            # end 입력 시 연결 종료
            if data.decode() == "end":
                self.request.close()
                break

            # 전체 사용자에게 메시지 전송
            self.broadcast("[{}] {}".format(username, data.decode()))

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


class ChatServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass
    # ThreadingMixIn: 클라이언트마다 스레드로 처리
    # TCPServer: TCP 서버 기능 제공


print("chat server start...")

# 서버 생성
# 9999번 포트 사용
# 클라이언트 요청은 MyHandler가 처리
chat_serv = ChatServer(("", 9999), MyHandler)

# 서버 계속 실행
chat_serv.serve_forever()

# 서버 종료 처리
chat_serv.shutdown()
chat_serv.server_close()
# chat_client.py

import socket
from threading import Thread


def recvData(sock):
    # 서버에서 오는 메시지를 계속 받는 함수
    try:
        while True:
            data = sock.recv(1024)
            print(data.decode())

    except ConnectionAbortedError:
        # 서버 연결이 강제로 종료된 경우
        print("서버로부터 연결이 강제로 종료됨")


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

# 2. 접속 시도
sock.connect(("127.0.0.1", 9999))

# 서버 메시지를 받는 스레드 생성
th = Thread(target=recvData, args=(sock,))

# 데몬 스레드 설정
# 클라이언트가 종료될 때 이 스레드도 같이 종료됨
th.daemon = True

# 수신 스레드 시작
th.start()

while True:
    # 3-1. 데이터 송신
    send_data = input("입력: ")

    # 문자열을 bytes로 바꿔 서버에 전송
    sock.send(send_data.encode())

    # end 입력 시 반복 종료
    if send_data == "end":
        break

# 4. 접속 종료
sock.close()

print("클라이언트 종료!")