소켓을 이용한 일대일 채팅 및 파일전송

 

서론

요즘 네트워크(인터넷 포함)을 이용하지 않는 개발은 개발로 쳐주지 않을 정도로 네트워크의 관심과 활용이 높습니다.
그런데 이 네트워크 프로그램은 일반 나홀로프로그램 보다 신경써야 할 부분들이 많습니다.
웹을 통한 프로그램이라면 웹서버와 그 환경들이 잘 갖추어져 있어 밑에 부분들은 신경 쓸것이 없는데 반해, 스스로 서버클라이언트를 구축하는데는 머리털 다 빠지는 일이 한두가지가 아닙니다.
네트워크 프로그램의 특징은 원격지와의 통신이라는 부분이 있는데 이는 우리가 싱글로 프로그램을 개발할 때 보다 더 많은 예외처리를 예상해야 합니다.
일단, "쉽지는 않다. 하지만 누구나 할 수 있다."는 정신을 가지고 진행합니다. (요즘 기술사 준비하고 있는데 이말이 딱 맞습니다. ^^)

이번 강좌는 다음 순서에 의해 진행겠습니다.

1. 컴포넌트로 제공되는 마이크로소프트 Winsock

2. 일대일 채팅
3. 일대일 파일전송

많은 관심 부탁드리며, 혹 거짓말 하더라도 너그러이 용서 바랍니다.

 

1. 컴포넌트로 제공되는 마이크로소프트 Winsock

VB사용자에게는 고마운 것 중의 하나가 마이크로소프트로부터 Winsock 컴포넌트가 제공된다는 사실입니다.
사실 이 컴포넌트는 그다지 훌륭하지는 않다. 하지만 여러모로 쓸모가 많습니다.
이 컴포넌트로 웹서버나, 대현 전문 서버를 구축하겠습니다고 하면 망치 하나로 빌딩을 세우겠다는 생각과 같습니다.
하지만, 간단하게 어떤 정보를 주고 받거나 혹은 원속이 어떻게 생겼는지 궁금해서 한번 해 본다던지 할 때 간단하게 사용할 수 있습니다.
원속은 내부적으로 TCIP/IP 기반의 프로토콜을 지원하면 일반적인 예외처리를 해 놓았기 때문에 네트워크 구조를 잘 몰라도 그냥 가져다 쓸 수 있습니다.


2. 일대일 채팅

소켓으로 연결하려면 한쪽은 서버가 되어야 하고, 한쪽은 클라이언트가 되어야 합니다.
이런저런 예외처리 없이 서로 메시지만 주고 받을 수 있도록 처리하겠습니다.
간단하게 프로세스를 기술하면 (한쪽을 A컴, 다른쪽을 B컴 이라 하겠습니다.)

- A컴에서 원속을 전속을 대기한다. (Listen 메쏘드 사용)

- B컴에서 A컴주소와 통신포트를 입력하고 연결을 수행한다. (Connect 메쏘드 사용)

- A컴에서 B컴으로부터 접속 요청이오면 연결을 한다.

- B컴과 A컴이 연결이 완료되었음으로 서로 대화한다.

우선 새로운 프로젝트를 열고 시작하겠습니다.
그리고 구성요소에서 Microsoft Winsock Control을 추가합니다.

폼을 그림과 같이 구성합니다.

메뉴를 2개 만들고, '다른 컴에서 연결', '연결대기' 그리고 각각의 ID는 'mnuConnect', 'mnuListen'으로 합니다.

다음 텍스트박스 2개를 위와 같이 배열하고
큰건 이름을 'txtMessage'로 하고 멀티라인 'true' 해주고, 스크롤바는 '수직'만 선택합니다.
작은건 이름을 'txtChat'로 합니다.

버튼을 하나 추가하고 캡션을 '전송'으로 하고 이름을 'cmdSend'로 합니다.

도구상자에서 원속()을 더블클릭해서 한개 추가합니다.

이제 코드를 작성하겠습니다.

연결대기 즉 다른 사람으로부터 접속을 대기하는 기능을 만들겠습니다.
서로 통신할때 포트를 정하고,
연결을 대기하기 위해서는 winsock의 Listen이란 메소드를 사용합니다.
다른쪽에서 접속을 요청하면 ConnectionRequest 란 이벤트가 발생합니다.

Private Sub mnuListen_Click()

    ' 혹시 소켓이 연결되어 있을지 모르므로 연결을 절단한다.
    Winsock1.close

    ' 연결대기할 포트를 설정한다. 임으로 1234로 한다.
    Winsock1.Bind 1234

    ' 연결대기한다.
    Winsock1.Listen

End Sub

Private Sub Winsock1_ConnectionRequest(ByVal requestID As Long)

    ' 일대다 통신을 만들때는 접속자마다 원속을 1개씩 더 생성해서 연결해야 하지만,
    ' 이건 일대일 이므로 원속을 1개를 그냥 재사용한다. 재사용하기 위해서는 소켓을 닫는다.
 
    Winsock1.Close

    ' 요청을 소켓에 연결한다.
    Winsock1.Accept requestID

End Sub

여기서, 의야하게 생각할 부분이 외 소켓을 닫는지 일 것인데,  소켓을 닫는 이유는 원래 다중채팅의 경우는 사용자의 요청이 있을 때 마다 소켓을 새로 생성해야서 연결해 주어야 하지만, 왜... 다른 사람이 또 접속을 요청할지 모르기 때문에 대기해야 하므로, 하지만 여기는 연습으로 일대일 채팅만 만드는 것이므로 그렇게 까지는 할 필요가 없어서 그냥 있는거 또 생성안하고 사용합니다.
인자로 넘어오는 걸보면 requestID 란 것이 넘어오는데 이것은 접속요청의 고유한 ID이며, 이 ID가 연결을 위한 키로 동작합니다.
이 ID를 소켓에 붙여주면 서로간의 연결은 끝난
것입니다.

다른 컴으로 부터 접속이 가능해졌습다. 너무 간단하지 않은가.. (^^)

 

다음 단계로 다른컴으로 접속을 요청하는 기능을 만들겠습니다.
접속하는 기능은 우선 접속할 상대의 주소와 포트를 알려주어야합니다.
접속이 되면 connect란 이벤트가 발생합니다.

Private Sub mnuConnect_Click()

    Dim ip As String
    Dim port As String

    ' 접속할 상대의 IP와 포트를 물어본다.
    ip = InputBox("상대방 IP주소를 입력하세요", "접속정보(IP)", Winsock1.LocalIP)
    If ip = "" Then
        '입력한 내용이 없으면 취소한것으로 간주한다.
        Exit Sub
    End If

    ' 접속할 상대의 포트를 물어본다.
    port = InputBox("상대방 port를 입력하세요", "접속정보(포트)", "1234")
    If port = "" Then
        ' 입력한 내용이 없으면 취소한것으로 간주한다.
        Exit Sub
    End If

    ' 상대방에게 접속을 요청합니다.  역시 연결요청하기 전에 소켓을 클리어하고
    Winsock1.Close
    Winsock1.Connect ip, port

End Sub

Private Sub Winsock1_Connect()

    ' 접속이 되었다.
    MsgBox "접속되었습니다."

End Sub


서로간에 대기하고 또는 접속하고 하는 부분이 모두 끝났으니 이제는 서로간에 메세지를 전달하고 전달 받을 수 있도록 해보자.
메세지를 보내기 위해서는 SendData란 메쏘드를 사용하고, 상대로 부터 메세지가 오면 DataArrival 이란 이벤트가 발생합니다.

Private Sub cmdSend_Click()    ' 입력된 메세지를 상대에게 전달한다.
    ' 끝에 갱행신호를 보내는 것은 받는쪽에서 다음내용이 표시될때 개행되도록 하기 위해서 이다.

    Winsock1.SendData txtChat.Text & vbCrLf

    ' 보낸메세지를 화면에 표시한다.
    txtMessage.Text = txtMessage.Text & "보낸메세지> " & txtChat.Text & vbCrLf

    ' 메세지를 전송한 후에는 입력한 메세지를 클리어한다.
    txtChat.Text = ""

End Sub

Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long )

    Dim rMsg As String

    ' 소켓으로 부터 받아온 메세지를 변수에 담아온다.
    Winsock1.GetData rMsg, vbString

    ' 받은 메세지를 화면에 출력한다.
    txtMessage.Text = txtMessage.Text & "받은메세지> " & rMsg

End Sub


이제 일대일 통신을 하기위한 모든 코딩은 완료되었다. 어려운 부분은 없었으리라 생각합니다.
혹시 어렵거나 이해가 되지 않는 부분이 있다면 밑에 컴멘트 달아주세요. (^^ 역시 VB의 생산성은 대단해)

테스트 프로그램을 컴파일하고 2개를 실행합니다.
한쪽은 메뉴에서 '연결대기'를 눌러 연결을 기다리고, 한쪽은 '다른 컴에 연결'을 눌러 서버주소와 포트를 입력하면 2개의 프로그램이 연결됩니다.
그러면 아래쪽에 있는 입력창에 메세지를 입력하고 '전송' 버튼을 누르면 다른 쪽으로 메세지가 전달됩니다.

아래 그림은 두개의 프로그램이 서로 연결되어 메세지를 주고 받는 화면을 갭쳐한것입니다.

 

 

 

3. 일대일파일 전송

앞에는 비교적 간단한 일대일 통신을 구현해 보았습니다.
이번에는 일대일 통신에서 한단계 더 나아가 파일전송이 가능하도록 해보겠습니다.
파일 전송으로 들어가면 여러가지로 복잡한 문제들이 발생합니다.
채팅의 경우는 그냥 스트링이므로 데이타가 변형되던지 아님 반만 오던지 할때 문제가 전혀없습니다.
왜냐하면 사람이 다 알아서 그냥 봐주기 때문이다. 하지만 파일전송이 되면 그때부터는 상황이 전혀 달라집니다.

파일은 바이러리 형태이기 때문에 한 Byte라도 잘못되면 내용에 문제가 생긴다. 실행화일이나 포맷이 정해진 (엑셀이나 아래한글) 문서라면 당연히 깨질것입니다.

또한, 지금 들어오는 데이타가 채팅인지 아님 파일인지도 역시 구별해야합니다.

흠 왠지 어려워 질거 같지 않은가.. 하지만 천천히 따라가다 보면 쉬워집니다.

역시 쉽지는 않습니다. 하지만, 누구나 할 수 있다란 정신을 다시 한번 상기하며 .. 자 가볼깝쇼...

우선 메세지프로토콜을 정의해야합니다.
메세지프로토콜 ???? 이게 뭐지 하는 분들이 있을 것입니다.
메세지프로코톨은 데이타의 시작과 끝을 구분하고 데이타가 무엇에 쓰는 것인지를 알려주는 구조입니다.
어 말이 어렵다하겠지만 차분히 따라오면 알 수 있습니다.

소켓은 동작은 수신측에서 데이타를 보낼 때 Byte형태로 보냅니다. 받는측 역시 데이타가 오면 그냥 Byte단위로 돌려줍니다.
채팅을 위해서라면 대충 그냥 받아서 쭉 보여주면 끝나겠지만, 채팅데이타, 파일데이타가 같이 다닌다면 어떤게 채팅이고 어떤게 파일인지 도대체 알길이 없습니다. 그래서 메세지프로토콜이란 소프트웨어적 데이타구조를 만들고 송신측에서 보낼때 이 구조에 맞게 데이타를 가공해서 보내고, 받는 측 역시 이 구조를 기반으로 수신데이타를 분석하여 구분하는 것입니다.
아직도 어렵다고 생각하는 분들이 있을텐데 그냥 대충 그런가 보다 하고 넘어가다 보면 자연스럽게 아 뭐라 말하기는 어렵지만 이런거구나 하고 알게될 것이니 걱정말고 따라오셩.. ^^  (우잉 사실 설명을 좀 더 쉽게 했었는데 이 놈의 InterDev가 자살하는 바람에 ...)

메세지프로토콜을 정의에 빠져보실랍니까.. 자 그럼 빠져 봅시다.(<-- 안호봉식 멘트)<!--StartFragment-->

&h0E

&h08

&H44

&H13

X

X

X

X

X

X

X

X

T/F

...

헤더

데이타크기

자료

구분

실제 데이타

 

위와 같이 메세지프로토콜을 정의합니다.
보면 데이타의 시작부분은 이게 시작이다 하고 알리는 헤더를 포함합니다. 수신측에서는 데이타가 들어오면 헤더를 찾아서 아 여기서부터가 메세지의 시작이구나 알게 되는 것입니다.
그 다음에 보면 데이타 크기라고 되어 있는데, 이 부분은 뒤에 따라올 실제 데이타가 몇 Byte인지를 나타냅니다. 8 자리로 잡아놓았습니다.
8자리니까 아.. long 형의 크기와 맞추었구나 생각할 수 있는데.. ^^ 원래는 그런식으로 해야 하지만 그러려면 코딩량이 늘어나니까.. 그냥 숫자로 8자리를 표현하는 식으로 하겠습니다.
그럼 8자리 숫자면 일시백천.. 음 99,999,999 크기까지 전송이 가능하겠져.. 대략 95MByte 정도까지 송수신할 수 있네요. ^^
즉 데이타 크기 이후에 오는 실제 데이타의 크기가 얼마다 하고 알려주는 것이라 이거죠.
그럼 프로그램에서는 음.. 뒤에 데이타가 얼마겠군 하고 계속 소켓으로부터 데이타 요청을 하면서 그 크기만큼이 다 수신될 때 까지 기다리고 있겠죠.. 그러다 다 수신이 끝나면 어떤 동작을 하게되는 겁니다.
보면 자료구분이라고 있는데 여기에는 'T', 'F' 중 하나가 오기로 하죠. 'T'면 그냥 채팅이고 'F'면 파일인걸로 가정하는 겁니다.

자 이제 메세지프로토콜 정의도 끝났고 지리한 이론도 끝났으니 홀가분한(?) 마음으로 코딩으로 돌아가 볼까요..

보내는 부분과 받는 부분을 메제지프로토콜 방식으로 수정하도록하겠습니다.

Private Sub SendData(DataType as String, Data As String)

    Dim sData As String 
    Dim bData() As Byte

    ' 헤더를 생성한다.
    sData = ChrB(&HE) & ChrB(&H8) & ChrB(&H44) & ChrB(&H13)

    ' 데이타의 크기를 전송할 데이타에 추가한다.
    sData = sData & StrConv(Format(LenB(Data), "00000000"), vbFromUnicode)

    ' 실제데이타를 추가한다.
    sData = sData & Strconv(DataType, vbFromUnicode) & Data

    ' ASCII문자열을 Byte배열로 바꾸어 전송한다.
    bData = sData
    Winsock1.SendData bData '<-- 그냥 sData를 보내면 VB가 유니코드인줄 알고, ASCII로 변환해서
                            ' 보내게 되는데, 이진화일은 깨짐.

End Sub

Private Sub cmdSend_Click()

    ' 입력된 메세지를 상대에게 전달한다.
    ' 끝에 갱행신호를 보내는 것은 받는쪽에서 다음내용이 표시될때 개행되도록 하기 위해서 이다.

    ' Winsock1.SendData txtChat.Text & vbCrLf
    call SendData("T", Strconv(txtChat.Text & vbCrLf, vbFromUnicode))

    ' 보낸메세지를 화면에 표시합니다.
    txtMessage.Text = txtMessage.Text & "보낸메세지> " & txtChat.Text & vbCrLf

    ' 메세지를 전송한 후에는 입력한 메세지를 클리어한다.
    txtChat.Text = ""

End Sub

위와 같이 SendData란 함수를 추가하고, 빨강부분을 수정합니다.
빨강부분을 보면 전송하는 부분을 함수(SendData)으로 빼 낸 것을 볼 수 있습니다.
코드의 가독성 측면에서 보면 더 보기가 좋죠.

메세지를 전송할 때 SendData란 함수를 호출 하는데 호출할 때 데이타타입을 'T'를 주어서 채팅데이타라고 SendData함수에게 알려주고 있습니다. 나중에 파일 전송을 처리할 때 나오겠지만, 만일 파일로 전송한다면 'F'를 넘기면 되겠죠.
메세지를 전송할 때 ASCII형태로 변환하여 전송하고 있는데, SendData 함수가 ASCII형태 문자열로 동작하도록 코드되어 있기 때문입니다. 이 부분에 대해서는 SendData 함수를 설명할 때 자세히 설명하겠습니다.

Strconv 함수

우리가 알고 있어야 하는 것 중의 하나가 VB는 문자열이 기본적으로 유니코드(Unicode)체계를 따르고 있다는 것 입니다.
유니코드는 다국어를 처리하기 위해 만들어진코드로 영문1글자도 2Byte로 처리가 되어 있습니다.
Strcnv함수는 이런 문자열을 ASCII 형태 또는 유니코드 형태로 변형하도록 기능을 제공해 주고 있습니다.

참고 : MSDN

StrConv 함수

지정된 대로 변환된 Variant(String)값을 반환합니다.

구문

StrConv(string, conversion, LCID)

StrConv 함수 구문은 다음과 같은 <!-- badtag filtered -->명명된 인수로 구성됩니다.

구성 요소 설명
string 필수. 변환될 <!-- badtag filtered -->문자식
Conversion 필수. <!-- badtag filtered -->Integer. 수행될 변환 형식을 지정하는 값들의 합
LCID 선택. 시스템 LocaleID와 LocaleID가 다르면 LocaleID입니다. 기본값은 시스템 LocaleID입니다.

설정

conversion <!-- badtag filtered -->인수 설정은 다음과 같습니다.

상수 설명
vbUpperCase 1 문자열을 대문자로 변환합니다.
VbLowerCase 2 문자열을 소문자로 변환합니다.
VbProperCase 3 문자열 내 모든 단어의 첫 글자를 대문자로 변환합니다.
VbWide* 4* 1바이트 문자를 2바이트 문자로 변환합니다.
VbNarrow* 8* 2바이트 문자를 1바이트 문자로 변환합니다.
VbKatakana** 16** 일본 히라가나 문자를 가다가나 문자로 변환합니다.
VbHiragana** 32** 일본 가다가나 문자를 히라가나 문자로 변환합니다.
VbUnicode 64 시스템의 기본 코드 페이지를 사용하여 문자열을 <!-- badtag filtered -->Unicode로 변환합니다.
VbFromUnicode 128 Unicode 문자열을 시스템의 기본 코드 페이지로 변환합니다.

*이 사항은 극동 아시아 지역의 로케일에만 해당됩니다.

**이 사항은 일본에만 해당됩니다.

메모   이 <!-- badtag filtered -->상수는 Visual Basic for Applications에 지정되어 있습니다. 따라서 사용자의 코드 내 어디서나 사용될 수 있습니다. 상호 배타적인 경우(예: vbUnicode + vbFromUnicode)만 아니면 대부분 통합될 수 있습니다(예: vbUpperCase + vbWide). vbWide, vbNarrow, vbKatakana, vbHiragana 상수는 그들이 적용될 수 없는 <!-- badtag filtered -->로케일에서 사용되면 <!-- badtag filtered -->런타임 오류가 발생합니다.

다음은 적절한 대/소문자 구분을 위해 사용할 수 있는 유효한 단어 구분 기호입니다: <!-- badtag filtered -->Null(Chr$(0)), 수평 탭(Chr$(9)), 라인 피드(Chr$(10)), 수직 탭(Chr$(11)), 폼 피드(Chr$(12)), 캐리지 리턴(Chr$(13)), 공백(SBCS) (Chr$(32)). 나라마다 <!-- badtag filtered -->DBCS에 대한 실제 공백값이 다릅니다.

참고

ANSI 형식에서 Byte 배열을 문자열로 변환하려면 StrConv 함수를 사용하고 Unicode 형식에서 이런 배열을 변환하려면 대입문을 사용합니다.

참고로 아래내용을 디버깅창에서 테스트 해보시면 재미있는 현상을 보실 수 있습니다. (유니코드는 영문자도 2Byte를 할당한다.)

? len("우리는 ABC로 간다.")
12   <== 글자수를 한글이던 영문이던 무조건 1개로 간주해 12이란 숫자가 나왔네요.

? lenb("우리는 ABC로 간다.")
24   <== 글자수를 한글이던 영문이던 무조건 2개로 간주해 24란 숫자가 나왔네요.

? lenb(Strconv("우리는 ABC로 간다.", vbFromUnicode ))
18   <== Strconv를 통해 ASCII 코드로 문장을 변경해서 한글은 2개 영문은 1개로 간주해 우리가 원하는 정확안 Byte를 보여주네요.

? len(Strconv("우리는 ABC로 간다.", vbFromUnicode ))
9    <== 9가 나온 이유는 len의 동작이 lenb로 나온값을 2로 나누기 때문에 9로 나온거죠..

 

SendData 함수를 보면, 우선 SendData란 함수는 기본적인 동작이 ASCII형태의 문자열로 이루어지도록 해 놓았습니다.
이유는 파일과 같이 이진형태로 처리해야 할 경우를 대비해서죠. 그냥 유니코드형태의 문자열을 받게 되면, 이진데이터를 막 VB가 자동으로 변경하면서 여러가지문제를 발생시키게 됩니다. 소켓을 통한 파일전송을 할 때 이런부분은 꼭 염두에 두어야 합니다.(C/C++ 경우에는 기본적으로 ASCII형태로 동작하므로 이런 부분에 고민할 필요가 없는데 VB는 고민을 많이 해야합니다.)
sData란 변수를 만들고 거기에 메세지프로토콜의 구조로 전송할 데이타를 만들고 있습니다.
우선 헤더를 만들어서 붙이고, 거기에 데이타의 크기를 붙이고, 데이타의 종류를 붙이고 데이타를 붙여서 윈속을 통해 전송하고 있습니다.
전송될 데이터를 만들 때 보면 필요에 따라 Strconv 명령을 이용해 문자열의 형태를 바꾸고 있습니다.
헤더 같은 경우는 Chrb란 함수를 이용해 ASCII Code로 만들어 처리하고 있고, 문자열의 길이를 구할때는 Lenb함수를 이용해 이진형태의 크기를 구하고 있죠.
여기서 특이할 점은 Format함수를 이용해 데이터의 크기를 문자열로 바꾸고, 이걸 다시 Strconv함수를 이용해 ASCII 형태로 바꾸고 있음에 주의해야합니다. 이는 소켓을 통해 자료를 전송할 때 이진형태로 전송하기 위함입니다.

그리고 또 한군데 주의해서 보아야 할 곳이 마지막에 Winsock을 통해 SendData 할 때 입니다.
이때 그냥 Winsock1.SendData sData 하게 되면, sData의 형이 String이니까 VB 전송할 때 내부적으로 ASCII 형태로 다시 바꾸어 전송한다는 것입니다. 이미 Ascii 형태의 문자열인데 이걸 다시 ASCII현태로 바꾸게 되니 데이타가 이상해 지는 거죠.
즉, VB는 변수 타입이 String이라고 하면 무조건 UNICODE라고 생각한다는 겁니다.
그래서 bData란 바이트배열을 만들어서 거기에 sData를 할당해주고 전송하고 있습니다. Winsock1.SendData에 값을 B바이트배열을 넘기면 SendData는 변형을 하지 않고 그냥 전송을 합니다.

자. 이제 전송하는 부분은 우리가 만든 프로토콜로 변환하여 전송하도록 바뀌었습니다.

이제 수신부를 수정해 볼까요.

우선 수신부의 전체적이 프로세스의 로직을 나열해 보겠습니다.

- 소켓으로 부터 데이타를 수신한다.
- 수신된 데이타에 헤더위치를 찾는다. (만일, 헤더를 못 찾으면 데이타가 더 수신될 때까지 처리를
  보류한다.)
- 수신된 데이타에서 처리할 데이타 크기를 얻는다. (만일, 데이타크기인 8Byte가 수신되지 않았으면
  처리를 보류한다.)
- 수신된 데이타에서 처리할 데이타타입을 얻는다. (만일, 데이타타입이 수신되지 않았으면, 처리를
  보류한다.)
- 수신된 데이타가 처리할 데이타 크기만큼 모두 수신되었는지 확인하고 수신되었다면, 처리를 하고,
  아니라면 데이타가 모두 수신될 때까지 처리를 보류한다.
- 데이타 수신이 완료되었으면, 수신된 데이타를 파일인지 채팅데이타인지 확인하여 처리한다.

위 로직을 바탕으로 코드를 만들어 보면 아래와 같습니다.

Private mLeftData As String

Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long)

    ' Dim rMsg As String
    Dim rMsg() As Byte

    Dim pos As Long
    Dim size As Long
    Dim dataType As String
    Dim data As String

    ' 소켓으로 부터 받아온 데이타를 변수에 담아온다.
    ' Winsock1.GetData rMsg, vbString ' vbString하면 데이터를 수신할 때 ASCII에서 유니코드 바꾸어
    ' 수신하니 주의바람.
(1) Winsock1.GetData rMsg, vbArray + vbByte ' 바이트배열 형태로 데이터 수신

    ' 소켓으로 부터 받아온 데이타를 기존에 처리하고 남은 데이타에 더한다.
    ' CStr 함수를 사용해 바이트배열을 ASCII형태 문자열로 변형

    mLeftData = mLeftData & CStr(rMsg)

    ' 수신된 데이타에 프로토콜 시작위치인 헤더를 찾는다.
(2) pos = InStrB(1, mLeftData, ChrB(&HE) & ChrB(&H8) & ChrB(&H44) & ChrB(&H13))
    If pos < 1 Then
        ' 헤더가 존재하지 않으로 처리하지 않고 데이타가 더 수신되도록 이벤트를 빠져 나간다.
        Exit Sub
    End If

    ' 헤더를 찾았으로, 이제 받을 데이타의 크기를 확인한다.
(3) If LenB(MidB(mLeftData, pos + 4, 8)) < 8 Then
        ' 헤더 다음에 데이타가 8개가 안되므로 더 데이타를 수신해야 하기 때문에
        ' 이벤트를 빠져 나간다.

        Exit Sub
    End If

    ' 수신된 데이타크기를 숫자로 변환한다.
(4) size = Val(Strconv(MidB(mLeftData, pos + 4, 8), VbUnicode))

    ' 수신될 데이타의 크기를 받았으므로 이제는 데이타의 타입을 확인해 본다.
(5) If LenB(MidB(mLeftData, pos + 4 + 8, 1)) < 1 Then
        ' 데이타크기 다음에 데이타 타입정보가 아직 수신되지 안았기 때문에 이벤트를 빠져나간다.
      
 Exit Sub
    End If

    ' 수신된 데이타 타입을 ASCII에서 문자형(UNICODE)으로 변환한다.
(6) dataType = StrConv(MidB(mLeftData, pos + 4 + 8, 1), vbUnicode)

    ' 이제 필요한 데이타가 모두 수신되었는지 확인하고, 수신이 덜 되었으면,
    ' 더 데이타가 수신되도록 이벤트를 빠져나가고, 수신이 완료되었으면 처리를 한다.

(7)
If LenB(MidB(mLeftData, pos + 4 + 8 + 1, size)) < size Then
       ' 데이타가 덜 수신되었으므로 이벤트를 빠져나간다.
      Exit Sub
    End If

    ' 처리할 데이터를 data변수에 넘기고, 남은 데이터를 mLeftData에 남긴다.
(8) data = MidB(mLeftData, pos + 4 + 8 + 1, size)
    mLeftData = MidB(mLeftData, pos + 4 + 8 + 1 + size)

   ' 수신된 데이타의 타입에 따라 메세면 채팅창에 메세지를 표시하고, 파일이면 저장한다.
(9)    Select Case dataType
        Case "T":
        ' 받은 메세지를 화면에 출력한다.
        'txtMessage.Text = txtMessage.Text & "받은메세지> " & rMsg
        txtMessage.Text = txtMessage.Text & "받은메세지> " & StrConv(data, vbUnicode)

 
       Case "F":
        ' 받은 데이타가 화일이므로 화일저장으로 보낸다.
        Call SaveFile(data)
    End Select

End Sub

헐, 이번 코드는 좀 길죠 ^^
그러나, 천천히 따라오시면 간단합니다.

(1)  Winsock1.GetData rMsg, vbArray + vbByte를 보면 윈속으로부터 바이트배열 형태로 데이터를 수신하고 있는데, 채팅의 경우라면 유니코드형태로 처리되도 아무런 문제가 없지만, 파일전송 기능이 추가되므로 이제는 무조건 ASCII형태로 처리해야 합니다. 그래서 데이터를 수신할 때 바이트배열 형태로 수신하는 것이지요.
다음으로 mLeftData
란 전역변수에 소켓으로부터 수신된 데이타를 추가하고 있습니다.
근데, 왜 mLeftData에 수신된 데이타를 추가할 까요 ? 왜 mLeftData란 전역변수가 필요할 까요 ? 
이것은 소켓의 특징 때문입니다. 우리가 예를 들어 1000Byte를 소켓을 통해 전송한다고 할 때, 소켓 아래에서는 패킷이라는 전송 단위가 있는데 이게 100 Byte라고 가정하면 우리가 데이타를 보내라고 명령하면 100Byte씩 끊어 10개의 패킷을 만들어 전송합니다. 수신을 하는 쪽에서는 소켓에 1패킷이 들어오면 'DataArrival
' 이벤트를 발현하게 됩니다. 그러면 거기서 데이타를 소켓으로 부터 가져와 처리를 하게되는데, 우리는 저쪽에서 1000Byte를 보냈다면 1000Byte를 모두 받아야 처리가 가능하죠.. (왜냐면 프로토콜을 구성하는 모든 데이타가 와 있어야 처리 가능하니까요)
그런데, 1패킷만 받으면 이벤트가 발현됩니다. 이 때는 처리가 불가능하겠죠.. 그러니 일단 수신된 자료를 전역변수에 추가해 놓고 나머지 데이타가 더 수신되기를 기다려야 하는 것 입니다.
mLeftData는 일종의 수신버퍼라고 생각하심 편하실 겁니다.

(2) 수신된 데이타에서 Instrb란 함수를 사용해 헤더를 찾고 있습니다.
여기서 Instrb는 문자를 탐색할 때 Byte 단위로 탐색하란 말입니다. (참고적으로 우리가 알고 있는 문자열 처리 함수 뒤에 B를 붙이면 모두 Byte단위의 문자열 처리도 동작합니다.)
헤더의 위치를 알아야 프로토콜의 시작위치를 알 수 있기 때문이죠
그 다음에 찾은 위치가 1 보다 작으냐고 묻고 있는데 1보다 작다면 헤더문자열을 찾지 못했다는 말이고, 이는 데이타를 더 수신해야 된다는 말이 됩니다. 그래서 Exit Sub 명령을 주어 이벤트를 빠져 나가고 있습니다.
처리에 필요한 데이타가 더 수신되면 그 때 다시 이벤트가 발생할 꺼고 그 때 다시 처리를 하면 되겠지요..
참고적으로 만일 여기서 헤더문자열을 찾지 못하는 경우가 발생한다면, 프로토콜이 1개 이상 로스되었다고 보면 됩니다. 정상적이라면 데이타가 수신되었다고 하니까 헬더가 있어야 할 텐데 헤더가 없다는 것은 네트웍에서 로스가 나고 있다는 말이 됩니다.

(3) 이제 데이타의 크기를 알아야 하겠지요. 데이타의 크기는 헤더 다음에 8Byte로 구성되어 있습니다.
그래서 비교문에서 수신된 데이타에서 8Byte를 꺼내와서 꺼내온 데이타의 크기가 8Byte가 되지 않는다면, 데이타가 아직 다 오지 않았다고 판단하여 이벤트를 빠져나가는 것 입니다. 데이타가 더 수신되기를 기다리는 것이지요.

(4) 8Byte의 데이타가 모두 수신되었으면, 그걸 숫자로 변환하여 크기를 알아내고 있습니다.
여기서 1나 알것은 문자열을 숫자로 변환할 때 val함수를 사용했는데 val함수는 인자가 UNICODE문자열이 와야 정상적으로 숫자로 변환해 줍니다. 그래서 Strconv함수를 이용해서 수신된 데이터크기를 UNICODE문자열로 변환한겁니다.

(5) 프로토콜에서 데이타 크기 다음에 오는 것이 데이타타입이죠. 그래서 수신된 데이타에서 1Byte를 꺼내고 있습니다. 그런데 꺼내온 Byte가 1Byte가 안되면 즉 아직 수신이 되지 않았다면, 역시 이벤트를 빠져나가고 있지요.

(6) 수신된 데이타타입을 문자열로 변환하여 dataType 변수에 할당하고 있습니다. 이 부분에서 문자열(UNICODE)로 변환한 이유는 나중에 데이타 타입을 확인하여 필요한 처리를 할 때 좀 더 편하게 처리하기 위해 변환하는 것입니다. 뭐. 그냥 ASCII로 담아 놓구 나중에 나오는 Select Case 문에서 변환해서 처리해도 상관은 없습니다만 이렇게 하는게 조금 더 편할 겁니다.

(7) 이제 데이타타입도 알았고, 크기도 알았으니, 크기만큼 데이타를 가져오면 되겠죠.
역시 데이타가 size만큼 수신되었는지 확인하고 있습니다. 만일 size보다 데이타가 적으면 이벤트를 빠져나가서 더 수신되기를 기다리고 있습니다.

(8) 처리에 필요한 데이터를 mLeftData에 데이터 크기 만큼 꺼내서 data변수에 할당하고, 혹시 남아 있는 수신데이터가 있을지도 모르기 때문에 mLeftData에 처리한 이후 데이터를 남겨 놓고 있습니다.
여기서 왜 데이터가 남을 수 도 있다고 할까 생각 하실텐데, 이는 또 다른 메세지가 도착했을 경우를 대비하기 위함 입니다.
무슨 소린고 하니, VB의 처리형태 때문입니다. MS Winsock 컴포넌트는 완벽하게 쓰레드로 동작합니다. VB가 뭔가 처리하고 있는 동안에도 계속 데이터를 수신하고 있다는 것이지요. 근데 Winsock1.GetData 부분에서 크기를 지정하지 않고 데이이터를 받고 있는데, 이렇게 하면 소켓에 버퍼링되어 있던 모든 데이터가 다 넘어오게 됩니다. 그럴 때를 대비해서 이렇게 해 놓은 것입니다.

(9) 데이타가 모두 수신되었다면, 데이타타입을 보구 이 데이타를 어떻게 처리할 지 결정합니다.
데이타 타입이 'T'이면 채팅데이타 이므로 채팅창에 메세지로 추가하고 있지요.
만일 'F'라면 SaveFile이란 함수를 호출해 처리하고 있습니다. 이 SaveFile은 짐작이 가시죠.. 수신된 데이타를 파일로 저장하는 코드가 들어가 있겠죠.. SaveFile 함수를 뒷쪽에 구현 코드를 적어 놓겠습니다.

여러분께서 항상 소켓을 통한 자료처리를 하실 때 주의하실점은 여기에 있습니다.
데이타는 항상 여러분이 보낸 크기 단위로 처리되지 않고, 패킷단위로 나누어서 처리된다는 것입니다.
간혹, 소켓을 공부하는 분들 중에 많이 막히는 부분이 이런 패킷이라 개념을 모르고 왜 어떤 때는 이렇고 어떤 때는 이렇다 하면서 머리를 뽑는 분들이 계시는데 이런 개념을 알고 계시면 이제 대머리 될 일은 없겠겠지요 ^^

다음 코드는 SaveFile함수입니다. 뭐 별로 어려운 부분이 없으니 그냥 코드만 보여드리고 넘어가겠습니다.

Private Sub SaveFile(data As String)

    Dim fn As String
    Dim bData() As Byte

    On Error GoTo EXIT001
    With CommonDialog1
        .CancelError = True '<-- 취소를 누르면 오류를 발생시키라는 겁니다.
                            '    오류나면 EXIT001로 갑니다.

        .ShowSave '<-- Save File Dialog를 보여주어라.
        fn = .FileName
    End With
    On Error GoTo 0

    bData = data ' <-- 바이트 배열에 할당하는 이유는 data가 ASCII 형태이기 때문.
    Open fn For Binary Access Write As #1
    Put #1, , bdata '<-- 만일, data를 그냥 저장하면, VB가 친절하게 data가 String이므로 UNICODE라
                    '    가정하고, ASCII형태로 변형하여 저장함. 그럼 데이터가 변형이 생김.
    Close #1

EXIT001:

End Sub

 

기본적인 준비는 다 끝났고 이제 파일보내기 메뉴하나 만들고 파일 보내면 끝나는 군요.
일단, 아래와 같이
UI를 수정하도록 하겠습니다.

UI를 보면 빨강색원으로 칠해진 부분이 새로 추가된 부분이다.
메뉴에 '파일보내기'를 추가하고 ID는 'mnuSendFile'로 합니다.
그리고 파일을 선택할 때 다이얼로그를 사용하기위해 Microsoft Common Dialog Control을 구성요소에서 추가해서 UI에 추가()했다.

이제 파일전송을 하는 부분을 코딩해보죠.
이부분도 별로 어려운 부분이 없어서 그냥 코드만 보여드리고 말겠습니다.

Private Sub mnuSendFile_Click()

    Dim fn As String
    Dim size As Long
    Dim Data() As Byte

    On Error GoTo EXIT001
    With CommonDialog1  ' <-- 열기 다얼로그를 띄어 파일을 선택한다.
        .CancelError = True
        .ShowOpen
        fn = .FileName
    End With
    On Error GoTo 0

    size = FileLen(fn)  ' <-- 파일의 크기를 알아낸다. (바이트 단위로 알려줌)
    ReDim Data(size)    ' <-- 파일크기만큼 바이트배열을 선언
    Open fn For Binary Access Read As #1
    Get #1, , Data      ' <-- 몽땅 읽어 들임.
    Close #1

    Call SendData("F", CStr(Data)) ' <-- 소켓을 통해 전송

EXIT001:
End Sub

자 이제는 테스트만 해보면 됩니다. ^^
항상, 테스트 할 때 느끼는 거지만, VB의 생산성은 정말 대단한거 같습니다.
여담이지만, .NET가면 VB가 엄청 강력해 지는데... 좀 더 어렵고, 불편해진거 같아 아쉽습니다. 물론 더 강력해질 필요가 있어 그랬겠지만, 왠지 VB말 망가트린 느낌이 드네요.. ^^
어떤분은 그냥 C#으로 전향하는게 더 나을거라고 하는 분들도 있더군요.. 저도 차라리 그게 더 나을거라 생각합니다.
사설은 이제 그만하고 테스트 합시다. ^^

우선 컴파일해서 실행파일 만들고 프로그램 2개 실행해서 채팅 테스트 해보구..

파일보내기 메뉴 눌러서 파일 선택해서 보내고, 수신쪽에서 파일이름 줘서 저장해 봅니다.
그리고 수신된 파일이 정상적인가 확인해 보면 되겠지요.. (확인할 때 비주얼스트디오 제공툴인 windiff 같은 쓰면 편합니다.)

이상으로 파일전송까지 마쳤습니다.

여러분도 이제 아시겠지만, 소켓을 이용한 프로그램이 좀 복잡하기는 하지만 소켓의 원리를 조금만 알면 뭐 그리 어려운것 도 아니지요.
요약하겠습니다.
VB를 이용해 소켓으로 채팅을 할 때는 별 신경안쓰고 그냥 대충 해도 잘된다.
파일전송 기능을 구현하려면, 항상 ASCII형태로 자료가 왔다갔다 해야한다.
소켓은 패킷단위로 전송이 이루어진다. 즉, 내가 한번에 데이타를 왕창 보내도, 내부적으로 패킷의 크기로 나누어 여러번 보낸다.
VB는 기본적으로 유니코드로 동작한다. 그러나 소켓은 ASCII형태로 동작한다.

그동안 부족한 글을 읽어주신 여러분께 감사드립니다.

끝으로, 꼭 소스코드 입력해서 실행해 보세요. 안 그러구 눈으로만 읽으면 이해가 되지 않고, 이해된다 하더라도 금방 잊어버립니다.
혹시 이상하거나, 문제가 있음.. 컴맨트 달아 주세요...
다음번에 또 갑자기 글쓰고 싶어지면, 새로운 주제를 가지고 뵙겠습니다.

출처 : [직접 서술] 직접 서술

Posted by housegod
l