nio 패키지는 기존의 io패키지와는 달리 nonblocking을 지원해 준다. 이런 입출력의 nonblocking은
서버와 클라이언트간의 스레드의 과부하를 덜어주고 높은 성능과 효율성을 가진다. 이런 기능에서 중요한 역활을 하는 것이
SelectableChannel과 Selector 클래스이다. 우선 channels패키지의 전체 흐름을 보자.
java.nio.channels 패키지에는 총 7개의 인터페이스와 이른 구현하는 13개의 클래스로 구성이 되어있다, 인터페이스와 클래스관계가 좀 복잡해 보이지만 중요한 것들만 기억하면 된다. 이들은 다음장에서 구체적으로 살펴본다.
채널은 한마디로 서버와 클라이언트간의 통신수단을 나타낸다. 좀더 깊이 있게 본다면 하드웨어
장비, 파일 ,네트워크 소켓, 혹은 프로그램 컴포넌트와 같이 읽기나 쓰기등 한 개 이상의 뚜렷한 입출력 작업을 수행할 수 있는
개방된 연결을 나타낸다.
2> 채널의 역활
채널은 비동기적으로 닫히고 중단(interrupt)될 수 있다. 따라서 한 스레드가 한 채널에서
하나의 입출력 작업으로 블록화하면 다른 스레드가 그 채널을 닫을 수도 있다. 비슷하게 한 스레드가 한 채널의 입출력 작업으로
블록화되면 다른 스레드가 블룩화된 스레드를 중단시킬 수 있다. 그래서 파일입출력에서 블록화된 스레드를 언제든지 중지시킬 수 있게
되었으며, 이를 이용해서 네트워크에서 non-blocking 입출력이 가능해졌다.
3> 채널의 특징
- 읽기 쓰기를 동시에 할 수 있다.
- Buffer 클래스를 사용하므로 데이터형에 맞는, 전용 메모리 공간을 가지고 있다.
- 블로킹된 스레드를 깨우거나 중단시킬수 있다.
nio의 non-blocking 입출력 메카니즘
nio의 non-blocking 입출력 메카니즘은 selectors와 channels를 근간으로
구축되었다, channel 클래스는 서버와 클라이언트간의 통신 메커니즘을 나타내고 selector 클래스는 클라이언트로부터
들어오는 클라이언트 요청을 나누어 각 요청에 해당하는 각각의 처리자에게 이들을 보낸다. Channel 클래스와 Selector
클래스의 각각의 기능과 이들이 협력하여 자바 플랫폼에 대한 nonblocking 입출력 메커니즘을 생성한다. 아직은 뭔지 감이 오지 않을 것이다. 당연하다. 다급해 하지말고 이제부터 시작해 보자.
9. java.nio.channels 인터페이스
이번장에서는 java.nio.channels 패키지에 정의되어있는 7개의 인터페이스를 살펴보자.
이들 인터페이스는 채널관련 클래스들이 주로 사용하는 메서드를 정의, 채널 클래스류에게 제공해 준다. 따라서 인터페이스와 그에
따를 메서드를 반드시 알아두는 것이 좋다.
1> Channel 인터페이스 --> 모든 채널 클래스류의 최상위 인터페이스
먼저 Channel이라는 기본적인 인터페이스가 있다. 이는 모든 인터페이스들이 상속을 받으므로 Channel 인터페이스가 가진 메서드는 모드 사용할 수 있다. 메서드는 다음과 같다.
ㅁ public boolean isOpen() : 현재의 채널 상태가 오픈일지 아닌지를 판단, 리턴. 현재의 채널이 오픈일 경우 true 리턴.
ㅁ public void close() throws IOException : 현재의 채널을 닫는다.이때 닫힌 채널로 입출력 조작을 실시하려고 하면ClosedChannelException 가 발생. 채널이 닫힌 상태에서 이 메소드를 호출해도, 아무런 효과도 없다.
2> InterruptibleChannel 인터페이스 --> 블로킹된 스레드(입출력)를 깨우는 기능 ==> non-blocking 기능
채널 입출력 중 다른 스레드에 의해 채널이 중단될 수 있음을 나타내는 인터페이스이다. 다음의 메서드를 선언한다.
ㅁ public void close() throws IOException
: close()
는 Channel 인터페이스에도 있지만 InterruptibleChannel의 close()는 blocking된 입출력에 대해
java.nio.channels.AsynchronousCloseException 예외를 발생시키는 방법으로 스레드를 깨어나게
해준다. AsynchronousCloseException 예외란 다른 thread가 채널 또는 입출력 조작시에 블록 되는 채널의
일부를 클로우즈 했을 때에 thread가 받는 체크 예외이다.
New I/O의 모든 채널 클래스들은 이 인터페이스를 구현하고 있으므로 모든 채널은
blocking 중에 다른 스레드에 의해 깨어날 수 있는 것이다. 이것은 중요한 특성으로 파일처리뿐만 아니라 나중에 네트워크에
관한 사항을 다룰때 나오는 Selector에 관련된 내용이기도 하다.
InterruptibleChannel을 구현하는 클래스로는 FileChannel,
DatagramChannel, SocketChannel, ServerSocketChannel, Pipe.SinkChannel,
Pipe.SourceChannel 이 있다.
3> ReadableByteChannel 인터페이스 --> 읽기를 위한 인터페이스
ReadableByteChannel 에는 읽기위한 메서드를 가지고 있다.
ㅁ public int read(ByteBuffer dst) throws IOException
: 채
널로 부터 데이터를 읽어들인다. 인자로 들어온 ByteBuffer의 position으로부터 채널의 데이터들이 들어 오게 되며,
limit까지 진행된다. 이때 읽는 곳의 위치와 데이터 개수의 최대값은 버퍼의 position과 limit의 범위이다. 스트림을
끝까지 읽으면 -1을 반환한다. 이 메서드를 호출시 발생하는 예외는 다음과 같다.
NonReadableChannelException - 이 채널이 읽어들여 가능하지 않은 경우
ClosedChannelException - 이 채널이 클로우즈 하고 있는 경우
AsynchronousCloseException - read 조작의 진행중에, 다른 thread에 의해 이 채널이 클로우즈 되었을 경우
ClosedByInterruptException - read 조작의 진행중에 다른 thread로부터의 세치기가 있었기 때문에 채널이 클로우즈 해, 현재의 thread의 세치기 상태가 설정되었을 경우
IOException - 그 외의 입출력 에러가 발생했을 경우
4> WritableByteChannel 인터페이스 --> 쓰기를 위한 인터페이스
WritableByteChannel 에는 쓰기위한 메서드를 가지고 있다.
ㅁ public int write(ByteBuffer src) throws IOException
:
채널에 데이터를 출력한다. 인자로 들어온 ByteBuffer의 position으로부터 limit까지 데이터를 출력한다. 이때
쓰는 곳의 위치와 데이터 개수의 최대값은 버퍼의 position과 limit의 범위이다. 리턴값은 출력하는 데이터수이다. 이
메서드를 호출시 발생하는 예외는 다음과 같다.
NonWritableChannelException - 이 채널이 출력이 가능하지 않은 경우
ClosedChannelException - 이 채널이 클로우즈 하고 있는 경우
AsynchronousCloseException - 출력중에 다른 thread에 의해 이 채널이 클로우즈 되었을 경우
ClosedByInterruptException - 출력중에 다른 thread로부터의 Interrupt가 있었기 때문에 채널이 클로우즈 해, 현재의 thread의 Interrupt 상태가 설정되었을 경우
IOException - 그 외의 입출력 에러가 발생했을 경우
5> ScatteringByteChannel 인터페이스와 GatheringByteChannel인터페이스 --> 여러개의 버퍼를 읽어들이거나 쓰는 것을 지원한다.
ScatteringByteChannel는 ReadableByteChannel의 하위 인터페이스로서 여러개의 ByteBuffer에서 데이터를 읽어들일 수 있다.
ㅁ public long read(ByteBuffer [] dsts) throws IOException
ㅁ public long read(ByteBuffer [] dsts, int offset, int length) throws IOException
GatheringByteChannel는 WritableByteChannel의 하위 인터페이스로서 여러개의 ByteBuffer에서 데이터를 출력할 수 있다.
ㅁ public long write(ByteBuffer [] srcs) throws IOException
ㅁ public long write(ByteBuffer [] srcs, int offset, int length) throws IOException
이들 메서드는 각각 여러 개의 버퍼들에 대해 순차적으로 하나씩 데이터들을 써나가거나 읽어들이게
된다. 여러 개의 버퍼들에 대해 순차적으로 데이터를 써나가거나 읽어들이기 때문에 데이터의 구조가 어떻게 되어 있는지를 미리 알고
있으며, 이들을 여러 버퍼에 쓰거나 읽어들여야 할 때에는 유용하게 쓰인다. 그래서 여러 데이터 유형을 가진 이런 메서드들은
작업을 간편하게 만들어준다.
6> ByteChannel 인터페이스 --> 데이터의 읽기와 쓰기를 지원한다.
ByteChannel 은 ScatteringByteChannel 인터페이스와
GatheringByteChannel인터페이스를 상속받으므로 읽기와 쓰기를 동시에 할 수 있다. 따라서 앞으로 많이 사용하는
인터페이스이다. 이 인터페이스는 단지 ScatteringByteChannel 인터페이스와
GatheringByteChannel인터페이스를 통합한다는 것뿐 다른 기능은 없다.
ByteChannel 인터페이스를 구현하는 클래스로는 FileChannel, DatagramChannel, SocketChannel이다.
채널의 기본 입출력 버퍼는 ByteBuffer 이다.
위에서 살펴본 인터페이스들에 Byte라는 낱말이 들어가 있는데 이는 채널의 기본 입출력이
ByteBuffer임을 말해준다. 그러므로 다른 유형의, 예를 들면 CharBuffer나 FloatBuffer 등을 쓰고자
한다면 ByteBuffer의 asXXXBuffer()류 메서드를 이용해서 바탕은 ByteBuffer이지만 다른 데이터 유형으로
다룰 수 있는 버퍼를 만들어야 한다. |
10. java.nio.channels 클래스
이제부터 채널 클래스들을 살펴본다. 이들 클래스들은 앞서 살펴본 인터페이스를 하나 혹은 둘 이상을 구현하고 있다. 따라서 상속관계와 인터페이스를 구현에 따른 기능들을 유심히 살펴본다.
채널 클래스들의 최상위 클래스는 java.nio.channels.spi.
AbstractInterruptibleChannel 클래스이다. 이 클래스는 InterruptibleChannel 인터페이스를
구현한 Abstract class로서 사실상 모든 채널 관련 클래스의 상위 클래스이다. 이
클래스의 패키지는 java.nio.channels.spi인데 SPI(Service Provider Interface)로
프로그래머가 제공하는 클래스로 대체할 수 있는 기능을 제공해준다. 이는 관련된 클래스들의 기본 구현을 프로그래머가 바꿀 수
있다는 뜻이 된다. 단, 이것은 특별한 경우에만 해당되므로 이런 것이 있다는 정도만 알아두자.
1> AbstractInterruptibleChannel 클래스
1) 특징
- AbstractInterruptibleChannel클래스는 모든 채널 클래스의 상위 클래스이다.
- 상속하는 클래스로는 FileChannel, SelectableChannel클래스이다.
- 비동기적으로 외부에서 채널 클래스들의 작업을 중단시킬수 있다.(Non-Blocking 을 지원해 준다.)
AbstractInterruptibleChannel 클래스의 특징은 크게 3가지로 볼 수있는데
그중에서 눈여겨 볼 것이 바로 3번이다. 이러한 특징으로 AbstractInterruptibleChannel 클래스를 주로
사용은 하지만 직접 쓰는 경우는 없다. 그럼 어떤 원리로 외부에서 중단이 가능한 건지는 우선 이 클래스가 가진 메서드를 먼저
살펴보고 알아보자.
2) 주요 메서드
ㅁ protected void begin() : 입출력 동작이 일어나기 전에 호출된다.
ㅁ void close() : 현재 채널을 닫는다.
ㅁ protected void end(boolean completed) : 입출력 동작이 일어난 후에 호출된다.
ㅁ protected abstract void implCloseChannel() : 현재 채널을 닫는다.
ㅁ boolean isOpen() : 현재 채널이 열려있는지 아닌지를 리턴
AbstractInterruptibleChannel 클래스는 직접 사용하는 경우는 없지만 채널
클래스들이 비동기적으로 외부에 의해서 중단될 수 있도록하는 기능을 제공해준다. 이 클래스를 상속하는 클래스는
FileChannel 클래스와 SelectabelChannel클래스이다. FileChannel 클래스는 파일과 관련된 채널
클래스이며, SelectabelChannel클래스는 여러 채널이 동시에 입출력을 수행하며 이중에서 입출력이 진행된 채널만을
골라서 처리할 수 있게 해주는 기능을 가진다. 이들 클래스에 외부에서 중지시킬수 있는 기능은 바로
AbstractInterruptibleChannel 클래스로부터 비롯된다.
11. FileChannels 클래스
FileChannels는 파일입출력을 위한 채널로 AbstractInterruptibleChannel 클래스를 상속해서 비동기적으로 중단될 수 있게 되어있다. 그리고 ByteChannel 인터페이스를 구현해서 읽기와 쓰기를 동시에 할 수 있다.
1> FileChannels 클래스 객체 생성
FileChannels 클래스는 스스로 인스턴스를 만들 수 없다. 왜냐하면
FileChannels 클래스는 abstract class이고 어떠한 클래스 메서드도 가지지 않는다. 그럼 어떻게
FileChannels 클래스를 사용할 수 있을까? 바로 java.io패키지의 FileInputStream,
FileOutputStream, RandomAccessFile 클래스의 getChannel()메서드를 호출함으로서
FileChannels 클래스의 인스턴스를 얻을 수 있다. FileInputStream의 getChannel()메서드를
사용한다면 읽기만 할 수 있는 FileChannel을 얻게 되고, FileOutputStream의 getChannel()메서드를
사용한다면 쓰기만 할 수 있는 FileChannel을 얻게 되고, RandomAccessFile의 getChannel()메서드를
사용한다면 읽기와 쓰기를 모두 할 수 있는 FileChannel을 얻게 된다. 예를 들면 다음과 같다.
FileInputStream fis=new FileInputStream("파일명"); FileChannels fc=fis.getChannels(); |
2> 주요메서드
ㅁ read()류 메서드와 write()류 메서드
: 읽기와
쓰기를 위한 메서드로 전부 ByteBuffer를 인자로 받고 있어서 버퍼기반의 파일입출력을 할 수 있다. 또한 특정 포인터
위치를 지정해서 입출력을 할 수 있는데 이는 인자로 position값을 주면된다. 리턴값은 long형으로 해당 채널의 스트림의
끝에 이르고 있는 경우는 -1 를 리턴한다.
ㅁ public abstract void force (boolean metaData) throws IOException
:
버퍼에 쓰기를 다한 다음 그 내용을 컴퓨터 하드디스크에 강제적으로 반영하도록하는 메서드이다. FileChannels에서 쓰기는
버퍼의 내용을 바꾸는 것이므로 실제 파일에 변경내용을 반영하려면 force()를 호출해야 한다. 파라미터값으로 true가 오면
파일의 내용뿐만 아니라 파일의 최종 변경시간 등에 대한 내용도 반영이 되고 false이면 파일의 내용만 반영이 된다. 단 로컬에
위치한 파일에만 적용된다.
ㅁ public abstract long size () throws IOException
: 파일의 크기를 리턴. 단 파일이 close된 경우(close()메서드 호출) 예외가 발생한다.
ㅁ public abstract FileChannel truncate (long size) throws IOException
: 인자로 들어온 수로 크기를 설정한다. 현재 크기보다 인자로 들어온 수가 작다면 크기는 줄어드나 인자가 크다면 아무런 변화가 없다. 즉 크기는 줄일 순 있지만 늘리려면 새로운 데이터를 쓰는 방법을 써야 한다.
ㅁ transferFrom (ReadableByteChannel src, long position, long count): FileChannels에 대해 ReadableByteChannel 인스턴스인 채널로부터 읽어들인다.
ㅁ transferTo (long position, long count, WritableByteChannel target) : FileChannels에 대해 WritableByteChannel 인스턴스로 데이터를 출력한다.
--> 이들 메서드는 인자로 읽거나 쓰기를 할 위치를 position으로 지정하고 얼마나 읽거나 쓸지를 conut로 지정한다. 그리고 성공적으로 작업을 마친 경우에는 읽거나 쓴 값을 long형으로 리턴해 준다.
--> 이들 메서드 인자로 FileChannel을 주게 되면 파일간의 복사도 할 수 있다. 이렇게 하면 read(),write()메서드를 사용하는 것보다 빠르게 처리할 수 있다.
ㅁ public final FileLock lock ()
: 파일에 Lock을 걸어서 다른 프로세스나 스레드의 접근을 막은 메서드이다. 이 메서드를 호출하면 FileLock 클래스의 인스턴스를 리턴한다.
ㅁ public abstract FileLock lock (long position, long size, boolean shared)
:
인자인 position에서 size까지의 일부 영역을 lock을 걸수 있고 shared가 true이면 공유 FileLock을
얻을 수 있는데 이는 FileLock이 공유가 될 수 있다는 뜻이다. 이는 하나의 스레드나 프로세스가 독점을 방지하기
위해서이다. 공유 FileLock이라면 그 파일에 대해 두 개의 혹은 프로세스가 접근할 수 있다. 이 메서드 외에도
tryLock()에서도 설정할 수 있다.
ㅁ public final FileLock tryLock ()
: 다른 스드가 lock을 걸고 있는 경우 null를 리턴해서 lock()메서드가 블로킹되는 것을 방지해 준다.
※ 파일 Lock 풀기
- release() 사용.-> 이 FileLock이 release()된것인지 아닌지는 isValid()로 알 수 있다.
- FileLock에 연결된 하부 채널이 close()된 경우.
- 자바가상머신이 종료된경우
ㅁ public abstract MappedByteBuffer map (FileChannel.MapMode mode, long position, long size)
:
파일의 영역을 position의 위치에 size만큼의 크기로 직접 메모리에 매핑한다. 이 메서드는 파일을 처리하는데 있어 그
내용을 자주 읽거나 쓰는 경우에 주로 사용한다. 이 메서드의 리턴형은 MappedByteBuffer이다.
MappedByteBuffer 클래스는 Heap영역이 아닌 자바가 관리하는 일반 메모리 영역을 따로 잡는다. 이 영역은 순수하게
버퍼의 목적으로 쓰이기 때문에 객체를 보관하는 목적의 Heap영역보다 속도가 빠르다. 대신 일반 메모리를 잡아서 처리하는 것이
아니므로 준비시간이 많이 걸린다. 따라서 작은 파일보다는 큰 파일을 처리하고자 할 때 이용하는 것이 좋다.
인자로 들어온 mode는 MappedByteBuffer를 파일에 대한 버퍼로 사용할 때 어떤 방식으로 사용할 지를 설정해주는 것으로 FileChannel.MapMode 클래스에 상수로 선언이 되어 있다. 다음과 같은 값들이 있다.
- public static final FileChannel.MapMode READ_ONLY : 읽기전용
- public static final FileChannel.MapMode READ_WRITE : 읽기/쓰기 가능
- public static final FileChannel.MapMode PRIVATE
: copy-on-write로 읽기와 쓰기는 가능하지만 쓰기를 하는 경우 복사본을 만들어서 변화내용을 따로 보관한다. 원래
파일에는 적용되지 않는다.
MappedByteBuffer에서 파일의 내용을 실제 메모리로 읽어들이는 메서드는 load(). 로드되었는지 여부는 isLoaded()로 알 수 있다. |
이로서 FileChannel클래스가 가진 메서드를 살펴보았다. 보면 알수 있겠지만
FileChannel는 파일과 관련된 작업들을 할 수 있어 io패키지의 FileInputStream이나
FileOutputStream과 같다. 하지만 다른점은 Non-Blocking를 지원한다는 점이다. 이는 네트워크하고 관련이
깊다. 따라서 네트워크를 모른다면 이해할 수 없을 것이다. 그리고 파일에 Lock을 걸수 가 있다는 것이다. 이는 아무나 그
파일에 접근할 수 없다는 것이다. 어쨌든 전에 비해서 많은 것들이 추가 되었음을 볼수 있다.
3> FileChannel클래스 예제
1. 파일에 쓰기와 읽기
mefile.java 소스는 문자열을 파일에 저장해서 이를 다시 읽어서 화면에 출력하는 소스이다. FileChannel클래스를 사용하고 있으며 버퍼로는 BytrBuffer 클래스를 사용하고 있다.
import java.io.*; import java.nio.*; import java.nio.channels.*; //FileChannel클래스를 사용하기 위해 import한다.
class mefile { public static void main(String[] args) throws Exception {
String s="Hello! getJAVA..안녕!! 겟자바.."; // 버퍼에 문자열 s를 담기 위해 바이트 배열을 바꾼다. byte[] by=s.getBytes(); // 버퍼 생성 ByteBuffer buf=ByteBuffer.allocate(by.length); // 바이트배열을 버퍼에 넣는다. buf.put(by); // 버퍼의 위치(position)는 0으로 limit와 capacity값과 같게 설정한다. buf.clear();
// FileOutputStream 객체를 생성 FileOutputStream f_out=new FileOutputStream("a2.txt"); // getChannel()를 호출해서 FileChannel객체를 얻는다. FileChannel out=f_out.getChannel(); // 파일에 버퍼의 내용을 쓴다. out.write(buf); // 채널을 닫는다. 이때 스트림도 닫힌다. out.close();
// FileInputStream 객체 생성 FileInputStream f_in=new FileInputStream("a2.txt"); // getChannel()를 호출해서 FileChannel객체를 얻는다. FileChannel in=f_in.getChannel();
//바이트 버퍼 생성. 이때 버퍼의 크기는 현재 파일의 크기만큼 만든다. // 파일의 크기는 리턴하는 size()는 long형을 리턴하므로 int형으로 캐스팅한다. ByteBuffer buf2=ByteBuffer.allocate((int)in.size());
// 파일을 내용을 읽어 버퍼에 담는다. in.read(buf2);
// array()로 버퍼의 내용을 바이트 배열로 바꾼다. byte buffer[] = buf2.array(); // 스트링으로 변환--> 한글이 깨지지 않도록 하기 위해 한다. String a=new String(buffer); //출력한다. System.out.println(a); } }
|
<< 실행결과 >>
C\>java mefile
Hello! getJAVA..안녕!! 겟자바.. |
결과를 보면 a2.txt파일이 생성되고 파일의 내용이 저장되어 있는 것을 확인할 수 있다.
그리고 화면을 통해 한글도 깨지지 않고 출력됨을 알 수 있다. 한글이 깨지지 않게 출력이 되도록 하기위해 바이트 배열을 스트링
객체로 변환 시켰지만 한글이 없는 경우에는 ByteBuffer클래스를 CharBuffer클래스로 바꾸고 출력해 주면 된다.
소스를 보면 FileChannel 객체를 쓰기와 읽기용 두개를 생성하고 있는데
RandomAccessFile의 getChannel()메서드를 사용한다면 읽기와 쓰기를 모두 할 수 있는 FileChannel을
얻을 수 있게되어 소스가 좀더 간단하게 된다
2. 파일 복사.
FileTest.java는 파일간 복사를 하는 소스로 4가지 방법으로 복사를 한다. 결과는 같지만 하는 방법이 틀리다.
1. io패키지 사용
2. 매핑을 이용.
3. read()와 write()사용
4. transferTo() 사용
이 기능은 번호로 구분하며 명령행 인자로 실행시 해당 번호와 파일명, 복사파일명을 주면 그 기능이 실행되도록 하였다.
import java.io.*; import java.nio.*; import java.nio.channels.*;
class FileTest { // 메인메서드 public static void main(String[] args) throws Exception {
// 명령행 인자로 3가 오지 않으면 사용법 출력 if(args.length<3){ System.out.println("사용법 : java FileTest 메서드번호 파일명 복사파일명"); System.out.println("1. io패키지 사용"); System.out.println("2. 매핑을 이용."); System.out.println("3. read()와 write()사용"); System.out.println("4. transferTo() 사용"); System.exit(1); } int met=Integer.parseInt(args[0]);// 실행 번호 FileInputStream f_in=new FileInputStream(args[1]); FileOutputStream f_out=new FileOutputStream(args[2]); FileChannel in=f_in.getChannel(); FileChannel out=f_out.getChannel(); // 인자로 들어온 번호에 맞는 메서드 호출 switch(met){ case 1: copyIO(f_in,f_out);break; case 2: copyMap(in,out);break; case 3: copyNIO(in,out);break; case 4: copyTransfer(in,out);break; }
}
// copyIO() -- >1. io패키지를 이용한 파일 복사 public static void copyIO(FileInputStream f_in, FileOutputStream f_out) throws Exception{
byte[] buf=new byte[1024]; for (int i; (i=f_in.read(buf))!=-1; ) { f_out.write(buf, 0, i); } f_in.close(); f_out.close(); System.out.println("1. 파일간 복사 --> io패키지 사용 성공!!");
}
// copyMap() -->2. MappedByteBuffer를 이용한 파일간 복사 public static void copyMap(FileChannel in, FileChannel out) throws Exception{
// 입력파일을 매핑한다. MappedByteBuffer m=in.map(FileChannel.MapMode.READ_ONLY,0,in.size()); // 파일을 복사한다. out.write(m); in.close(); out.close(); System.out.println("2. 파일간 복사 --> 매핑 사용 성공!!");
}
// copyNIO() --> 3. read()와 write()사용한 파일 복사 public static void copyNIO(FileChannel in, FileChannel out)throws Exception{
ByteBuffer buf=ByteBuffer.allocate((int)in.size());//버퍼를 읽기를 할 파일의 크기만큼 생성 in.read(buf);// 읽기 buf.flip(); // 버퍼의 position을 0으로 만든다. out.write(buf);// 쓰기 in.close(); out.close(); System.out.println("3. 파일간 복사 --> read()와 write()사용 성공!!");
}
// copyTransfer()-->4. transferTo() 사용한 파일 복사 public static void copyTransfer(FileChannel in, FileChannel out)throws Exception{
in.transferTo(0,in.size(),out);// 0부터 읽기용 채널 크기까지 쓰기채널에 출력한다. in.close(); out.close();System.out.println("4. 파일간 복사 --> transferTo() 사용 성공!!");
} }
|
위 소스는 4가지 방법으로 파일간의 복사를 하고있다. 어느 것이 빠르겠는가는 테스트를 해 보면 간단하다. 파일 복사간의 시간차를 평균을 내보면된다. 이는 소스 자료실에 있다.
첫번째 방법은 우리가 잘 알고 있는 스트림을 이용한 복사이다. 이는 예전 io강의때 자세히 살펴보았다.
두번째 방법은 MappedByteBuffer를 버퍼로 사용해서 복사를 한다. 읽기용 파일채널이
우선 버퍼에 매핑이 되고 그 버퍼는 다시 출력 파일 채널에 사용된다. 이는 이 버퍼에 통째로 전체 파일이 저장되어 있으므로 이
버퍼를 그대로 출력을 하면 파일을 복사하는 것과 동일한 기능을 나타낸다.
세번째 방법은 파일채널이 가진 일반적인 복사 방법이다. 미리 준비된 버퍼에 파일의 내용을
읽여들인다. 그런후 버퍼의filp() 메서드를 이용해서 현재 위치의 한도를 설정하고 현재 위치를 0으로 만들어 쓰기 준비를
한다. 그리고 쓰기...
네번째 방법은 transferTo() 를 사용하는 것이다. 이들 방법중 가장 빠른 것은
transferTo() 를 사용하는 것이다. 이는 기존의 반복문을 사용한 복사보다 더 효율적이다. 몇몇의 운영체계들은 실제
복사하지 않고 파일시스템 캐시에서 타켓 채널로 바이트를 직접 전송하는 방법을 사용한다. 즉 transferTo()는 이런
운영체제의 특성에 의존해서 매우 빠른 파일 전송을 제공해준다.
12. SelectableChannel 클래스
자, 이제 1.4버전에서 새롭게 추가된 네트워크를 입출력 부분을 보자. 우선 크게 본다면 다음 3가지의 클래스를 반드시 이해를 해야한다.
- SelectableChannel 클래스 : 채널로서 관리대상(예들들어 서버소켓, 소켓....)
- Selector 클래스 : 채널 관리자.
- SelectionKey 클래스 : 채널들을 다룰때 필요한 정보.
위 클래스는 서로 연관성을 가지고 있는데 이들의 중요성은 non-blocking I/O의 지원을
해준다는 것이다. 물론 그외 장점들을 가지고 있다. 이들 관계를 간단히 말한다면 Selector클래스는
SelectableChannel 클래스를 SelectionKey로 관리한다. 즉 Selector는 관리자 역활을 하며
SelectableChannel는 서버와 클라이언트 사이에 일어난 작업을 하는 것이고 SelectionKey는 Selector가
SelectableChannel를 구별하는 정보가 된다. 이제 이를 하나씩 살펴보자.
1> SelectableChannel 클래스 정의
Selector에 의한 관리나 Non-Blocking I/O를 위한 기본적인 기능을 가진
Abstract class이다. SelectableChannel은 Selector클래스에 의해 선택될 수 있으며
Non-Blocking I/O를 가능하게 해준다. 가능한 이유는 AbstractInterruptibleChannel 클래스를
상속하기 때문이다.
non-blocking I/O vs blocking I/O
- blocking I/O이란 무엇인가?
우선 blocking I/O는 우리가 앞서
소켓프로그램을 배울때 잘 알고 있을것이다. 예를 들면 서버소켓이 클라이언트의 접속을 기다릴때나 입력이나 출력이 일어나는 경우,
이들은 블록화가 된다. 즉 모든 입출력에 대해 그 동작이 완료될때까지 서버는 기다린다는 뜻이다. 이는 접속하는 클라이언트 수많큼
스레드를 생성해야 하는 결과를 낳았으며 접속만 하고 아무것도 하지 않는 클라이언트인 경우 스레드도 역시 가만히 있어야 한다는
것이다. 이는 스레드 낭비이자 서버의 낭비이다. 참고로 한 스레드 하나당 1MB의 메모리가 할당된다.
- non-blocking I/O이란 무엇인가?
non-blocking은 접속한
클라이언트 수 만큼 스레드를 만들어서 처리를 하는 것이 아니라 하나의 스레드만으로 Selector만을 계속 지켜보고 있으면
Selector가 실제 입출력이 일어난 채널이 있으면 이를 알려주어 그때그때 이를 처리할 수 있다는 것이다. 이렇게 하면 모든
채널에 대해서 각각의 스레드가 존재할 필요가 없다.
|
2> SelectableChannel 클래스 주요 메서드
ㅁ public final SelectionKey register (Selector sel, int ops)
: 현재 채널을 sel로 지정된 selector에 등록하되 어떤 입출력 동작(ops)에 대해 선택할 것인지를 지정한다.
리턴값으로 SelectionKey 클래스의 인스턴스를 리턴한다. 여기서 ops는 입출력 동작의 종류를 나타내는 것으로
SelectionKey클래스에 정의되어 있다. 이 ops는 4가지로 다음과 같다.
- public static final int OP_ACCEPT
- public static final int OP_CONNECT
- public static final int OP_READ
- public static final int OP_WRITE
ㅁpublic abstract SelectionKey register (Selector sel, int ops, Object att)
: 위와 같은 역활을 하는 메서드로 인자로 att가 추가되었다. att는 해당 채널에 부가적으로 필요한 객체를 지정하는 것으로
Selector를 통해 SelectionKey만 얻을 수 있기 때문에 부가적인 정보를 담는 이러한 객체는 유용하게 사용된다.
==> register()메서드는 채널을 Selector에 등록시키는 메서드인데 이러한 등록은 한번에 하나의 Selector에만 가능하면 하나의 SelectableChannel 은 여러개의 Selector에 등록하는 것은 불가능하다.
==> 등록취소가 되는 경우
SelectionKey클래스의 cancel()메소드에 의해/SelectableChannel이 close()가 되는 경우/스레드가 intrrupt()가 되는 경우
ㅁ public abstract boolean isRegistered () : 현재 이 채널이 selector에 등록되어 있는지 아닌지를 판단.
ㅁ public abstract SelectableChannel configureBlocking (boolean block) : 채널의 입출력 모드를 선택한다. 인자인 block이 true 이면 blocking I/O , false 이면 Non-Blocking I/O이다. 이는 Selector에 등록한 후에는 바꿀 수 없다.
ㅁ public abstract boolean isBlocking () : 현재 채널의 입출력 모드를 알아 낸다.
ㅁ public abstract SelectionKey keyFor (Selector sel) : 인자로 들어온 sel에 해당 채널이 등록이 되어 있다면 해당 SelectionKey를 얻을 수 있다. 등록되어 있지 않는 경우는 null을 리턴한다.
ㅁ public abstract int validOps () : 현재 채널이 할 수 있는 입출력 동작을 리턴해준다. 리턴값으로 앞서 살펴본 SelectionKey클래스의 클래스 상수인 4가지이다.
** 예제는 Selector 클래스와 SelectionKey 클래스를 살펴보고 socket 예제에서 함께 다루도록 한다.
13. Selector 클래스와 SelectionKey 클래스
java.nio.channels.Selector 클래스는
SelectableChannel(채널)들을 관리하는 클래스로서 SelectionKey의 인스턴스로 관리한다. 이런
채널들을(엄밀히 말하면 SelectableChannel클래스) 관리하는 것이 바로 Selector 클래스의 역활이다.
Selector가 관리하는 SelectableChannel들은 전부 non-blocking I/O모드이어야 하며 blocking
I/O모드의 채널은 Selector에 의해 관리되지 못한다.
java.nio.channels.SelectionKey 클래스는 SelectableChannel
을 선택하는데 기준이 되는 동작(ops)과 부가적인 정보 객체(att)와 이에 연결된 Selector를 함께 표현하는 클래스로
SelectableChannel를 다루는데 있어 기본적인 정보 클래스이다.
우선 채널들이 selector에 등록이 된다. 등록된 채널들은 해당 입출력 동작이
일어날때마다 selector에 의해 선택되어서 실행이 된다. 따라서 스레드는 selector만 감시하면 나머지는
selector가 다하게 되는 것이다.
1> Selector 클래스의 주요 메서드
ㅁ public static Selector open () : selector를 얻는다. 예를 들면 다음과 같다.
Selector s=Selector.open();
ㅁ public abstract void close () : selector를 닫는다. 이때 selector가 관리중인 SelectionKey들도 모두 닫힌다.
ㅁ public abstract boolean isOpen () : selector가 닫혔는지 아닌지 여부를 리턴.
ㅁ public abstract Set keys () : 채널이 등록될때 생성된 SelectionKey를 java.util.Set객체로 리턴한다. 이 Set안에 SelectionKey들이 전부 들어 있다.
ㅁ public abstract Set selectedKeys ()
: selector가 선택한 SelectionKey를 java.util.Set객체로 리턴한다. 이들은 등록된
SelectionKey들 중에서 해당 동작(ops)이 일어난 SelectionKey들을 가리키는 것이다. 이들은
select()메서드나 selectNow()메서드가 호출된 다음에 Set 객체에 채워진다.
ㅁ public abstract int select () :
ㅁ public abstract int selectNow () :
->
둘다 등록된 SelectionKey들 중에서 해당 동작(ops)이 일어난 SelectionKey들을 SelectionKey
리스트에 포함시키는 메서드로서 실제 해당 동작이 일어난 SelectionKey의 개수를 리턴한다. 이 둘의 차이점은 우선
select()인 경우 해당 동작이 일어난 SelectionKey가 하나라도 생시지 않으면 그 상태에서 블로킹이 된다. 그러나
selectNow()는 당장 해당 동작이 일어난 SelectionKey가 없어도 곧바로 리턴이 된다.
ㅁ public abstract Selector wakeup () : select()가 블로킹되었을때 이를 깨워주는 메서드이다. 이때 SelectionKey의 Set에 SelectionKey가 추가되었는지 안되었는지는 알 수 없다.
2> SelectionKey 클래스의 주요 메서드
우선 클래스 변수부터 살펴보자. 앞서 말했지만 이들은 입출력 동작의 종류을 나타낸다.
- public static final int OP_ACCEPT
- public static final int OP_CONNECT
- public static final int OP_READ
- public static final int OP_WRITE
이들은 각각 비트값을 가진 int형 데이터로서 각각이 or연산에 의해 합해져서 어떤 동작들이 가능하거나 관심 있는지를 나타내게 된다.
ㅁ public abstract int interestOps () : 해당 동작을 위의 값들로 리턴한다. 예를 들어 OP_READ|OP_WRITE 값이 리턴되었다면 읽는 것과 쓰는 동작을 취한다는 ㅤㄸㅜㅅ이 된다. 그래서 읽거나 쓰거나 하는 동작이 취해지면 Selector에 의해 선택될 수 있게 된다.
ㅁ public abstract SelectionKey interestOps (int ops) : 이는 위와는 반대로 해당 동작을 지정한다.
ㅁ public final boolean isAcceptable ()
ㅁ public final boolean isConnectable ()
ㅁ public final boolean isReadable ()
ㅁ public final boolean isWritable ()
: 이들 메서드는 해당 동작들의 유효성을 점검할 수 있다.
잠깐!.. 현재 일어나는 해당 동작 알아내기!!
두가지 방법이 있는데 우선 readyOps ()메서드를 사용하는 방법이다. readyOps
()는 현재 준비가 가능한 동작들을 int형으로 리턴해 준다. 따라서 int값을 얻어서 이를 하나하나 비트값을 조사한다. 이는
하나의 동작일 수도 있고 여러개의 동작일 수도 있다. 예를 들어, 읽기인 경우...다음과 같이 비교한다.
k.readyOps() & OP_READ != 0
하지만 이 방법보다는 isAcceptable ()/ isConnectable ()/ isReadable () / isWritable () 이들 메서드를 이용해서 검사하는 것이 편하다. |
ㅁ public final Object attach (Object ob) : 채널 등록시 함께 등록될 객체(채널에 관한 정보를 담고 있는 객체)를 지정해준다. 이는 SelectableChannel클래스의 register ()메서드에서 인자 하나를 추가해서 지정해주는 것과 같다.
ㅁ public final Object attachment () : attach()메서드나 register()로 추가로 지정된 객체를 리턴한다.
ㅁ public abstract SelectableChannel channel () : 연결된 SelectableChannel 인스턴스를 리턴한다.
ㅁ public abstract Selector selector () : 연결된 Selector 인스턴스를 리턴한다.
** 예제는 socket 예제에서 함께 다루도록 한다.
14. ServerSocketChannel 클래스와 SocketChannel 클래스
이제 실전에 들어가기 전에 ServerSocketChannel 클래스와 SocketChannel
클래스에 대해 먼저 알아보자. 이들은 net패키지의 ServerSocket클래스와 Socket클래스를 채널로서 다루고자 할 때
쓰는 SelectableChannel이다. 이들 네트워크 관련 채널들은 독자적으로 소켓의 역할을 대처하지는 않는다. 대신 소켓
클래스를 내부에 가지고 있으면서 이들의 기능을 채널화하는데 적절히 이용하게 된다.
1> ServerSocketChannel 클래스
① ServerSocketChannel 클래스 생성
ServerSocketChannel
을 얻으려면 open()메서드를 사용한다. 단 이때 얻는 채널은 내부의 소켓이 아직 bind되지 않은 상태이기 때문에 적절한
IP주소와 포트 번호로 binding시켜줘야 한다. 일반적으로 다음과 같은 순서로 채널을 얻고 binding 한다.
1. ServerSocketChannel 얻기.
ServerSocketChannel server=ServerSocketChannel.open();
2. 내부 소켓을 얻는다.
ServerSocket socket=server.socket();
3. binding 한다.
SocketAddress addr=new InetSocketAddress(포트번호);
socket.bind(addr);
② ServerSocketChannel 클래스 주요 메서드
ㅁ public abstract SocketChannel accept()
: 이 채널의 소켓에 대한 접속을 받아들여 SocketChannel을 리턴한다. 이때 ServerSocketChannel이
Blocking I/O 모드라면 accept()는 Blocking되지만 Non-Blocking I/O 모드라면
Blocking되지 않는다. 따라서 당장 어떤 접속요구가 없다면 null을 리턴한다. 하지만 리턴된 SocketChannel은
ServerSocketChannel이 Blocking I/O 모드이든 아니든 상관없이 무조건 Blocking I/O 모드로
시작한다.
ㅁ public static ServerSocketChannel open() : ServerSocketChannel를 얻는다. 이때 리턴된 ServerSocketChannel는 아직 bind되지 않은 상태이므로 소켓의 bind 메소드를 사용해 특정의 주소에 binding을 해주어야 한다.
ㅁ public abstract ServerSocket socket() : 내부 소켓을 얻는다.
ㅁ public final int validOps() : 현재 채널이 할 수 있는 해당 동작(ops)을 리턴한다. 서버소켓채널은 SelectionKey.OP_ACCEPT 만 할 수 있다.
2> SocketChannel 클래스
① SocketChannel로 접속하기
소켓채널을 얻기 위해서는 open()메서드를 사용하면 되는데 open()에는 인자가 있는 것과
없는 것이 있다. 만약 인자 없이 open()를 사용한다면 접속이 되지 않는 소켓채널을 리턴하므로 connect()메서드를
이용해서 접속을 해주어야 한다. 인자로 SocketAddress 객체를 준다면 접속이 된 소켓채널을 얻을 수 있다. 두가지
경우를 다 보자.
* 접속된 소켓채널 얻기
SocketAddress addr=new InetSocketAddress("ip주소", 포트번호);
SocketChannel socket=SocketChannel.open(addr);
* connect() 사용해서 접속하기
SocketAddress addr=new InetSocketAddress("ip주소", 포트번호);
SocketChannel socket=SocketChannel.open();
socket.connect(addr);
* SocketChannel에서 Non-Blocking I/O 모드일 경우에는
open(SocketAddress addr)해도 되지만, open()해서 connect(SocketAddress addr)했을
경우에는 즉시 연결작업이 끝나지 않을 수 있어서 이 메서드가 false를 리턴하게 되므로, finishConnect()로
연결작업을 끊어줘야 한다. 만약 연결이 제대로 안되었다면 false가 리턴된다. Blocking I/O 모드일 때는
connect() 호출이 Blocking 되면 연결이 끊길때까지 지속된다.
② SocketChannel 클래스 주요 메서드
ㅁ public abstract boolean connect (SocketAddress remote) : 인자로 들어온 SocketAddress 객체 정보를 가지고 현재 채널에 소켓을 접속한다. 연결이 제대로 안되면 false를 리턴한다.
ㅁ public abstract boolean finishConnect () : 소켓채널의 접속 처리를 완료한다.
ㅁ public abstract boolean isConnected () : 채널소켓이 접속이 되었는지 유무를 리턴.
ㅁ public abstract boolean isConnectionPending () : 이 채널상에서 접속 조작이 진행중인지 어떤지를 판단. 즉 접속이 시작되고 완료하지 않은 경우, finishConnect()가 호출되고 있지 않는 경우 true를 리턴.
ㅁ public static SocketChannel open () : 접속되지 않는 소켓채널을 리턴.
ㅁ public static SocketChannel open (SocketAddress remote) : 접속된 소켓채널을 리턴.
ㅁ read()류 메서드
ㅁ public abstract int read (ByteBuffer dst)
ㅁ public final long read (ByteBuffer [] dsts)
ㅁ public abstract long read (ByteBuffer [] dsts, int offset, int length)
ㅁ write()류 메서드
ㅁ public abstract int write (ByteBuffer src)
ㅁ public final long write (ByteBuffer [] srcs)
ㅁ public abstract long write (ByteBuffer [] srcs, int offset, int length)
3> 예제
간단한 채팅을 만들어보자. 단순하게 서버에 클라이언트가 접속, 메세지를 보내면 서버가 메세지를 읽어 출력하고 서버도 메세지를 보내는 소스이다. 우선 순서를 보자.
<< 서버측 >>
- 채널들을 관리할 Selector를 얻는다.
Selector selector=Selector.open();
- ServerSocketChannel를 얻는다.
ServerSocketChannel server=ServerSocketChannel.open();
- 내부 소켓을 얻는다.
ServerSocket socket=server.socket();
- binding 한다.
SocketAddress addr=new InetSocketAddress(포트번호);
socket.bind(addr);
- ServerSocketChannel을 Selector에 등록시킨다. ServerSocket는 OP_ACCEPT동작만 할 수 있다.
server.register(selector, SelectionKey.OP_ACCEPT);
- 클라이언트의 접속을 대기한다. 이때 접속이 되면 accept()에 의해 상대방 소캣과
연결된 SocketChannel의 인스턴스를 얻는다. 이 채널는
읽기(OP_READ),쓰기(OP_WRITE),접속(OP_CONNECT) 행동을 지원한다.
SocketChannel socketChannel=serverChannel.accept();
- 접속된 SocketChannel를 Selector에 등록한다.
socketChannel.register(selector, 소켓채널의 해당행동);
- 채널이 취할 수 있는 3가지 동작(읽기(OP_READ),쓰기(OP_WRITE),접속(OP_CONNECT) )에 대해서 검사한다. 이때는 다음과 같은 메서드를 이용해서 체크를 한다.
isConnectable() : true이면 상대방 소켓과 새로운
연결이 됐다는 뜻이다. 이때 Non-Blocking I/O 모드일 경우에는 연결과정이 끝나지 않는 상태에서 리턴될 수도 있다.
그러므로 SocketChannel 의 isConnectionPending()메서드가 true를 리턴하는지 아닌지를 보고
true를 리턴한다면 finishConnection()를 명시적으로 불러서 연결을 마무리짓도록 한다.
isReadable() : true이면 읽기(OP_READ) 동작이 가능하다는 뜻으로 SocketChannel로부터 데이터를 읽는 것에 관한 정보를 받을 준비가 되었다는 뜻이다. 따라서 버퍼에 읽어들이기만 하면된다.
isWritable() : true이면 쓰기(OP_WRITE) 동작이 가능하다는 뜻으로 SocketChannel에 데이터를 쓸 준비가 되었다는 뜻이다. 따라서 버퍼의 데이터를 이 채널에 write()하면 된다.
<< 클라이언트측 >>
- SocketChannel를 얻어서 접속한다.
SocketAddress addr=new InetSocketAddress("localhost", 8080);
SocketChannel socket=SocketChannel.open(addr);
- 버퍼를 만들고 서버에서 들어온 데이터를 읽고 쓰기를 한다.
자, 이제 소스를 보자. 먼저 서버측 소스인 ServerSocketChannelTest1.java를 보자.
ServerSocketChannelTest1.java
import java.io.*; import java.net.*; import java.util.*; import java.nio.*; import java.nio.channels.*;
public class ServerSocketChannelTest1 implements Runnable { Selector selector; int port=8080;
// -> 생성자 public ServerSocketChannelTest1() throws IOException {
selector=Selector.open(); //셀렉터를 생성한다. ServerSocketChannel server=ServerSocketChannel.open();//binding 되지 않는 ServerSocketChannel 생성 System.out.println(server); ServerSocket socket=server.socket();// 내부소켓 생성 SocketAddress addr=new InetSocketAddress(port); socket.bind(addr);// binding System.out.println(server);
server.configureBlocking(false);// Non-Blocking I/O모드 설정
// 만들어진 ServerSocketChannel이 어떤 동작이 가능한지 본다. // 결과를 보면 ServerSocketChannel은 OP_ACCEPT하나만 가능하다는 것을 알수 있다. int validOps=server.validOps(); System.out.print("ServerSocketChannel.validOps() : "+validOps); System.out.println(", "+(validOps==SelectionKey.OP_ACCEPT));
// binding된 ServerSocketChannel을 Selector에 등록한다. server.register(selector, SelectionKey.OP_ACCEPT); System.out.println("****************************************"); System.out.println("클라이언트의 접속을 기다리고 있습니다"); System.out.println("****************************************"); }
// --> run() public void run() { // SocketChannel 용 행동 변수를 미리 만들어 둔다. int socketOps=SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE; ByteBuffer buf=null;// 버퍼
while(true) { try { //현재 selector에 등록된 채널에 관심있는 동작이 하나라도 실행된 경우 그 채널들을 SelectionKey의 Set에 추가한다. selector.select(); } catch(IOException ioe) { ioe.printStackTrace(); }
// 선택된 채널(위의 select()로 지정된)들의 리스트를 얻는다. Set selectedKeys=selector.selectedKeys(); // Set에 대해 Iterator를 얻어서 하나씩 처리하게 된다. Iterator iter=selectedKeys.iterator();
while(iter.hasNext()) { try { SelectionKey selected=(SelectionKey)iter.next(); iter.remove();// 현재 처리하는 SelectionKey는 Set에서 제거한다.
// channel()는 현재 동작에 관련된 채널들을 리턴하는데 // 이를 가지고 지금 현재 하는 작업이 읽기냐 쓰기냐 접속이냐를 판단한다. SelectableChannel channel=selected.channel(); // 현재 동작중인 채널이 ServerSocketChannel이라면 accept()를 호출해서 // 접속요청을 해온 상대방 소켓과 연결될 수 있는 SocketChannel 을 얻는다. if (channel instanceof ServerSocketChannel) { ServerSocketChannel serverChannel=(ServerSocketChannel)channel; SocketChannel socketChannel=serverChannel.accept(); // 지금 서버소켓채널은 Non-Blocking I/O모드 설정되어 있다. // 이는 당장의 접속이 없어도 블로킹되지 않고 바로 null을 리턴하므로 이를 체크를 해주야 한다. if (socketChannel==null) { System.out.println(" # null server socket"); continue; }
System.out.println(" # socket accepted : "+socketChannel);
socketChannel.configureBlocking(false);// 얻어진 소켓은 블로킹 모드이므로 이를 Non-Blocking I/O모드 설정한다. // 가능 행동 확인 // 결과를 보면 true를 리턴하는데 이는 소켓채널은 읽기(OP_READ),쓰기(OP_WRITE),접속(OP_CONNECT) 행동이 가능함을 알수 있다. int validOps=socketChannel.validOps(); System.out.print("SocketChannel.validOps() : "+validOps); System.out.println(", "+(validOps==socketOps));
// 소켓채널을 셀렉터에 등록 socketChannel.register(selector, socketOps); } else { SocketChannel socketChannel=(SocketChannel)channel; buf=ByteBuffer.allocate(20);
// 소켓채널의 가능 행동을 검사해서 맞다면 그에 대응하는 작업을 한다. // 접속인지 아닌지 if (selected.isConnectable()) { System.out.println(" # socket connected"); if (socketChannel.isConnectionPending()) { System.out.println(" # Connection is pending"); socketChannel.finishConnect(); } } // 읽기 인지 아닌지
if (selected.isReadable()) { socketChannel.read(buf); buf.clear(); System.out.print("# socket read :"); while(buf.hasRemaining()){ System.out.print((char)buf.get()); } }
// 쓰기인지 아닌지 if (selected.isWritable()) { String s="Hello Client!!"; byte[] bytes=s.getBytes(); buf.put(bytes); buf.clear(); socketChannel.write(buf); System.out.println(" # socket write : "+s); } } } catch(IOException ioe) { ioe.printStackTrace(); } } } }
// --> 메인메서드 public static void main(String[] args) throws IOException { ServerSocketChannelTest1 test=new ServerSocketChannelTest1(); new Thread(test).start(); } }
|
SocketChannelTest1.java -->클라이언트측
import java.io.*; import java.net.*; import java.util.*; import java.nio.*; import java.nio.channels.*;
public class SocketChannelTest1 { public static void main(String[] args) throws Exception { // SocketChannel생성해서 접속한다. SocketAddress addr=new InetSocketAddress("localhost", 8080); SocketChannel socket=SocketChannel.open(addr);
System.out.println(socket); System.out.println("# isBlocking() : "+socket.isBlocking());
ByteBuffer buf=ByteBuffer.allocate(20); //버퍼생성 while(true){ int read=0; socket.read(buf);// 서버가 보내온 데이터 읽기 buf.clear(); System.out.print("# socket read :"); while(buf.hasRemaining()){ System.out.print((char)buf.get()); } buf.clear();
// 서버에게 데이터 보내기 String msg="Hello Server!!"; byte[] bytes=msg.getBytes(); buf.put(bytes); buf.clear(); socket.write(buf); System.out.println("\n"+"# socket write : "+msg); buf.clear(); } } } |
<< 실행 결과 >>
서버측
C\>java ServerSocketChannelTest1
sun.nio.ch.ServerSocketChannelImpl[unbound] sun.nio.ch.ServerSocketChannelImpl[/0.0.0.0:8080] ServerSocketChannel.validOps() : 16, true ***************************************** 클라이언트의 접속을 기다리고 있습니다. ***************************************** # socket accepted : java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1049] SocketChannel.validOps() : 13, true # socket write : Hello Client!! # socket read : Hello Server!!
|
클라이언트측
C\>java SocketChannelTest1
java.nio.channels.SocketChannel[connected local=/127.0.0.1:1049 remote=/127.0.0.1:8080] # isBlocking () : true # socket read : Hello Client!! # socket write : Hello Server!!
|
이른 좀더 응용하면 훨씬 나은 채팅을 만들 수 있다.