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("클라이언트 종료!")
'보안' 카테고리의 다른 글
| 정보보안 14주차 - 보안 솔루션 (0) | 2026.06.11 |
|---|---|
| 정보보안 13주차 - 악성코드와 소프트웨어 보안 (0) | 2026.06.10 |
| 정보보안 12주차 - 암호 (0) | 2026.06.03 |
| 정보보안 9주차 - 파이썬 기초 (0) | 2026.05.28 |