본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Java(자바)

ServerSocketChannel, SocketChannel read write 메소드 사용시 주의해야 할 점

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2022. 12. 15.

 

Java에서 TCP 소켓 서버를 구현하기 위해 사용되는 클래스 중 ServerSocketChannel 클래스가 있습니다.

해당 클래스는 nio 패키지의 일부이며, 해당 클래스를 통해서 TCP 서버를 쉽게 구현 할 수 있습니다.

 

아래 샘플코드는 데이터를 받기 위해서 사용하는 프로세스의 일부분을 간략하게 표현한 코드입니다.

read  메소드를 통해서 데이터를 받을 수 있습니다.

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;

{
    ServerSocketChannel serverSocket = ServerSocketChannel.open(); 

    while(serverSocket.isOpen()){
        SocketChannel sc= serverSocket.accept();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);  //버퍼에 데이터를 담아둡니다.
    }
}

 

위 코드를 실행하면 buff 변수에 데이터가 존재하게 됩니다.

마찬가지로 데이터를 보내기 위해서는 write 라는 메서드를 호출하여 줍니다.

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;

{
    ServerSocketChannel serverSocket = ServerSocketChannel.open(); 

    while(serverSocket.isOpen()){
        SocketChannel sc= serverSocket.accept();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);  //읽기
        
        ByteBuffer result = ByteBuffer.allocate(1024);
        sc.write(result);  //쓰기

    }
}

 

구글링을 해 보면 위와 같은 형태의 코드를 많이 만날 수 있습니다.

아니면 Selector를 활용한 비동기 방식에서의 체널처리가 된 코드를 만나기도 합니다.

 

그런데 이러한 코드를 그대로 쓰다보면 만날 수 있는 최악의 경우는 "난 보냈는데 넌 못받았다" 입니다.

아래 사진을 보도록 하겠습니다.

좌측에서 우측으로!

 

클라이언트 프로그램이 서버로 128byte의 데이터를 전송 하였습니다.

물론 이렇게 데이터가 얼마 되지 않는 경우에는 구글링한 코드들은 문제없이 동작 합니다.

그런데 아래와 같은 경우가 발생하였다고 가정하여 봅니다.

어...바이트가 좀 됩니다?

 

클라이언트 프로그램이 서버로 전달해야되는 바이트의 크기가 짧지 않으면서, 네트워크 환경이 좋지 않는 경우가 있다고 가정하여 봅니다.

이러한 조건 속에서 클라이언트는 서버에게 데이터를 전달했다고 하여 봅니다.

0번째에 10이라는 숫자를, 2400번에는 -10 이라는 숫자를 전송 했다고 가정하여 봅니다.

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;

{
    ServerSocketChannel serverSocket = ServerSocketChannel.open(); 

    while(serverSocket.isOpen()){
        SocketChannel sc= serverSocket.accept();
        ByteBuffer buffer = ByteBuffer.allocate(65536);
        sc.read(buffer);  //버퍼에 데이터를 담아둡니다.
        
        //0번째에 10이라는 숫자를, 2400번에는 -10 이라는 숫자를 전송 했다고 가정하여 봅니다.
        int number = buffer.getInt(0); //0번째의 데이터를 4바이트 가져와서 사용합니다.
        System.out.println(number);  //숫자 10이 출력됩니다.
        
        int number2 = buffer.getInt(2400);  // 2400번째 데이터를 4바이트 가져와서 사용합니다.
        System.out.println(number2); //숫자 뭐가 출력될까요????
        
    }
}

 

number2 변수에서 "숫자 -10" 이 출력 되었다면 정말 운이 좋은 케이스 입니다.

그러나 안타깝게도 이러한 경우에 대부분 값은 동기화 블럭인 경우에는 -1, 비동기인 경우에는 0 값을 반환하게 됩니다.

클라이언트는 "나 데이터 보냈어!" 라고 하지만,

서버는 "나 못받았는데? 0이 왔어!" 라고 응답하며 서로 맞네 틀리네 하며 싸우게(?) 되는 것 입니다.

 

위 사진을 조금 변형한 사진 입니다.

자 이렇게 오고있는 중이라고 생각 해 봅시다!!

 

TCP 통신에서 연결이 끊어지지 않는다면 서버는 클라이언트가 전송하는 모든 데이터를 받을 수 있습니다.

그런데 너무 큰 데이터의를 받아야 하거나 아니면 각종 간섭에 의해서(인터넷 환경이 좋지 않는 등) 인터넷이 느린 경우에 서버는 데이터를 받는 시간이 필요 합니다.

만약 데이터가 아직 다 도착을 안했는데 개발된 코드에서 ByteBuffer값을 바로 호출 한 다면 서버는 일단 ByteBuffer 값이 다 채워지지 않았더라도 전달을 합니다.     * 니가 불렀잖아?

그러므로 서버에서는 다 채워지지 않는 버퍼를 사용 하기 때 문에 0또는 -1 값이 나오게 되는 것 입니다.

왜냐하면 아직 받고있는 중에 사용자가 호출하였으니까요.

이를 비유하자면, 마치 컵에 물을 따르고 있는 중인데 사용자가 컵의 물을 마시기위해 입에 가져다 덴 것과 마찬가지 상황이라 볼 수 있습니다.

 

그러므로 위 코드는 서버는 Bytebuffer 클래스에 할당한 크기인 65536 갯수의 데이터를 받고 싶었지만, 실제로 read 메소드를 1번만 실행하였으므로 얼마나 받은지 모른 상태에서 버퍼클래스 값을 전달하게 되는 것 입니다.

* 다 받을수도 있고 500개만 받을수도 있고..아무도 모릅니다...네트워크 땜시..전부 안줭~

 

다시 아래 코드를 살펴 봅니다.

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;

{
    ServerSocketChannel serverSocket = ServerSocketChannel.open(); 

    while(serverSocket.isOpen()){
        SocketChannel sc= serverSocket.accept();
        ByteBuffer buffer = ByteBuffer.allocate(65536);
        int result = sc.read(buffer);  //버퍼에 데이터를 담아둡니다.
        System.out.println(result);  //result...?
        
        //0번째에 10이라는 숫자를, 2400번에는 -10 이라는 숫자를 전송 했다고 가정하여 봅니다.
        //int number = buffer.getInt(0); //0번째의 데이터를 4바이트 가져와서 사용합니다.
        //System.out.println(number);  //숫자 10이 출력됩니다.
        
        //int number2 = buffer.getInt(2400);  // 2400번째 데이터를 4바이트 가져와서 사용합니다.
        //System.out.println(number2); //숫자 뭐가 출력될까요????
        
    }
}

 

read 메소드를 통해서 result 라는 변수에 값을 담게 하였습니다 (저는 처음에 반환값이 void인 줄 알았습니다.)

read 메소드의 반환값은 Int 형태의 숫자인데 해당 숫자의 의미는 아래와 같습니다.


int java.nio.channels.SocketChannel.read(ByteBuffer dst) throws IOException

Reads a sequence of bytes from this channel into the given buffer. 
An attempt is made to read up to r bytes from the channel,where r is the number of bytes remaining in the buffer, that is, dst.remaining(), at the moment this method is invoked. 

Suppose that a byte sequence of length n is read, where 0 <= n <= r.This byte sequence will be transferred into the buffer so that the firstbyte in the sequence is at index p and the last byte is at index p + n - 1,where p is the buffer's position at the moment this method isinvoked. Upon return the buffer's position will be equal to p + n; its limit will not have changed. 

A read operation might not fill the buffer, and in fact it might notread any bytes at all. Whether or not it does so depends upon thenature and state of the channel. A socket channel in non-blocking mode,for example, cannot read any more bytes than are immediately availablefrom the socket's input buffer; similarly, a file channel cannot readany more bytes than remain in the file. It is guaranteed, however, thatif a channel is in blocking mode and there is at least one byteremaining in the buffer then this method will block until at least onebyte is read. 

This method may be invoked at any time. If another thread hasalready initiated a read operation upon this channel, however, then aninvocation of this method will block until the first operation iscomplete. 

Specified by: read(...) in ReadableByteChannel
Parameters:dst The buffer into which bytes are to be transferredReturns:The number of bytes read, possibly zero, or -1 if thechannel has reached end-of-streamThrows:NotYetConnectedException - If this channel is not yet connectedIOException - If some other I/O error occurs


 

영어라 어렵지만...               파파고..

 

read 메소드를 통해서 반환된 값은 지정된 버퍼값의 남은 길이를 의미 합니다.

그러므로 read 메소드를 통해 반환된 값은 65536 개중 읽은 갯수를 의미하는 것 입니다.

위 코드를 이제 다시 의미 있게 바꾸어 보면,

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;

{
    ServerSocketChannel serverSocket = ServerSocketChannel.open(); 

    while(serverSocket.isOpen()){
        SocketChannel sc= serverSocket.accept();
        ByteBuffer buffer = ByteBuffer.allocate(65536);
        int result = 0;
        while(result >=65536){
            result += sc.read(buffer);  //읽은 갯수가 지정된 버퍼값이 될 때 까지 읽습니다.
        }
        
        System.out.println(buffer);  //65536개가 전부 꽉 차 있는 버퍼가 나왔습니다!
    }
}

 

이렇게 read 메소드를 지정된 버퍼 크기만큼 계속 읽게 반복문을 통해 동작 하도록 바꾸어 준 다면 -1 또는 0 값을 만나지 않게되는 것 입니다!

마찬가지로 write 메소드도 이와 비슷합니다.

내가 지정한 크기의 byte 값이 전달 되었는지 해당 갯수를 반환하기 때 문에 해당 값이 0이 될때 까지 write 메소드를 동작 시켜주도록 해야 합니다.

 

이상으로 ServerSocketChannel 서버에서 SocketChannel 클래스의 read메소드, write메소드 사용시 주의해야 할 점에 대해서 정리하여 보았습니다.

잘못된 부분 또는 궁금한점이 있다면 언제든지 연락주세요!

 

* TCP 서버를 깊게 하지 않아서 내용이 틀릴 수 있습니다. 잘못된 점이 있다면 꼭 피드백 부탁드립니다!

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글