Syncope.T-*
article thumbnail
Published 2016. 5. 11. 09:25
Network Programming - Socket BackEnd/Java
728x90

5년전에 VB6.0으로 Socket을 이용한 Real-Time Source Coding을 만들적에 Network Programming 공부를 했습지요...

색다른 경험이였긴 한데, 하면서 가장 인상 깊었던건, 당시 사회에서도 네트워크 프로그래밍이 들어가지 않은 프로그램이면 프로그램 취급도 못받고 그랬었습니다.

요즈음엔 기색이 잘 보이지 않긴한데 ( 앱과 모바일쪽이 강성해졌기 때문이죠! )....

다시 RTSC를 Java로 제작해볼려고 Socket Programming을 시작했습니다.!

당시 만들었던 프로그램 동작 모습입니다.


동작 과정은 이러합니다. 열람 된 파일에 대해서는 동기적인 요소이며 이를 제외한 모든 요소는 비동기적으로 돌아갑니다.

※비동기/동기란? :동기는 수행 과정에 접속한 User가 있으면 다른 User는 대기해야 합니다. 그만큼 안정적이지요. 네트워크 충돌이 일어나지 않습니다! 패킷 손실도 거의 없을 수 밖에요!.

Thread로 따지면 Wait이지요. 비동기는 그 반대입니다. 누구든지 동시에 접근 가능합니다. 그래서. 2명이서 수정중이면, 수정중인 과정을 서로에게 보여주게 할 수도 있지만, 편집을 동시에 해버리면 자료가 날아갈 수도 있겠지요?


모두들 Socket을 이용한 채팅 프로그램을 작성 해 보았을겁니다. String만 보내면 되기 때문에, 아무런 데이터 규약 없이 전송이 가능하지요.

하지만 파일 전송에 있어선 좀 더 규약이 필요합니다. 그 규약이 뭐냐 하니

데이터 프로토콜이라고 불리우는 다음과 같은 표 입니다. 

헤더(Header) 데이터 크기(File Length) 자료 구분(File/Chat/etc) 실제 데이터(Bye형)

데이터의 시작과 끝을 알아서, 어떤 데이터 이며 무엇에 쓰이는지 알려주는 구조이죠.

알고리즘의 구조체를 만들어 두는겁니다. 컴퓨터가 좀 더 능률적인 동작을 하기 위해서요!

꼭! 이 프로토콜을 따르지 않아도 됩니다.

C/6665/asdfafgfdsgdfgfdsgsg

이런 규약을 새로 만들어도 되지만. 현존하는 규약중엔 전자가 확실히 능률적이란 말!

자 위의 데이터 프로토콜을 한 번 살펴보자면

헤더 : 데이터의 시작을 알립니다. 수신 Event에서 데이터가 들어오면 헤더인지 판별하여 여기서부터 시작이구나! 하는걸 체크하는거지요

데이터 크기 : 들어 올 데이터의 크기가 몇인지 적어놓은 것입니다. 변수 선언이 간편하겠지요.

자료 구분 : File이냐 Msg이냐 구분하는 정도입니다.

실제 데이터 : 데이터가 Byte형태로 담긴 구간입니다. 실제 자료입니다. 데이터가 마지막인지 체크하는부분은 코드상에서 설명하겠습니다.

VB에서는 이렇게 나타납니다! 자료는 Devpia garosero 님의 자료를 가져왔습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 
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
    ' 헤더를 찾았으로, 이제 받을 데이타의 크기를 확인한다.
(3If LenB(MidB(mLeftData, pos + 48)) < 8 Then
        ' 헤더 다음에 데이타가 8개가 안되므로 더 데이타를 수신해야 하기 때문에
        ' 이벤트를 빠져 나간다.
        Exit Sub
    End If
    ' 수신된 데이타크기를 숫자로 변환한다.
(4) size = Val(Strconv(MidB(mLeftData, pos + 48), VbUnicode))
    ' 수신될 데이타의 크기를 받았으므로 이제는 데이타의 타입을 확인해 본다.
(5If LenB(MidB(mLeftData, pos + 4 + 81)) < 1 Then
        ' 데이타크기 다음에 데이타 타입정보가 아직 수신되지 안았기 때문에 이벤트를 빠져나간다.
        Exit Sub
    End If
    ' 수신된 데이타 타입을 ASCII에서 문자형(UNICODE)으로 변환한다.
(6) dataType = StrConv(MidB(mLeftData, pos + 4 + 81), vbUnicode)
    ' 이제 필요한 데이타가 모두 수신되었는지 확인하고, 수신이 덜 되었으면,
    ' 더 데이타가 수신되도록 이벤트를 빠져나가고, 수신이 완료되었으면 처리를 한다.
(7If 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
cs

(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 함수를 뒷쪽에 구현 코드를 적어 놓겠습니다.

여러분께서 항상 소켓을 통한 자료처리를 하실 때 주의하실점은 여기에 있습니다.

데이타는 항상 여러분이 보낸 크기 단위로 처리되지 않고, 패킷단위로 나누어서 처리된다는 것입니다.

간혹, 소켓을 공부하는 분들 중에 많이 막히는 부분이 이런 패킷이라 개념을 모르고 왜 어떤 때는 이렇고 어떤 때는 이렇다 하면서 머리를 뽑는 분들이 계시는데 이런 개념을 알고 계시면 이제 대머리 될 일은 없겠겠지요 ^^

VB에서는 패킷으로 들어오는걸 유니코드형식인 VB데이터 타입을 서로 맞춰주어야 하는 경우가 있어 StrConv와 같은 함수를 사용해야 했는데..

여러모로 필요한게 많았지요 위에 소스 처럼 LeftData 처리도 해주어야하고... Garbage Collector가 없는 문제에 대한 것도 말이죠.

자! 이제 자바에서는 어떻게 하는지 봅시다.

Server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package socketTest;
 
import java.net.*;
import java.io.*;
 
public class server {
    public static void main(String[] args) throws IOException {
 
        // 포트 지정 후 서버소켓을 생성합니다.
        ServerSocket serverSocket = new ServerSocket(8080);
        // Server Open 상태로 만들어줍니다.
        Socket socket = serverSocket.accept();
 
        System.out.println("Accepted connection : " + socket);
 
        // 보낼 파일을 선정합니다.
        File transferFile = new File("Desert.jpg");
 
        // 보낼 파일의 크기만큼 바이트 단위로 배열을 선언을 합니다!
        byte[] bytearray = new byte[(int) transferFile.length()];
 
        // 그 파일을 스트림 객체에 담지요.
        FileInputStream fin = new FileInputStream(transferFile);
 
        // 스트림을 버퍼화 시킨 후에
        BufferedInputStream bin = new BufferedInputStream(fin);
 
        // 바이트화 합니다.
        bin.read(bytearray, 0, bytearray.length);
 
        // 보내려는 소켓의 OutputStream을 선언하고
        OutputStream os = socket.getOutputStream();
        System.out.println("Sending Files...");
 
        // 파일을 보냅니다! 직빵으로요! 알아서 패킷단위로 보내집니다! 놀랍죠!
        os.write(bytearray, 0, bytearray.length);
        // 다 보내지면 스트림을 비워 줍니다.
        os.flush();
 
        // 소켓을 닫진 않아도 됩니다. 만약 파일전송용 소켓이면 닫아두어야 겠지요.
        socket.close();
        System.out.println("File transfer complete");
 
        // 바이너리 스트림을 닫아서 memory leak를 방지합니다.
        bin.close();
        // 위와 이유가 같습니다.
        serverSocket.close();
    }
}
cs

Client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package socketTest;
 
import java.net.*;
import java.io.*;
 
public class client {
    public static void main(String[] args) throws IOException {
        /*
         * 파일 사이즈를 선정합니다. 만약 데이터 프로토콜을 지킨다면 들어오는 데이터의 데이터크기를 이 변수에 집어넣어주시면 됩니다.
         * 단위는 바이트 단위입니다.
         */
        int filesize = 1022386;
        int bytesRead;
        int currentTot = 0;
 
        // 소켓을 생성해 접속하고
        Socket socket = new Socket("127.0.0.1"8080);
 
        // 받을 데이터를 담을 변수를 선언합니다. 배열의 사이즈는 받는 크기보다는 커야 합니다.
        byte[] bytearray = new byte[filesize];
 
        // 여기서부터 데이터를 받기 시작합니다.
        InputStream is = socket.getInputStream();
 
        // 받을 파일의 이름을 정합니다.
        FileOutputStream fos = new FileOutputStream("download.jpg");
 
        // Stream을 Buffer화 시켜주고요
        BufferedOutputStream bos = new BufferedOutputStream(fos);
 
        // 바이트화 합니다.
        bytesRead = is.read(bytearray, 0, bytearray.length);
 
        // 현재 받은량을 체크하는겁니다.
        // bytesRead가 is, 즉 받아오는 Socket에서의 데이터 량인데
        // 이게 0 미만이면 받아오는게 없다는 겁니다. 그래서 아래의 While 조건문에 적혀있지요
        currentTot = bytesRead;
        do {
            // 계산법입니다. 3번째의 length가 현재량을 말하는겁니다.쌓이는 bytesRead많큼 계속 빼져지겠지요.
            // 즉, is 객체가 언제 종료될지를 어떻게 아느냐~ 의 로직을 짠 겁니다.
            // 텍스트면 마지막 값이 손실되던가 어떻게 해서든 파일이 만들어 질텐데...
            // 이미지나 영상같은 경우는 이와 같은 로직이 필요하지요!
            bytesRead = is.read(bytearray, currentTot, (bytearray.length - currentTot));
            if (bytesRead >= 0)
                currentTot += bytesRead;
        } while (bytesRead > -1);
        // 다 모아진 데이터를 바이트화 하여 파일을 만드는 거지요.
        bos.write(bytearray, 0, currentTot);
        bos.flush();
        bos.close();
        socket.close();
    }
}
cs


profile

Syncope.T-*

@Syncope

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...