Hi yoahn 개발블로그

컴퓨터 네트워크 및 실습 - 소켓 프로그래밍 본문

sswu/컴퓨터네트워크

컴퓨터 네트워크 및 실습 - 소켓 프로그래밍

hi._.0seon 2020. 6. 10. 20:52
반응형

1. 네트워크 구조 모델

- 컴퓨터의 역할에 따른 분류

1) 피어-투-피어 구조

- 모든 컴퓨터가 동등하게 요청과 응답이 가능한 구조

- 각 노드가 자원을 분산해서 관리

모든 것이 동등하기 때문에 보안이 좋지 않음

private, 개인적으로 사용하는 용도

내 ip에서 다른 pc에 연결해서 데이터를 주고받을 때 사용

 

2) 클라이언트-서버 구조

- 모든 자원이 서버에 집중

- 가장 일반적인 네트워크 구조

 

장점

- 역할 분리 -> 유지보수 쉬움

- 수정/업그레이드/패치를 클라이언트와 서버가 독립적으로 할 수 있다.

- 보안 유지가 수월함

- 클라이언트의 자원 액세스 권한을 쉽게 제어

- 오랜시간 검증되므로 사용자/개발자 친화적, 사용이 쉽다.

 

단점

- 모든 자원이 서버에 집중됨

-> 트래픽 몰림, 클라이언트 접속이 늘어나면 처리비용 급격히 증가

- 분산형 네트워크에 비해 견고함이 떨어진다.

 

2. 클라이언트-서버 모델과 파이썬 소켓 모듈

- 서버는 클라이언트의 요청을 받아 처리하고 응답을 전송하는 방식

- 하나의 서버와 다수의 클라이언트 통신 기능

 

서버

- 클라이언트의 요청이 접수되면 처리하여 응답을 전송하는 동작

- 클라이언트와 통신을 위해 소켓을 사용

- 소켓

  서버와 클라이언트의 논리적 전송 통로 (전송계층 -> 응용계층으로 가기 위해 왔다갔다 하는 C-S에 대한 논리적인 전송 통로)

  소켓 식별 주소: ( HW 주소(IP) + 포트번호 )

                     IP 주소로 클라이언트를 식별하고 포트번호로 프로세스에 대한 서비스를 식별한다.

 

클라이언트

- 소켓을 사용하여 서버와 연결 설정 (서버의 존재와 주소를 알고 있어야 한다.)

- 데이터 송수신 (요청 -> 응답 수신)

 

- TCP/IP 프로토콜의 네트워크 인터페이스 계층, 네트워크 계층, 전송계층은 운영체제 속에 구현되어 있다.

- 사용자 프로그램전송 계층의 서비스를 받아 동작 == 응용계층 프로그램

- 전송 계층의 서비스를 받는 종단점 == 소켓 -> 인터넷 통신을 위해 소켓을 생성하고 연결해야 함

응용계층 <-소켓-> 전송계층 <-> 네트워크 계층 <-> 네트워크 인터페이스 계층

- 전송계층에 있는 TCP/UDP 소켓 프로그램을 통해서 응용 SW에 대한 서비스를 받을 수 있다.

 

* 응용 프로그램이 연결된 소켓을 식별하기 위해서는 IP주소(컴퓨터식별)와 함께 포트번호가 필요하다.

 

소켓 관련 모듈

socket 모듈

- 동기식 I/O 수행

 

asyncio 모듈

- 코루틴과 이벤트 루프를 사용하여 프로세스를 병행 처리

- 코루틴: 피호출 루틴의 실행이 완전히 끝나기 전에 제어가 호출 루틴으로 돌아갈 수 있으며, 제어가 피호출 루틴으로 돌아왔을 때는 중단된 부분부터 다시 수행이 계속된다.

(호출 루틴과 피호출 루틴이 대등 관계, 호출 루틴과 피호출 루틴의 실행이 상호 이동 가능)

 

select 모듈

- 낮은 수준의 네트워크 프로그램 작성시 사용

- 여러개의 소켓 중에서 읽기, 쓰기, 오류 이벤트가 발생한 소켓 알려줌

- 사용자 프로그램에서 소켓 목록을 조사하여 대기하지 않고 나중에 비동기식으로 한번에 처리할 수 있다.

- socket 모듈과 함께 사용

3. socket 모듈

기능

1) 소켓 생성

2) 연결 설정

3) 데이터 송수신

4) 연결 해지

 

socket 메서드로 소캣 객체를 생성해서 사용

            socket.socket(socket.AF_INET, socket.SOCK_STREAM)

family: 소켓의 주소 유형(AF_INET,,

type: 소켓 유형( SOCK_STREAM ( TCP, 연결위주 ),  SOCK_DGRAM ( UDP, 비연결 전송 )

proto: 항상 0, can 소켓에서만 사용됨

 

 

- 서버 연결 메소드

sock.bind( IP, port ) : 종단점(ip, port)을 소켓과 결합

sock.listen(n) : n개의 클라이언트 연결 대기

sock.accept() : 클라이언트 연결 수용

 

- 클라이언트 연결 메서드

sock.connect() : 서버에 연결 요청, 문제 발생 시 예외 반환

sock.connect_ex() : 문제 발생시 오류 코드 반환

 

- 데이터 송수신 메소드

 메소드기능
TCPsock.recv()TCP 소켓을 통해 메시지 수신. 수신 데이터 반환
sock.recv_into()TCP 소켓을 통해 메시지 수신하여 버퍼에 저장
sock.send()TCP 소켓으로 메시지 전송. 송신 바이트 수 반환
sock.sendall()TCP 소켓으로 메시지를 버퍼에 남기지 않고 모두 전송한다
UDPsock.recvfrom()UDP 소켓을 통해 메시지를 수신한다.
sock.recvfrom_into()UDP 소켓을 통해 메시지 수신하여 버퍼에 저장
sock.sendto()UDP 소켓으로 메시지 송신

4. TCP 소켓 프로그래밍

TCP 소켓 프로그램 순서 (C-S)

 

 

서버

- socket 생성: IP버전과 TCP/UDP 타입을 지정(SOCK_STREAM, SOCK_DGRAM)

- bind: [소켓] --- 연결 --- (주소와 포트번호: 종단점)

- listen() 연결 대기할 클라이언트 수 지정

- accept(): 연결 요청 받으면 연결

    -> 연결된 Client의 소켓, 주소(ip, port) 를 반환

 

클라이언트

- socket 생성: IPv4 / IPv6를 지정, TCP/UDP 타입 지정(SOCK_STREAM/SOCK_DGRAM)

- connect( (주소) ): 주소=('ip주소', port번호) =서버 주소, port 에 연결 요청

 

연결된 클라이언트와 서버는 sendall, send와 recv 를 통해서 메시지를 주고받는다.

 

sys.argv[0] : 파이썬 프로그램에 대한 정보

sys.argv[1] : port 번호

 

 

4.1 TCP 소켓을 이용한 파일 송수신

 파일 송신 프로그램

(서버)

1. TCP 소켓을 생성하고 클라이언트로부터 준비 완료 메시지 받음

2. 전송할 파일의 이름 전송하기

    c_sock.sendall(filename.encode())

3. 파일 열고 내용을 송신

    with open(filename, 'rb') as f:  # rb = 바이너리 읽기 모드로 open (파일 (닫을)종료할 필요 없다.)

         c_sock.sendfile(f, 0)   # f : 파일포인터, 0 : offset

 

파일 수신 프로그램

(클라이언트)

1. 소켓 생성 -> 서버 접속, 준비완료 메시지 전송

2. 파일 이름 수신 -> 'wb' 바이너리 쓰기모드로 파일 열기(생성)

    fn = s_sock.recv(1024).decode() # 파일 이름 받음

    with open('new_'+fn, 'wb') as f:  # 쓰기 모드로 수신 파일 열기

3. 일정한 크기로 파일 내용을 받아 파일에 저장

print('receiving file...')
while True:
    data = s_sock.recv(8192)
    if not data:
    	break
    f.write(data)

5. UDP 소켓 프로그래밍

- 편지를 이용한 통신과 유사

- 비연결형, 연결위한 과정이 따로 없음

- SOCK_DGRAM

송신: sendto

수신: recvfrom

-> UDP 소켓에서 connect 함수를 이용하여 상대방과 먼저 연결하면 send(), recv()를 사용할 수 있다.

 

- 서버는 accept, listen (대기, 연결 허용) 과정이 없고,

   클라이언트는 connect로 연결요청하는 과정이 없음

 

- 서버의 IP 주소와 포트번호만 알면 data를 바로 보낸다.

- 상대방이 받았는지 알 수 없음 -> 비신뢰성

 

 

UDP 소켓 프로그램 순서

 

UDP 서버

- socket 생성: IPv4/IPv6를 지정하고 socket.SOCK_DGRAM으로 udp 소켓 타입으로 생성

- bind : 주소=(ipaddr, port) 를 소켓과 연결

- recvfrom(BUFFSIZE) : 클라이언트가 데이터를 전송하면 data, addr을 반환한다.(수신 메시지, 상대방 주소)

- sendto(data, addr): data를 addr에 보냄 (recvfrom에서 반환된 송신자의 주소)

 

UDP 클라이언트

- socket 생성: IPv4/IPv6, socket.SOCK_DGRAM 의 소켓 생성

- sendto( msg, ( IP주소, port ))

   메시지를 보낼 때 상대방의 IP주소와 port 번호를 지정하여 보낸다.

- recvfrom( BUFFSIZE ): 메시지 수신

    -> data, addr 을 반환 ( 받은 메시지, 상대방 주소 )

 

 

* UDP 서버

- 클라이언트와 연결 설정하지 않음 ---> 여러 클라이언트와 통신 가능

- 전송 도중 데이터 손실을 처리하기 위해 " 정지-대기 방식 "의 오류 제어를 사용할 수 있다.

   ( 수신 측이 정상 수신되면 응답 전송하고, 아니면 안보내서 송신 측이 응답 없으면 재전송 하는 방식

     응답 대기 시간을 설정하여 최대 응답시간이 되면 상대방 종료한 것으로 간주하고 종료 )

 

6. 프레임과 파싱

1) 단편화 Fragmentation

- 큰 데이터를 네트워크에서 전송할 수 있는 작은 단위(프레임)로 나누는 작업

(큰 데이터를 프레임 단위로 나누는 것)

- 프레임 = 다수의 필드로 구성됨

- 수신자가 연속적인 데이터 흐름 속에서 페이로드(실제 데이터 영역, 데이터그램)의 시작과 끝을 찾을 수 있어야 함

 

- 프레임의 길이가 일정하다면 프레임 크기를 아는 것임

           -> 시작점만 찾아서 프레임을 재조립, 원래 메시지 복구 가능

 

- 메시지를 4개로 (단편화) 쪼개서 보냄 -> 재조립

 

▶ 프레임 작업

- 데이터를 분할 & 필드(헤더 정보) 추가

  -> 수식 측에서 데이터를 찾을 수 있도록 구성

- 추가된 정보 = 제어 필드 (데이터에 관련된 정보)

프레임제어필드데이터필드
시작문자 (1)주소 (2)순서번호 (4)길이 (4)페이로드

- 시작문자: 프레임의 시작을 나타내는 특수 문자 (1byte)

- 주소: 수신자를 지정하는 주소 (2byte)

- 순서번호: 프레임 누락, 중복 검사 

    수신측에서 프레임을 받으면 제어필드를 보고 페이로드를 순서번호에 따라 프레임을 이어붙임

- 길이: 가변길이 프레임일 경우 필요

7. 브로드캐스팅과 멀티캐스팅

1) 브로드캐스팅

- 네트워크에 연결된 모든 호스트에게 한번에 동일한 메시지를 전송하는 기법

- UDP 소켓 사용 (특정 연결이 없어도 프로토콜이 메시지를 보낼 수 있음 -> SOCK_DGRAM)

 

- 브로드캐스트 주소를 사용 -> 로컬 네트워크 호스트에게만 전송

IP주소 = 네트워크 주소 + 호스트주소

- 브로드 캐스트 주소 = 네트워크주소 .255

              192.168.0.255   or '<broadcast>'

        -> 네트워크 주소가 192.168.0.xxx 인 모든 컴퓨터에게 전송

 

- 송수신 후, 다시 데이터 송수신이 가능하도록 setsockopt() 함수의 SO_REUSEADDR 옵션 설정

  ( 한 클라이언트의 연결 종료 후 다른 소켓이 주소를 다시 사용할 수 있도록 하는 것) = 주소 재사용

 

- 브로드캐스트 송신 프로그램에서 SO_BROADCAST 옵션 설정

    메시지를 네트워크에 연결된 모든 컴퓨터에게 전송하기 위한 옵션

 

sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)

두번째 인자의 값이 1

2) 멀티캐스팅

- 한번에 특정 그룹의 사용자에게 메시지를 전송하는 방식

- UDP 프로토콜 사용

멀티 캐스트 주소

- 최상위 비트가 '1110'   -> 224.0.0.0 ~ 239.255.255.255 범위

 

- 다른 서브네트워크 사용자에게도 전송 가능함

- 특정 IP 주소를 사용 => 멀티캐스트 그룹을 지정

 

패킷을 멀티캐스트 그룹으로 전송 -> 그룹에 가입된 장치 - 패킷 수신

- 패킷을 라우터로 한번만 전송하면 그룹에 가입된 장치들이 패킷 수신함

 

패킷을 멀티캐스트 그룹으로 전송 -> 그룹에 가입된 장치들이 패킷 수신
멀티캐스팅 송수신 과정

 

- 라우터

 연결된 그룹 가입자들에게 메시지 송신 (패킷 형태로 전송)

 송신자가 멀티캐스트 주소로 msg 보냄

    -> 라우터가 데이터그램을 복사하여 (패킷) 전송

         -> 멀티캐스트 그룹 가입자들이 메시지 수신

 

> 멀티캐스팅 송신 프로그램

- TTL을 설정

  = time to live, 라우터를 거쳐 목적지까지 갈 수 있는 시간

  하나의 라우터를 거칠 때마다 TTL이 감소

  - TTL이 없으면 패킷이 목적지를 찾지 못하면 계속 떠돌아다니게 되어 네트워크가 마비될 수 있다. 이것을 TTL을 사용하여 방지하는 것

sock.setsockopt(IPPROTO_IP, IP_MULTICAST_TTL, TTL)

 

- 멀티캐스트 메시지를 보내는 호스트도 메시지를 받게 됨, 받지 않으려면 IP_MULTICAST_LOOP 옵션을 FALSE로 설정

sock.setsockopt(IPPROTO_IP, IP_MULTICAST_LOOP, False)

 

> 멀티캐스팅 수신 프로그램

- UDP 소켓을 생성하고 멀티캐스트 그룹에 가입  -> 가입해야 메시지 받을 수 있다.

- 멀티캐스트 그룹 주소: 바이너리 표기 사용(바이트 스트림)

 

8. 동시성 소켓 프로그래밍

8.1 서버의 종류

1) 반복 서버

- 위에서 본 서버 프로그램들이 해당됨

- 클라이언트 A가 서버에 연결, 서비스 받고있으면 뒤에 온 클라이언트 B는 A가 끝날 때까지 대기

- 먼저 서비스 받고 있는 클라이언트가 오래 받으면 뒤에 온 클라이언트들의 대기시간은 길어짐

 

2) 병행 서버

- 클라이언트들을 동시에 서비스하는 서버

- 스레드 방식

   클라이언트마다 별도의 스레드 사용: 멀티 스레드

- 이벤트 구동 방식  (비동기 서버)

   이벤트 발생하면 처리하는 것 (접속요청, 데이터 도착 =이벤트)

8.2 TCP 멀티스레드 서버

1) 스레드란?

- 운영체제에 의해 시간이 배분되고 관리되는 프로그램의 실행 단위

- 스택, 데이터 메모리 등 공유

- 스레드에게 함수 실행을 맡김 -> 사용자 개입 필요 없음

- 메인 스레드 -> 클라이언트 연결

     데이터처리 -> 스레드 생성, 서브 스레드에게 맡김

 

2) 멀티스레드 구현 방법

- 소켓 생성 & 결합

                         -> listen, accept -> 연결된 클라이언트의 요청을 처리할 스레드 생성

1. _thread 모듈 사용

- 메인 스레드로 실행되는 main 함수에서 서브 스레드를 생성하고 인자와 함께 핸들러 함수 지정

   _thread.start_new_thread(handler, (clientsock, addr))

- 서브스레드를 생성하고 실행

(서브스레드로 실행할 함수, (함수로 전달할 인자))

 

- 핸들러 함수:

 클라이언트로부터 수신한 데이터를 화면에 표시하고 응답 송신

 핸들러 함수의 실행을 스레드에게 맡기면 사용자 프로그램에서는 데이터 수신을 관리할 필요 X

 

- 서버 프로그램 실행

- 두개 이상의 클라이언트 프로그램 실행

- 첫 번째 클라이언트의 요청 -> 서브스레드 생성 -> 핸들러 함수 실행

      다른 클라이언트의 요청 -> 서브 스레드 생성 -> 핸들러 함수 실행 

(첫번째 실행된 핸들러와 두번째로 실행된 핸들러는 별도로 실행됨)

 

◆ threading 모듈을 이용한 멀티 스레드 구현

2. threading + subclass 사용

threading.Thread 의 파생 클래스를 생성하고 run() 메서드를 재정의

class sub_class(threading.Thread):
    def __init__(self, args):
    	threading.Thread.__init__(self)
        
    def run(self):
    	# 스레드가 시작되면 run 함수가 실행됨

threading.Thread 의 파생 클래스를 정의하고 인수와 함께 파생 클래스 객체를 생성

스레드에서 처리할 내용을 run()메소드에 정의

파생 클래스 객체의 start() 메소드를 사용하여 스레드를 시작한다. t.start()

객체의 데몬 속성 t.daemon = True 로 지정하면 메인 스레드가 종료될 때 서브 스레드도 종료됨

 

3. threading.Thread() 함수 사용

서브스레드가 실행할 함수를 정의하고 스레드로 실행

def handler(argu):
    # 함수 정의
    
t = threading.Thread(target= handler, args=(arguments)) #스레드 객체 생성

t.start() #스레드 시작

- 서브스레드의 핸들러를 정의, threading.Thread 클래스를 사용하여 스레드 객체를 생성한 다음, start() 메소드를 사용하여 서브 스레드를 실행한다. threading.Thread로 스레드 객체를 생성할 때 실행할 함수 이름과 인수를 지정

 

메인 스레드서브 스레드 (handler)



- 새로운 소켓 -> 소켓 리스트에 추가


- 데이터가 없으면 소켓 제거

멀티 스레드 채팅 프로그램

- 어떤 클라이언트가 메시지 송신 -> 모든 클라이언트가 메시지를 수신

 

4. concurrent.future 모듈 사용

 

8.3 UDP 채팅 서버 프로그램

 

- 메시지 수신

- 새로운 클라이언트가 접속하면 목록에 추가

- 수신 메시지 출력

- 발신자를 제외한 다른 모든 클라이언트에게 메시지 송신

 

 

 

 

 

반응형
Comments