본문 바로가기
Python

생소켓으로 비디오 스트리밍 받아오는 과제2

by jennyiscoding 2024. 5. 29.

SERVER

# 필요한 패키지 import
# SERVER
import socket # 소켓 프로그래밍에 필요한 API를 제공하는 모듈
import struct # 바이트(bytes) 형식의 데이터 처리 모듈
import pickle # 객체의 직렬화 및 역직렬화 지원 모듈
import cv2 # OpenCV(실시간 이미지 프로세싱) 모듈

# 서버 ip 주소 및 port 번호
ip = '127.0.0.1'
port = 5000

# 1. 소켓 객체 생성
server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

# 2. 바인딩
server_socket.bind((ip, port))

# 3. 접속 대기 
server_socket.listen(10)

# 4. 접속 수락(클라이언트 연결안되면 여기서 멈춰있음)
client_socket, address = server_socket.accept()

# 수신한 데이터를 넣을 버퍼(바이트 객체)
data_buffer = b""

# calcsize : 데이터의 크기(byte)
# - L : 부호없는 긴 정수(unsigned long) 8 bytes
# data_size = struct.calcsize("L")
data_size = struct.calcsize("Q")

# struct.calcsize("L")은 부호 없는 긴 정수(unsigned long)의 크기를 바이트 단위로 계산하는 함수
# 64비트 아키텍처를 사용하고 있다면 8바이트가 나온다. 

print(f'{data_size} 나는 data_size 8')
print(f'{data_buffer} 나는 data_buffer')

while True:
    # 설정한 데이터의 크기보다 버퍼에 저장된 데이터의 크기가 작은 경우
    # 현재까지 수신된 데이터의 크기가 기대되는 데이터의 크기보다 작을 때까지 계속해서 데이터를 수신하는 무한 루프입니다
    while len(data_buffer) < data_size:
        # 5. 데이터 수신
        data_buffer += client_socket.recv(4096)
        # 이 부분은 클라이언트 소켓에서 최대 4096바이트의 데이터를 수신하여 data_buffer에 추가하는 역할을 합니다
        # recv() 메서드는 소켓으로부터 데이터를 읽어오는 역할

    # 버퍼의 저장된 데이터 분할
    packed_data_size = data_buffer[:data_size]
    # print(f'data_buffer: {data_buffer}')
    print(f'packed_data_size: {packed_data_size}') # \x00\x00T\x0e\x80\x04\x95\x03
    print(f'packed_data_size int로: {int.from_bytes(packed_data_size, byteorder="big")}')
    data_buffer = data_buffer[data_size:]
    # print(f'data_buffer 끝 배열: {data_buffer}')
    
    # struct.unpack : 변환된 바이트 객체를 원래의 데이터로 반환
    # - > : 빅 엔디안(big endian)
    #   - 엔디안(endian) : 컴퓨터의 메모리와 같은 1차원의 공간에 여러 개의 연속된 대상을 배열하는 방법
    #   - 빅 엔디안(big endian) : 최상위 바이트부터 차례대로 저장
    # - L : 부호없는 긴 정수(unsigned long) 4 bytes 
    frame_size = struct.unpack(">Q", packed_data_size)[0]
    print(f'frame_size: {frame_size}') # 23215 출력됨
    
    # 프레임 데이터의 크기보다 버퍼에 저장된 데이터의 크기가 작은 경우
    while len(data_buffer) < frame_size:
        # 데이터 수신
        data_buffer += client_socket.recv(4096)
    
    # 프레임 데이터 분할
    frame_data = data_buffer[:frame_size]
    data_buffer = data_buffer[frame_size:]
    
    print("수신 프레임 크기 : {} bytes".format(frame_size))
    
    # loads : 직렬화된 데이터를 역직렬화
    # - 역직렬화(de-serialization) : 직렬화된 파일이나 바이트 객체를 원래의 데이터로 복원하는 것
    frame = pickle.loads(frame_data)
    
    # imdecode : 이미지(프레임) 디코딩
    # 1) 인코딩된 이미지 배열
    # 2) 이미지 파일을 읽을 때의 옵션
    #    - IMREAD_COLOR : 이미지를 COLOR로 읽음
    frame = cv2.imdecode(frame, cv2.IMREAD_COLOR)
    
    # 프레임 출력
    cv2.imshow('Frame', frame)
    
    # 'q' 키를 입력하면 종료
    key = cv2.waitKey(1) & 0xFF
    if key == ord("q"):
        break

# 6. 접속종료
client_socket.close()
server_socket.close()
print('연결 종료')

# 모든 창 닫기
cv2.destroyAllWindows()

 

CLIENT

import cv2 
import struct 
import pickle 
import socket

ip = '127.0.0.1'
port = 5000

capture = cv2.VideoCapture(0)

capture.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

# 1. 소켓 생성
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket: 
    # 2. 접속
    client_socket.connect((ip, port)) # 서버와 연결
    
    # 메시지 수신
    while True:
        # 프레임 읽기
        retval, frame = capture.read()
        # retval: 프레임을 읽어오는데 성공했는지 여부
        # frame: 읽어온 프레임 자체를 나타내는 이미지 배열(array)
        print(f'{retval}하고 {frame}')
        
        # imencode : 이미지(프레임) 인코딩
        # 1) 출력 파일 확장자
        # 2) 인코딩할 이미지
        # 3) 인코드 파라미터
        #   - jpg의 경우 cv2.IMWRITE_JPEG_QUALITY를 이용하여 이미지의 품질(0 ~ 100)을 설정
        #   - png의 경우 cv2.IMWRITE_PNG_COMPRESSION을 이용하여 이미지의 압축률(0 ~ 9)을 설정
        # [return]
        # 1) 인코딩 결과(True / False)
        # 2) 인코딩된 이미지
        retval, frame = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 25])
        # 이미지를 JPEG 형식으로 인코딩하는 함수
        # jpg: 인코딩할 이미지의 확장자를 나타내는 문자열
        # frame: 인코딩할 이미지를 나타내는 NumPy 배열(numpy array)입니다. 이는 OpenCV에서 읽어온 이미지입니다.
        # JPEG 인코딩 옵션을 나타내는 리스트입니다. 여기서는 이미지의 품질(quality)을 설정합니다. 
        # cv2.IMWRITE_JPEG_QUALITY는 JPEG 압축 품질을 지정하는 상수로, 값은 0에서 100까지 가능합니다. 
        # 높은 값일수록 이미지의 품질이 좋아지지만 파일 크기가 커집니다. 여기서는 90을 지정하여 상대적으로 
        # 고품질의 JPEG 이미지를 생성하도록 합니다.
        
        # dumps : 데이터를 직렬화
        # - 직렬화(serialization) : 효율적으로 저장하거나 스트림으로 전송할 때 데이터를 줄로 세워 저장하는 것
        # 이미지 데이터를 직렬화 19\xa6\xd1@\x0e\xcd\x1b\x8d6\x96\x80?\xff\xd9\x94t\x94b...
        frame = pickle.dumps(frame)
        print(f'전송 프레임 크기 : {len(frame)} bytes') #21884 bytes
        # print(frame) 은 e\xcd\x1b\x8d6\x96\x80?\xff\xd9\x94t\x94b... 이런거 나옴
        
        # sendall : 데이터(프레임) 전송
        # - 요청한 데이터의 모든 버퍼 내용을 전송
        # - 내부적으로 모두 전송할 때까지 send 호출
        # struct.pack : 바이트 객체로 반환
        # - > : 빅 엔디안(big endian)
        #   - 엔디안(endian) : 컴퓨터의 메모리와 같은 1차원의 공간에 여러 개의 연속된 대상을 배열하는 방법
        #   - 빅 엔디안(big endian) : 최상위 바이트부터 차례대로 저장

        # 3. 데이터 송신    
        client_socket.sendall(struct.pack(">Q", len(frame)) + frame)
        # struct.pack(">Q", len(frame)) 부분은 len(frame)을 부호 없는 긴 정수로 변환한 후에 
        # 빅 엔디안(big endian) 방식으로 바이트로 묶은 것

        # 클라이언트 소켓을 통해 이미지 데이터를 서버에 전송하는 코드
        # 이미지 데이터의 길이를 네트워크로 전송하기 위해 패킹하는 과정
        # len(frame): 이미지 데이터의 길이를 구하는 함수로, frame은 이미지 데이터를 나타내는 바이트 배열
        # struct.pack(">L", len(frame))은 이미지 데이터의 길이를 네트워크 바이트 순서에 맞춰 패킹
        # ">L"은 빅 엔디안(big-endian) 방식으로 unsigned long(부호 없는 긴 정수)를 나타내며, 
        # 데이터를 네트워크로 보낼 때 사용하는 표준적인 바이트 순서
        # + frame: 이 부분은 이미지 데이터 자체를 뒤에 이어붙이는 과정입니다. 
        # 앞서 패킹된 이미지 데이터의 길이 정보와 실제 이미지 데이터를 연결하여 전송할 데이터를 완성합니다.
        # Broken pip: sendall() 함수는 한 번에 모든 데이터를 소켓을 통해 전송하며, 데이터가 모두 전송될 때까지 블로킹됩니다.
        # 파이프(또는 소켓)의 한쪽이 데이터를 보내고 있는 동안 다른 쪽이 연결을 끊었을 때 발생

# 메모리를 해제
capture.release()