19.04.24 NIO 기반 입출력 및 네트워킹-2

Back-End/Java 2019. 4. 25. 01:50
728x90
반응형

-  서버소켓 채널과 소켓 채널의 용도 -


이 두 채널은 IO의 ServerSocket과 Socket에 대응되는 클래스로, IO가 버퍼를 사용하지 않고

블로킹 입출력 방식만 지원한다면 ServerSocketChannel, SocketChannel은 버퍼를 이용하고 블로킹과 넌블로킹 방식을 모두 지원한다.

사용방법은 ServerSocketChannel은 클라이언트 SocketChannel의 연결 요청을 수락하고 통신용 SocketChannel을 생성한다.


서버를 개발하려면 ServerSocketChannel 객체를 얻어야 한다.

ServerSocketChannel은 정적 메소드인 open( )으로 생성하고, 블로킹 방식으로 동작시키기 위해 configureBlocking(true)

메소드를 호출한다.

기본적으로 블로킹 방식으로 동작한다.

포트에 바인딩하기 위해서는 bind( ) 메소드가 호출되어야 한다.

포트 정보를 가진 InetSocketAddress 객체를 매개값으로 주면 된다.

포트 바인딩까지 끝났다면 ServerSocketChannel은 클라이언트 연결 수락을 위해 accept( ) 메소드를 실행해야 한다.

accept( ) 메소드는 클라이언트가 연결 요청을 하기 전까지 블로킹되기 때문에 UI 및 이벤트를 처리하는 스레드에서 accept( ) 메소드를

호출하지 않도록 한다.

클라이언트가 연결 요청을 하면 accept( )는 클라이언트와 통신할 SocketChannel을 만들고 리턴한다.


 

  블로킹이란?

  스레드가 대기 상태가 된다는 뜻이다.


  바인딩이란?

  컴퓨터 프로그래밍에서 각종 값들이 확정되어 더 이상 변경할 수 없는 구속(bind) 상태가 되는 것


  포트란? 

  모뎀과 컴퓨터 사이에 데이터를 주고받을 수 있는 통로 




 

  SocketChannel socketChannel = serverSocketChannel.accept( );




연결된 클라이언트의 IP와 포트 정보를 알고 싶다면 SocketChannel의 getRemoteAddress( ) 메소드를 호출해서

SocketAddress를 얻으면 된다. 실제 리턴되는 것은 InetSocketAddress 인스턴스이므로 타입 변환할 수 있다.


 

  InetSocketAddress socketAddress = (InetSocketAddress) socketChannel.getRemoteAddress( );




InetSocketAddress에는 다음과 같이 IP와 포트 정보를 리턴하는 메소드들이 있다.


리턴 타입

 메소드명(매개 변수)

 설명

 String

 getHostName( )

 클라이언트 IP 리턴

 int

 getPort( )

 클라이언트 포트 번호 리턴

 String

 toString( )

 "IP : 포트번호" 형태의 문자열 리턴



더 이상 클라이언트를 위해 연결 수락이 필요 없다면 ServerSocketChannel의 close( ) 메소드를 호출해서 포트를 언바인딩 시켜야 한다.

그래야 다른 프로그램에서 해당 포트를 재사용할 수 있다.


 

  serverSocketChannel.close( );





- 예제 및 출력 결과 -


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
package com.hs.chap19;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
 
public class ServerExample {
    public static void main(String[] args) {
        ServerSocketChannel serverSocketChannel = null;
 
        try {
            serverSocketChannel = ServerSocketChannel.open(); // open() 메소드를 사용해 서버 소켓 채널 얻기
            serverSocketChannel.configureBlocking(true); // 블로킹 방식으로 동작시키기 위해 호출한다. 이걸 안써도 기본적으로 블로킹 방식으로 동작하지만 //명시적으로
                                                            // 설정하는 이유는 넌블로킹과 구분하기 위해서이다.
            serverSocketChannel.bind(new InetSocketAddress(5001)); // bind()메소드의 매개값으로 5001번 포트를 줘서 바인딩 시킨다.
            while (true) {
                System.out.println("[연결 기다림]");
                SocketChannel socketChannel = serverSocketChannel.accept(); // 연결 수락을 위해 accept() 메소드를 호출
                InetSocketAddress isa = (InetSocketAddress) socketChannel.getRemoteAddress(); // getRemoteAddress() 메소드를
                                                                                                // 호출해 IP와 포트정보를 얻고,
                                                                                                // 타입변환을한다.
                System.out.println("[연결 수락함]" + isa.getHostName());
            }
        } catch (Exception e) {
    }
        if (serverSocketChannel.isOpen()) { // ServerSocketChannel이 열려있을 경우
            try {
                serverSocketChannel.close(); // ServerSocketChannel 닫기
            } catch (IOException e1) {
            }
        }
    }
}
 
cs




- 소켓 채널 생성과 연결 요청 -


SocketChannel은 정적 메소드인 open( ) 으로 생성하고, 블로킹 방식으로 동작시키기 위해 configureBlocking(true) 메소드를 호출한다.

기본적으로 블로킹 방식으로 동작하지만, 명시적으로 설정하는 이유는 넌블로킹과 구분하기 위해서이다.

서버 연결 요청은 connect( ) 메소드를 호출하면 되는데, 서버 IP와 포트 정보를 가진 InetSocketAddress 객체를 매개값으로 주면 된다.

connect( ) 메소드는 연결이 완료될 때까지 블로킹되고, 연결이 완료되면 리턴된다.

다음은 로컬 PC의 5001 포트에 바인딩된 서버에 연결을 요청하는 코드이다.


 

  SocketChannel socketChannel = SocketChannel.open( );

  socketChannel.configureBlocking(true);

  socketChannel.connect(new InetSocketAddress("localhost", 5001));





- 예제 및 출력 결과 -


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
package com.hs.chap19;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
 
public class ServerExample {
    public static void main(String[] args) {
        ServerSocketChannel serverSocketChannel = null;
 
        try {
            serverSocketChannel = ServerSocketChannel.open(); // open() 메소드를 사용해 서버 소켓 채널 얻기
            serverSocketChannel.configureBlocking(true); // 블로킹 방식으로 동작시키기 위해 호출한다. 이걸 안써도 기본적으로 블로킹 방식으로 동작하지만 //명시적으로
                                                            // 설정하는 이유는 넌블로킹과 구분하기 위해서이다.
            serverSocketChannel.bind(new InetSocketAddress(5001)); // bind()메소드의 매개값으로 5001번 포트를 줘서 바인딩 시킨다.
            while (true) {
                System.out.println("[연결 기다림]");
                SocketChannel socketChannel = serverSocketChannel.accept(); // 연결 수락을 위해 accept() 메소드를 호출
                InetSocketAddress isa = (InetSocketAddress) socketChannel.getRemoteAddress(); // getRemoteAddress() 메소드를
                                                                                                // 호출해 IP와 포트정보를 얻고,
                                                                                                // 타입변환을한다.
                System.out.println("[연결 수락함]" + isa.getHostName());
            }
        } catch (Exception e) {
        }
        if (serverSocketChannel.isOpen()) { // ServerSocketChannel이 열려있을 경우
            try {
                serverSocketChannel.close(); // ServerSocketChannel 닫기
            } catch (IOException e1) {
            }
        }
    }
}
 
cs

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
package com.hs.chap19;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
 
public class ClientExample {
 
    public static void main(String[] args) {
        SocketChannel socketChannel = null;
 
        try {
            socketChannel = SocketChannel.open(); // SocketChannel을 open()메소드를 사용해 생성
            socketChannel.configureBlocking(true); // 블로킹 방식으로 동작시키기 위해 configureBlocking 메소드를 호출
            System.out.println("[연결 요청]");
            socketChannel.connect(new InetSocketAddress("localhost"5001)); // localhost 5001포트로 연결요청을 한다.
            System.out.println("연결 성공");
        } catch (Exception e) {
        }
        if (socketChannel.isOpen()) {
            try {
                socketChannel.close();
            } catch (IOException e1) {
            }
        }
    }
}
 
 
cs



- 소켓 채널 데이터 통신 -


클라이언트가 연결 요청 (connect( )) 하고 서버가 연결 수락 (accept()) 했다면, 양쪽 SocketChannel 객체의 read( ), write( ) 메소드를 호출해서 데이터통신

을 할 수 있다. 이 메소드들은 모두 버퍼를 이용하기 때문에 버퍼로 읽고, 쓰는 작업을 해야한다.

다음은 SocketChannel의 write( ) 메소드를 이용해서 문자열을 보내는 코드이다.


 

  Charset charset = Charset.forName("UTF-8");

  ByteBuffer byteBuffer = charset.encode("Hello Server");

  socketChannel.write(byteBuffer);




다음은 SocketChannel의 read( ) 메소드를 이용해서 문자열을 받는 코드이다.


 

  ByteBuffer byteBuffer = ByteBuffer.allocate(100);

  int byteCount = socketChannel.read(byteBuffer);

  byteBuffer.flip( );

  Charset charset = Charset.forName("UTF-8");

  String message = charset.decode(byteBuffer).toString( );



- 예제 및 출력 결과 -

 

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 com.hs.chap19;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
 
public class ServerExample {
    public static void main(String[] args) {
        ServerSocketChannel serverSocketChannel = null// 소켓채널에 null값을 준다. 값을 담아야하기 때문
        try {
            serverSocketChannel = ServerSocketChannel.open(); // open() 메소드를 사용해 SocketChannel객체를 얻는다.
            serverSocketChannel.configureBlocking(true); // 블록킹방식으로 동작하기 위해 configureBlocking(true)호출
            serverSocketChannel.bind(new InetSocketAddress(5001)); // 새로운 5001번 로컬호스트를 연결 시킨다.
            while (true) {
                System.out.println("[연결 기다림]");
                SocketChannel socketChannel = serverSocketChannel.accept(); // 클라이언트 연결 수락을 위해 accept() 메소드 실행
                InetSocketAddress isa = (InetSocketAddress) socketChannel.getRemoteAddress(); // socketChannel의 IP주소와 포트
                                                                                                // 정보를 isa에 저장한다.
                System.out.println("[연결 수락함]" + isa.getHostName());
 
                ByteBuffer byteBuffer = null// 바이트버퍼에 null값을 준다. 값을 담아야하기때문
                Charset charset = Charset.forName("UTF-8"); // 매개값으로 주어진 문자셋을 담아놓는다. (문자열을 전송하기위해)
 
                byteBuffer = ByteBuffer.allocate(100); // 바이트버퍼에 100개의 공간을 새로 만들어 저장.
                int byteCount = socketChannel.read(byteBuffer); // 바이트버퍼의 수만큼 읽어서 카운트 변수에 저장
                byteBuffer.flip(); // position의 위치를 0번인덱스로 설정, (0이어야 ByteBuffer의 첫 바이트 부터 저장되기 때문)
                String message = charset.decode(byteBuffer).toString(); // 바이트버퍼를 디코딩해서 string타입으로 변환한다
                System.out.println("[데이터 받기 성공]: " + message);
 
                byteBuffer = charset.encode("Hello Client");
                socketChannel.write(byteBuffer);
                System.out.println("[데이터 보내기 성공]");
            }
        } catch (Exception e) {
        }
 
        if (serverSocketChannel.isOpen()) {
            try {
                serverSocketChannel.close();
            } catch (IOException e1) {
            }
        }
 
    }
}
 
cs

 

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
package com.hs.chap19;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
 
public class ClientExample {
    public static void main(String[] args) {
        SocketChannel socketChannel = null// 소켓채널에 null값을 준다. 값을 담아야하기 때문
        try {
            socketChannel = SocketChannel.open(); // open() 메소드를 사용해 SocketChannel객체를 얻는다.
            socketChannel.configureBlocking(true); // 블록킹방식으로 동작하기 위해 configureBlocking(true)호출
            System.out.println("[연결 요청]");
            socketChannel.connect(new InetSocketAddress("localhost"5001)); // 새로운 5001번 로컬호스트를 연결 시킨다.
            System.out.println("[연결 성공]");
 
            ByteBuffer byteBuffer = null// 바이트버퍼에 null값을 준다. 값을 담아야하기때문
            Charset charset = Charset.forName("UTF-8"); // 매개값으로 주어진 문자셋을 담아놓는다. (문자열을 전송하기위해)
 
            byteBuffer = charset.encode("Hello Server"); // 위에서 담은 문자셋으로 Hello Server을 변환한다.
            socketChannel.write(byteBuffer); // 바이트버퍼에 담은 문자열을 보낸다.
            System.out.println("[데이터 보내기 성공]");
 
            byteBuffer = ByteBuffer.allocate(100); // 바이트버퍼에 100개의 공간을 새로 만들어 저장.
            int byteCount = socketChannel.read(byteBuffer); // 바이트버퍼의 수만큼 읽어서 카운트 변수에 저장
            byteBuffer.flip(); // position의 위치를 0번인덱스로 설정, (0이어야 ByteBuffer의 첫 바이트 부터 저장되기 때문)
            String message = charset.decode(byteBuffer).toString(); // 바이트버퍼를 디코딩해서 string타입으로 변환한다
            System.out.println("[데이터 받기 성공]: " + message);
        } catch (Exception e) {
        }
 
        if (socketChannel.isOpen()) {
            try {
                socketChannel.close();
            } catch (IOException e1) {
            }
        }
 
    }
 
}
 
 
cs

 

 

 

 

read( ) 메소드가 블로킹 해제되고 리턴되는 경우

 

 블로킹이 해제되는 경우

 리턴값

 상대방이 데이터를 보냄

 읽은 바이트 수

 상대방이 정상적으로 SocketChannel의 close( )를 호출

 -1

 상대방이 비정상적으로 종료

 IOException 발생

 

 

- 스레드 병렬 처리 -

 

TCP 블로킹 방식은 데이터 입출력이 완료되기 전까지 read( )와 write( ) 메소드가 블로킹 된다.

만약 애플리케이션을 실행시키는 main 스레드가 직접 입출력 작업을 담당하게 되면 입출력이 완료될 때까지 다른 작업을 할 수 없는 상태가 된다.

그렇기 때문에 클라이언트 연결(채널) 하나에 작업 스레드 하나를 할당해서 병렬 처리해야 한다.

 

 

 

- 스레드풀 -

 

클라이언트 폭증으로 인해 서버의 과도한 스레드 생성을 방지하려면 스레드풀을 사용하는 것이 바람직하다.

스레드풀은 스레드 수를 제한해서 사용하기 때문에 갑작스런 클라이언트의 폭증은 작업 큐의 작업량만 증가시킬 뿐

스레드 수에는 변함이 없기 때문에 서버 성능은 완만히 저하된다.

 

728x90
반응형
: