ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Socket과 TCPServer를 이용한 TCP/IP 프로그래밍 (작성중)
    언어/파이썬 & 장고 2016. 12. 6. 19:50

    파이썬에는 HTTP, FTP, SMTP등 다양한 프로토콜을 구현한 네트워킹 라이브러리들이 있는데 그 라이브러리들이 공통으로 사용하는 라이브러리가 있습니다. 바로 Socket 모듈입니다. socket모듈은 TCP/IP, UDP/IP를 지원하는 버클리 소켓 인터페이스를 여러가지 함수와 socket클래스를 통해 제공합니다. socket 클래스를 이용하면 거의 모든 인터넷 프로토콜을 구현할 수 있다는 장점이 있지만, 익히고 사용하기가 간단하지 않다는 단점도 있습니다.  그래서 여기서는 socket클래스와 함께 TCPServer 클래스 모듈을 이용한 TCP/IP 프로그래밍을 설명합니다.


    TCPServer 클래스는 이름이 말하는 것처럼 서버 애플리케이션에서 사용하며 클라이언트의 연결 요청을 기다리는 역할을 합니다. TCPServer 클래스가 실이라면 BaseRequestHandler클래스는 바늘이라고 할 수 있습니다. TCPServer 클래스가 serve_forever() 메소드를 통해 클라이언트의 연결 요청을 기다리다가 클라이언트에게서 접속 요청이 오면 이를 수락한 뒤 BaseRequestHandler 객체의 handle() 메소드를 호출합니다. 서버 애플리케이션은 이 handler() 메소드를 재정의해서 클라이언트와 데이터를 주고받는 일을 수행하면 됩니다.

    한편, 서버가 클라이언트의 연결 오청을 수락해서 TCP 커넥션이 만들어지고 나면 그 다음부터는 서버 측의 socket 객체와 클라이언트 측의 socket 객체가 socket.send() 메소드와 socket.recv() 메소드를 통해 데이터를 주고 받을 수 있습니다. 서버 애플리케이션에서는 BaseRequestHandler의 request 데이터 속성이 바로 socket 객체입니다. 데이터를 주고받는 일을 마치고 나서 서버와 클라이언트의 연결을 종료할 때는 socket의 close() 메소드를 호출하면 됩니다.

    다음 그림은 서버와 클라이언트에서 TCP/IP 통신을 수행하기위해 사용하는 TCPServer와 BaseRequestServer, 그리고 socket 클래스의 메소드 호출 흐름을 나타냅니다.



    다음의 표에는 TCPServer와 BaseRequestHandler, socket클래스의 주요 메소드가 정리되어 있습니다. TCPServer와 BaseRequestHandler는 socketserver 모듈에 정의되어 있고, socket은 socket 모듈에 정의되어 있습니다.

    클래스메소드설명
    TCPServerserve_forever()클라이언트의 접속 요청을 수신대기합니다. 접속 요청이 있을 경우 수락하고 BaseRequestHandler의 handle() 메소드를 호출합니다
    BaseRequestHandlerhandle()클라이언트 접속 요청을 처리합니다.


    Socket

    connect()서버에 접속 요청을 합니다.
    send()데이터를 상대방에게 전송합니다.
    recv()데이터를 수신합니다.

    BaseRequestHandler를 상속하는 클래스 예제

    class MyTCPHandler(socketserver.BaseRequestHandler):
    	def handle(self):
    		print(self.client_address[0]) # 클라이언트의 IP 주소 출력
    		buffer = self.request.recv(1024).strip() # 데이터 수신
    		self.request.send (buffer) # 데이터송신

    위 코드에서 BaseRequestHandler로부터 상속을 받은 MyTCPHandler는 handler() 메소드를 재정의합니다. 앞에서 설명한 거서럼 handle() 메소드는 클라이언트의 연결 요청을 서버가 수락했을 때 호출됩니다. 다시 말하면 handle()메소드가 호출됐다는 것은 통신을 수행할 준비가 됐다는 것입니다. handle()메소드 안에서는 통신을 종료하기 전까지 socket클래스의 인스턴스인 request 데이터 속성을 이용하여 데이터를 주고 받으면 됩니다. 이렇게 만든 BaseRequestHandler의 파생 클래스 (MyTCPHandler)는 다음과 같이 TCPServer의 객체를 만들 때 생성자 매개변수로 사용합니다.


    서버 연결 요청 수신 

    server = socketserver.TCPServer(('192.168.100.11',5425),MyTCPHandler) # 첫 번째 인자값은 ip주소와 포트번호로 이루어진 튜플, 두 번째 인자값은 BaseRequestHandler를 상속한 클래스
    server.serve_forever() # server_forever()메소드를 호출하면 클라이언트의 접속 요청을 받을 수 있음

    TCPServer의 인스턴스인 server가 연결 요청 수신을 받을 준비가 되었습니다. 이번엔 클라이언트에서 socket 객체를 생성하고 서버에 연결을 요청하는 코드입니다.

    클라이언트 socket객체 생성 및 연결 요청

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket.bind(('182.168.100.13',0)) # 포트를 0으로 지정하면 OS에서 임의의 번호로 할당함
    socket.connect(('182.16.100.11',5425)) # 서버가 수신대기하고 있는 IP주소와 포트번호를 향해 연결 요청을 수행


    서버는 TCPServer.serve_forever()를 실행하고 있다가 클라이언트에서 위와 같이 연결 요청을 하면 연결을 수락 (이때 연결 또는 커넥션이 만들어졌다고 함)하고, MyTCPHandler.handle() 메소드를 호출합니다. 이후부터는 socket 객체를 이용해서 데이터를 주고받을 수 있습니다. 다음 코드는 클라이언트에서 socket 객체를 이용해서 데이터를 주고 받는 예제입니다.

    클라이언트에서 socket을 사용해 데이터 송수신

    sbuff = bytes(message, encoding='utf-8') # 파일에 읽고 쓸 때와 마찬가지로 텍스트는 네트워크로 송수신할 때도 인코딩 방식을 정하는 것이 좋습니다.
    sock.send(sbuff) # 데이터를 담은 bytes 객체를 send()메소드에 매개변수로 넘깁니다.
    rbuff - sock.recv(slen) # recv() 메소드는 수신할 데이터를 매개변수로 받아 상대방 노드로부터 데이터를 수신합니다. 정확하게 이야기하면 상대방 노드가 아니라 OS가 이미 상대방 노드로부터 OS의 버퍼에 받아놓은 데이터를 읽어오는 것입니다.
    received = str(rbuff, encoding='utf-8')


    다음 예제는 클라이언트가 보내는 메세지를 서버가 받은 뒤, 똑같은 내용을 클라이언트에게 전달하는 예제입니다.

    server

    import socketserver
    import sys
    
    
    class MyTCPHandler(socketserver.BaseRequestHandler):
        def handle(self):
            print('클라이언트 접속: {0} '.format(self.client_address[0]))
            sock = self.request
    
            rbuff = sock.recv(
                1024)  # 데이터를 수신하고 그 결과를 rbuff에 담습니다 rbuff는 bytes 형식입니다.
            received = str(rbuff, encoding='utf-8')
            print('수신 : {0}'.format(received))
    
            # 수신한 데이터를 그대로 돌려보냄
            sock.send(rbuff)  # 수신한 데이터를 그대로 클라이언트에게 다시 송신
            print('송신 : {0} '.format(received))
            sock.close()
    
    
    if __name__ == '__main__':
        if len(sys.argv) < 2:
            print('{0} <Bind IP>'.format(sys.argv[0]))
            sys.exit()
    
        bindIP = sys.argv[1]
        bindPort = 5425  # 임의 서버 포트 지정
    
        server = socketserver.TCPServer((bindIP, bindPort), MyTCPHandler)
    
        print('서버 시작..')
    
        server.serve_forever()  # 클라이언트로부터 접속 요청을 받아들일 준비

    client

    import socket
    import sys
    
    if __name__ == '__main__':
        if len(sys.argv) < 4:
            print('{0} <Bind IP> <Server IP> <Message>'.format(sys.argv[0]))
            sys.exit()
    
        bindIP = sys.argv[1]
        serverIP = sys.argv[2]
        message = sys.argv[3]
    
        sock = socket.socket(socket.AF_INET,
                             socket.SOCK_STREAM)  # SOCK_STREAM은 TCP socket을 뜻함
        sock.bind((bindIP, 0))
    
        try:
            sock.connect((serverIP, 5425))  # 서버에 연결 요청
    
            # 서버로 송신
            sbuff = bytes(message, encoding='utf-8')
            sock.send(sbuff)  # 메시지 송신
            print('송신 {0}'.format(message))
    
            # 서버로부터 수신
            rbuff = sock.recv(1024)  # 메시지 수신
            received = str(rbuff, encoding='utf-8')
            print('수신 : {0}'.format(received))
    
        finally:
            socket.close()


    실행은 커맨드 창을 두 개를 띄워 각 서버와 클라이언트를 실행합니다.


    파일 업로드 프로토콜

    여기서는 FTP를 사용하는 것이 아닌 프로토콜 설계를 해보는 연습을 합니다. 파일 업로드 프로토콜은 헤더와 바디의 두 부분으로 나뉩니다. 바디에는 실제로 전달하고자 하는 데이터를 담고, 헤더에는 본문 길이를 비롯해 메시지의 속성 몇 가지를 담습니다. 바디의 길이는 담는 데이터에 따라 달라지지만 헤더의 길이는 16바이트로 항상 일정합니다. 따라서 수신한 패킷을 분석할 때는 가장 먼저 16바이트를 먼저 확인해서 (바디의 길이를 포함한) 메시지의 속성을 확인하고, 그 다음에 바디의 길이만큼을 또 읽어 하나의 메시지 끝을 끊어내야 합니다. 

    고정길이와 가변길이의 비교

    스트림에서 패킷의 경계를 구분해 내는 일은 TCP 네트워크 프로그래밍에서 필수적입니다. 패킷의 경계를 구분하는 방법은 메시지 포맷을 설계할 때 고려해야 하는데 대표적인 방법이 고정 길이 형식과 가변 길이 형식입니다.

    고정 길이 형식에서는 모든 메시지가 같은 길이를 갖습니다. 16바이트면 16바이트씩만, 32바이트면 32바이트씩만 항상 잘라내는 것입니다. 구현하기는 간편하지만, 이 방식은 대역폭이 낭비될 가능성이 높다는 단점이 있습니다.

    가변길이 형식에는 두 가지 방식을 사용하는데, 메시지를 두 부분으로 나눠 길이가 고정된 앞 부분에 뒷부분의 길이를 기입하는 방식과 메시지를 구분하는 특정 값(' '라든가 캐리지 리턴 등)을 이용하는 방식이 있습니다.

    후자의 경우 텍스트 방식의 통신에 주로 이용되고 전자는 바이너리 통신에 이용됩니다.

    헤더

    다음 표에는 파일 업로드 프로토콜의 헤더가 갖고 있는 각 속성 필드에 대한 설명이 나타나 있습니다.

    필드 이름크기(byte)설명
    MSGID4메시지 식별 번호
    MSGTYPE4

    메시지의 종류

    0x01: 파일 전송 요청

    0x02: 파일 전송 요청에 대한 응답

    0x03: 파일 전송 데이터

    0x04: 파일 수신 결과

    BODYLEN4

    메시지 본문의 길이(단위: byte)

    FRAGMENTED1

    메시지의 분할 여부

    • 미분할: 0x0
    • 분할: 0x1
    LASTMSG1

    분할된 메시지의 마지막인지의 여부

    마지막 아님: 0x0

    마지막: 0x1

    SEQ2메시지의 파편 번호 

    바디

    파일 업로드 프로토콜의 바디는 모두 네 가지입니다. 헤더의 MSGTYPE이 가질 수 있는 값이 모두 네 개(0x01, 0x02, 0x03, 0x04)이므로 바디의 종류도 네 가지로 나뉩니다. 

    먼저 MSGTYPE이 파일 전송 요청(0x01)인 경우의 바디 구조를 보겠습니다. 이 메시지는 클라이언트에서 사용합니다. MSGTYPE 0x01의 바디는 다음 표와 같이 파일의 크기와 파일의 이름으로 이뤄져 있습니다.

    필드 이름크기(byte)설명
    FILESIZE8전송할 파일 크기 (단위: byte)
    FILENAMEBODYLEN - FILESIZE(8 byte)전송할 파일의 이름


    다음 표는 파일 전송 요청에 대한 응답(0x02) 메시지의 바디 구조를 나타냅니다. 이 메시지는 서버에서 사용하며, 클라이언트에서 보낸 파일 전송 요청(0x01) 메시지의 메시지 식별 번호와 같이 결과를 클라이언트에게 전송합니다.

    필드 이름크기(byte)설명
    MSGID4파일 전송 요청 메시지(0x01)의 메시지 식별 번호
    RESPONSE1

    파일 전송 승인 여부

    • 거절: 0x0
    • 승인: 0x1


    파일전송 요청에 대한 응답(0x02) 메시지의 RESPONSE필드가 0x1을 담고 클라이언트에 돌아오면, 클라이언트는 파일 전송을 개시합니다. 클라이언트의 파일은 네트워크 전송에 알맞도록 잘게 쪼개져서 파일 전송 데이터(0x03)메시지에 담겨 서버로 날아갑니다. 이 경우 파일 업로드 프로토콜의 바디는 DATA만 남습니다.

    필드 이름크기(byte)설명
    DATA헤더의 BODYLEN파일 내용


    클라이언트가 마지막 파일 데이터를 전송할 때에는 파일 전송 데이터 메시지 헤더의 LASTMSG 필드에 0x01을 담아 보냅니다. 마지막 파일 전송 데이터 메시지를 수신한 서버는 파일이 제대로 수신됐는지를 확인해서 파일 수신 결과(0x04) 메시지를 클라이언트에 보냅니다. 이때 메시지 바디에는 파일 전송 데이터 (0x03)메시지의 MSGID와 파일 수신 결과가 함께 담깁니다.

    필드 이름크기(byte)설명
    MSGID4파일 전송 데이터(0x03)의 식별번호
    RESULT1

    파일 전송 성공 여부

    • 실패: 0x0
    • 성공: 0x1


    다음은 서버와 클라이언트가 메시지를 주고 받는 과정을 나타낸 그림입니다.


    이제 프로토콜 설계가 끝났습니다.

    파일 업로드 서버와 클라이언트 구현

    해당 프로그램에서는 서버/클라이언트 공용 모듈, 서버, 클라이언트 와 같이 세 부분으로 진행하겠습니다.

    서버/클라이언트 공통 모듈 개발

    파일 업로드 서버와 클라이언트는 모두 파일 업로드 프로토콜을 사용합니다. 즉 파일 업로드 프로토콜을 처리하는 코드를 서버와 클라이언트 양쪽에 공유를 하기 위해 클래스 라이브러리로 개발하면 됩니다. 다음 코드는 프로토콜에서 사용할 각종 상수와 메시지의 구조를 나타내는 Message 클래스를 정의하는 message 모듈입니다.

    # 메시지 타입(MSGTYPE) 상수 정의
    REQ_FILE_SEND = 0x01
    REP_FILE_SEND = 0x02
    FILE_SEND_DATA = 0x03
    FILE_SEND_RES = 0x04
    
    # 파일 분할 여부(FRAGMENTED) 상수 정의
    NOT_FRAGMENTED = 0x00
    FRAGMENTED = 0x01
    
    # 분할된 메시지의 마지막 여부(LASTMSG) 상수 정의
    NOT_LASTMSG = 0x00
    LASTMSG = 0x01
    
    #파일 전송 수락 여부 (RESPONSE) 상수 정의
    ACCEPTED = 0x00
    DENIED = 0x01
    
    #파일 전송 여부(RESULT) 상수 정의
    FAIL = 0x00
    SUCCESS = 0x01
    
    class ISerializable:
        def GetBytes(self): # 메시지, 헤더, 바디는 모두 이 클래스를 상속합니다. 즉, 이들은 자신의 데이터를 바이트 배열로 변환하고 그 바이트 배열의 크기를 반환해야 합니다.
            pass
        
        def GetSize(self):
            pass
        
    class Message(ISerializable): # Message 클래스는 ISerializable로부터 상속을 받은 Header와 Body로 구성됩니다.
        def __init__(self):
            self.Header = ISerializable()
            self.Body = ISerializable()
            
        def GetBytes(self):
            buffer = bytes(self.GetSize())
            header = self.Header.GetBytes()
            body = self.Body.GetBytes()
            
            return header + body
        
        def GetSize(self):
            return self.Header.GetSize() + self.Body.GetSize()


    다음은 메시지의 헤더를 나타내는 message_header.py입니다.

    from message import ISerializable
    import struct
    
    
    class Header(ISerializable):
        def __init__(self):
            self.struct_fmt = '=3I2BH'  # 3 unsigned int, 2 byte, 1 unsigned short
            self.struct_len = struct.calcsize(self.struct_fmt)
    
            if buffer != None:
                unpacked = struct.unpack(self.struct_fmt, buffer)
    
                self.MSGID = unpacked[0]
                self.MSGTYPE = unpacked[1]
                self.BODYLEN = unpacked[2]
                self.FRAGMENTED = unpacked[3]
                self.LASTMSG = unpacked[4]
                self.SEQ = unpacked[5]
    
        def GetBytes(self):
            return struct.pack(
                self.struct_fmt,
                *(
                    self.MSGID,
                    self.MSGTYPE,
                    self.BODYLEN,
                    self.FRAGMENTED,
                    self.LASTMSG,
                    self.SEQ
                )
            )
    
        def GetSize(self):
            return self.struct_len


    아래는 메시지 본문(body)를 표현하는 클래스입니다. 파일 전송 요청(BodyRequest), 파일 전송 요청에 대한 응답(BodyResponse), 파일 전송 데이터(BodyData), 파일 수신 결과(BodyResult), 모두 네 가지 클래스를 구현하는 message_body.py 모듈입니다.

    from message import ISerializable
    import message
    import struct
    
    
    class BodyRequest(ISerializable):
        """
        파일 전송 요청 메시지(0x01)에 사용할 본문 클래스. FILESIZE와 FILENAME 데이터 속성을 가짐
        """
        def __init__(self, buffer):
            if buffer != None:
                slen = len(buffer)
    
                # 1 unsigned long long, N character
                self.struct_fmt = str.format('=Q{0}s', slen-8)
                self.struct_len = struct.calcsize(self.struct_fmt)
                if slen > 4: # unsigned long long의 크기
                    slen = slen - 4
    
                else:
                    slen = 0
    
                unpacked = struct.unpack(self.struct_fmt, buffer)
    
                self.FILESIZE = unpacked[0]
                self.FILENAME = unpacked[1].decode(encoding='utf-8').replace('\x00','')
            else:
                self.struct_fmt = str.format('=Q{0}s',0)
    ...


    댓글