해당 포스트는 "안드로이드 통신+보안 프로그래밍" 책의 내용을 요약한 것이다.



※ 소켓 채널 통신

: 이전 포스트에서 소켓 통신은 몇 개의 메서드가 블록모드로 작동한다는 문제점이 있다는 걸 알았다. 그 대안으로 비동기 작업을 수행할 수 있도록 새로운 입출력 채널(NIO)가 있다. NIO는 비동기 작업 수행을 위해 소켓채널과, 파일 입/출력, 버퍼를 제공한다. 소켓채널 통신은 원칙적으로 블록모드로 작동하지만 옵션을 사용해 Non블록모드로 변경할 수 있다. 또한 서버처럼 클라이언트의 빈번한 접속과 종료가 이루어지는 환경에서 많이 사용된다.

채널의 장점은 다음과 같다.

- 멀티 스레드 접근에 안전하다.

- 직접 메모리에 데이터 입/출력이 가능해 메모리 액세스 속도가 빠르다.

- 다른 스레드가 채널을 닫으면 블록된 채널은 Exception 예외를 발생시킨다.

자바는 채널과 관련해서 다음과 같은 패키지들을 제공한다.

- java.nio 는 버퍼와 관련된 클래스를 지원한다. 넌블록모드로 소켓채널을 사용하면 스트림 사용이 불가능해 별도의 버퍼를 제공한다.

- java.nio.channels : 디바이스, 파일, 네트워크 소켓 등에 입출력 작업을 할 수 있는 클래스를 지원한다.

- java.nio.charset : 바이트와 문자 변환 작업을 지원하는 인코딩과 디코딩 클래스를 제공한다.


※ NIO 버퍼

: 소켓 채널을 사용할 때는 자바에서 제공하는 기본 배열이 아닌 java.nio에서 제공하는 버퍼 클래스를 사용해야 한다. NIO 버퍼의 특징은 다음과 같다.

- 선형의 순차저긴 메모리이다.

- 같은 종류의 데이터 타입별로 연속하여 데이터를 저장한다.

- 고정된 크기의 데이터 타입만 저장이 가능하고 반복적인 사용이 가능하다. 따라서 메모리의 단편화를 최대한 적게 만들어 안드로이드와 같은 부족한 메모리 리소스 환경에서 최소한의 가비지 컬렉션 작업을 수행한다.

- NIO 버퍼에서 제공하는 메서드들은 단일 스레드 모델이기 때문에 버퍼에 읽고 쓰기 위해서는 synchronized() 또는 join()을 사용해야 한다.

- get()과 put() 메서드를 사용하여 데이터를 읽고 쓴다.


버퍼 생성은 버퍼 클래스들의 allocate(int capacity), allocateDirect(int capacity), wrap(byte[] array) 정적 메서드를 사용한다. allocate는 JVM의 힙 영역에 버퍼의 영역을 잡는다. 자바 변수들과 다양한 타입의 데이터를 서로 교환할 때 사용하면 좋다. allocateDirect는 운영체제의 커널 영역에 다이렉트 버퍼를 생성한다. 다이렉트 버퍼는 JVM의 가비지 콜렉션 작업을 수행하지 않고 직접 메모리를 제어하기 때문에 대용량의 데이터를 읽고 쓰거나 장기간에 걸쳐서 사용할 때 전체적인 성능향상에 도움이 된다. wrap() 메서드는 기존 버퍼 또는 바이트 배열을 덮어서 재사용할 때 사용한다. 

버퍼에는 인덱스 역할을 하는 4개의 필드가 존재한다. capacity는 버퍼의 전체 크기, position은 현재 읽거나 쓰고자 하는 위치의 인덱스, limit는 버퍼에서 읽고 쓸 때 허용되는 마지막 위치를 가리키는 인덱스이다. mark는 reset() 메서드를 호출하면 positon 위치가 mark가 가리키는 인덱스로 바뀐다. 반복해서 데이터를 읽을 때 사용한다. 

버퍼에 데이터를 넣을 때 버퍼의 limit를 넘어서 버퍼에 데이터를 쓰면 BufferOverflowException 예외가 발생한다. 또한 입력하고자 하는 데이터의 크기보다 남아 있는 버퍼의 크기가 적을 때 BufferUnderflowException 예외가 발생한다. 그리고 읽기 위주로 선언된 버퍼에 쓰는 작업을 수행하면 ReadOnlyBufferException 예외가 발생한다. 


ex)

public class AsBuffer {
	private static final int BSIZE = 1024;
	public static void main(String[] args) {
		ByteBuffer bb = ByteBuffer.allocate(BSIZE);
		IntBuffer ib= bb.asIntBuffer();   //ByteBuffer bb를 IntBuffer로 변경한다.
		
		ib.put(new int[]{11,24,25,45,56,78,90});
		System.out.println(ib.get(3));
		
		ib.put(3, 1333);    //4번째 위치에 1333을 넣는다.
		ib.rewind();  //배열의 처음 위치로 position 이동
		while(ib.hasRemaining()){
			int i = ib.get();
			if(i==0)  //끝은 0으로 채워져 있다.
				break;
			System.out.println(i);
		}
		while(bb.hasRemaining()){
			byte i = bb.get();
			System.out.print(i);
		}
	}
}


- 버퍼 관련 메서드

a. get(byte[] dst, int off, int len)  == for(int i=off; i<off+len; i++) dst[i]=src.get();

b. get(byte[] dst)  ==  src.get(dst,0,dst.length)

c. put(byte[] src, int off, int len)   ==   for(int i=off; i<off+len; i++) dst.put(src[i]);

d. clear() : 버퍼에 데이터를 채우기 전에 필드를 초기화한다. 

e. flip() : limit에 position 값을 대입하고 position을 0으로 설정한다. 버퍼에 데이터를 다 입력하고 읽기 작업을 시작할 때 주로 호출한다.

f. compact() : position과 limit 사이에 있는 데이터를 버퍼의 처음으로 이동시킨다. position 값은 limit-position값으로 변경되고 limit는 capacity로 설정된다. 이미 읽은 데이터는 버리고 다시 더 많은 데이터를 버퍼에 추가하기 위해 사용한다.

g. mark() : mark를 position 값으로 설정한다.

h. limit(int newLimit) : limit를 newLimit로 설정한다. 단, position>newLimit라면 position은 newLimit값으로 설정된다. mark>newLimit 라면 mark 값은 삭제된다.

i. remaining() : 버퍼의 남아있는 데이터 크기를 반환한다.(limit-position)


- ReadableByteChannel 인터페이스

: read(ByteBuffer dst) 메서드를 제공한다. 채널로부터 position에서 최대 limit까지 데이터를 읽는다. read 메서드는 읽은 데이터 수를 반환하는 데 -1을 반환하면 파일을 읽는 과정에서 EOF를 반환됬거나 상대방에서 FIN 패킷을 받았다는 뜻이다. 

read() 메서드를 호출할 때 예외는 다음과 같다.

1. NonReadableChannelException : 채널을 읽을 수 없을 때

2. ClosedChannelException : 채널이 닫혀진 상태에서 데이터를 읽으려 할 때

3. AsynchronousCloseException : read() 메서드로 작업 중인데 다른 스레드에서 채널을 닫을 때

4. ClosedByInterruptException : 읽기 작업 중에 다른 스레드로부터 스레드의 종료를 요청받고 채널이 닫혔을 때

5. IOException : 입출력 네트워크 예외가 발생했을 때


- WritableByteChannel 인터페이스

: write(ByteBuffer src) 메서드를 제공한다. src의 position에서 limit까지 데이터를 채널로 출력한다. 반환 값은 실제 출력된 데이터의 바이트 수다. write() 메서드를 호출할 때 예외는 다음과 같다.

1. NonWritableChannelException : 채널로 출력 가능하지 않을 때

2. ClosedChannelException : 채널이 닫혀진 상태에서 데이터를 출력할 때

3. AsynchronousCloseException : write() 메서드로 작업 중인데 다른 스레드에서 채널을 닫을 때

4. ClosedByInterruptException : 쓰기 작업 중에 다른 스레드로부터 스레드의 종료를 요청받고 채널이 닫혔을 때

5. IOException : 입출력 네트워크 예외가 발생했을 때


※ 소켓통신과 소켓채널 통신

: 안드로이드에서 소켓 통신을 할 시 read()나 accept()와 같은 블록킹 메서드 때문에 별도의 스레드를 만들어야 했다. 안드로이드는 리소스가 부족하기에 특정 개수의 스레드를 생성하게 되면 급격한 성능 저하 문제가 있다. 또한 스트림, 스레드, 문자열은 생성과 함께 데이터를 복사하고 사용후 바로 소멸하는 특징이 있어 많은 양의 데이터가 가비지 컬렉션 대상이 된다. 따라서 사용 빈도수가 높아지면 가비지 컬렉션 작업이 빈번하게 이루어지고 그만큼 성능 저하가 발생한다. 이에 대한 대안으로 소켓채널 통신이 있다. 넌블록킹인 소켓 채널은 채널관리자인 셀렉터가 실제 데이터 입출력이 일어난 채널(서버와 클라이언트의 연결 통로)을 감시하다가 입출력이 발생하면 그와 관련된 데이터를 처리하기 위해 처리할 작업 내용을 알려주는 선택키로 이벤트를 발생시킨다. 따라서 소켓처럼 메서드별로 독립적인 스레드가 존재할 필요가 없다. 하지만 소켓 채널 통신은 소켓 통신에 비해서 구현하는 게 어렵고 더 많은 코드를 작성해야 한다. 


소켓 채널 통신에서 중요한 기능을 제공하는 게 SelectaleChannel 클래스이다. 이 클래스는 채널 통신에 핵심적인 역할을 하는 메스드를 제공한다.

- SelectableChannel configureBlocking(boolean block) : 매개변수로 'true'를 입력하면 블록모드, 'false'를 입력하면 넌블록모드로 통신이 이루어진다. 해당 메서드를 통해 입출력 모드가 선택되면 변경이 불가능하다.


- SelectionKey register(Selector sel, int ops) : Selector(채널관리자)에게 채널에서 ops 작업 내용을 등록한다. ops 값은 다음과 같다. 

a. OP_ACCEPT : ServerSocketChannel에서 클라이언트 접속 승인

b. OP_CONNECT : SocketChannel과 서버 사이의 연결 작업의 성공 여부

c. OP_READ : 데이터 읽기 작업

d. OP_WRITE : 데이터 쓰기 작업 

두 개의 매개변수를 입력하는 메서드 말고 register(Selector, int, Object att) 메서드도 있다. att 객체는 SelectionKey(선택키)에 첨부된다. register메서드는 채널 관리자, 채널, 작업 내용(선택키)을 묶어주는 역할을 한다. 만약 특정 채널들에 등록된 작업들이 채널 관리자에 의해 감지가 되면 채널 관리자에서 감지된 채널들의 작업들을 담은 선택키들의 집합을 얻을 수 있다. 집합에서 각 선택키를 추출해 ops에 따라서 작업을 하면 된다. 자세한 것은 밑에서 예제를 통해 살펴볼 예정이다. register() 메서드로 등록한 내용의 취소 방법은 다음과 같다.


- SelectionKey keyFor(Selector sel) : 채널관리자 sel과 연결된 선택키를 반환한다.

- int validOps() : 채널에서 사용할 수 있는 작업내용(ops)를 반환한다.

- void close() : 채널을 닫는다. 닫힌 채널에 대해 입출력 메서드를 호출하면 ClosedChannelException 예외가 발생한다.


 ※ 클라이언트 소켓 채널 구현 절차

1. SocketChannel 객체를 생성하고 서버와 연결한다.

SocketAddress addr = new InetSocketAddress("ip주소",포트번호);
SocketChannel socket = SocketChannel.open(addr);
또는
SocketAddress addr = new InetSocketAddress("ip주소",포트번호);
SocketChannel socket = SocketChannel.open();
connect(addr);

SocketChannel이 블록모드이다면 소켓 통신과 같이 connect는 블록모드로 작동한다. 넌블록모드라면 connect는 바로 반환한다. 이미 연결되있다면 true, 그렇지 않으면 false를 반환한다. 대부분 false를 반환한다. 따라서 별도의 메서드를 통해서 연결 작업이 마무리 되었는 지 확인해야 한다. 다음은 연결을 확인하는 데 사용하는 메서드이다.

- boolena finishConnect() : 서버와 연결되있다면 true, 진행중이라면 false를 반환한다. 

- boolean isConnected() : 연결이 되었는 지 확인한다.

- boolean isConnectionPending() : 반환값이 true이다면 연결작업이 진행중이라는 뜻이고 끝나면 finishConnect 메서드를 호출한다. 

isConnected() 메서드는 단지 연결 유무만 나타내고 finishConnect() 메서드는 연결이 되었는지 확인 후 연결되지 않았다면 syn 패킷을 다시 서버로 전송해달라고 요청한다.


2. 채널에 작업 내용을 등록하고 채널을 감시한 후 작업 내용에 따라 데이터 송수신 작업을 한다.

client.configureBlocking(false);
clinet.connect(addr);
Selector selector = Selector.open();
client.register(selector, SelectionKey.OP_WRITE);

//5초 동안 기다리면서 채널에서 이벤트가 발생하는 지 여부 확인한다.(=블록된다)
while(selector.select(5000)!=0){
	Iterator<SelectionKey> selectedKeys =
			selector.selectedKeys().iterator();
	while(selectedKeys.hasNext()){
		SelectionKey key = selectedKeys.next();
		selectedKeys.remove();
		
		if(!key.isValid()) continue;
		if(key.isReadable())  //읽기 관련 이벤트
			readData(key);
		else if(key.isWritable())  //쓰기 관련 이벤트
			writeData(key);
	}
}

위 코드의 register()은 selector(채널 관리자)에게 client 채널에 OP_WRITE 작업이 발생하는 지 감시해달라고 하는 것이다. select메서드는 채널을 감시하다가 작업에 해당하는 이벤트가 발생할 경우 작업 내용을 SelectionKey 집합으로 반환한다. 기본적으로 select 메서드는 블록 모드로 작동하는 데 블록되 있는 select를 깨우기 위해서는 wakeup() 메서드를 사용해 select 함수가 0을 리턴하게 하거나 close() 메서드로 채널관리자를 종료시키면 된다. Select 클래스 관련 메서드로 keys()와 selectedKeys() 메서드가 있다. keys()는 register될 때 생성된 SelectionKey 객체를 집합으로 반환하고 selectedKeys()는 채널에서 발생한 이벤트 작업 내용들이 반환된다. 이 두 메서드들은 단일 스레드 모델이지만 블록이 이루어진 루틴이 있는 하나의 스레드에서 실행시키는 경우가 많아 synchronized 메서드를 사용할 일은 별로 없다. 채널에서 발생하는 이벤트는 SelectionKey 객체로 알 수 있다. SelectionKey 객체는 두 종류의 집합으로 나뉜다. register() 메서드를 통해 등록된 작업 내용 집합인 Interest Set 집합, 이벤트가 발생한 작업 내용 집합인 Ready Set 집합으로 나뉜다. SelectionKey 클래스의 주요 메서드는 다음과 같다.

- int interestOps() : register 메서드로 등록했던 작업 내용들을 반환한다.

- SelectionKey interestOps(int ops) : SelectionKey 객체 내 Interest Set에 주어진 작업내용(ops)으로 추가하거나 변경한다.

- int readyOps() : SelectionKey 객체 내 Ready Set에 주어진 작업내용을 반환한다.

- Object attach(Object ob) : SelectionKey 객체에 ob 객체를 첨부한다.

- Object attachment() : 첨부된 객체를 반환한다.

ex)소켓 채널 클라이언트 예

public class NioEchoClient implements Runnable {

	private final String hostAddress;
	private final int port;
	private final Selector selector;
	private static final byte LF=0x0A;
	private static final byte CR=0x0D;
	private String line = null;
	private static int PORT_NUMBER = 6000;
	private static final long TIME_OUT = 3000;
	private final ByteBuffer readBuffer = ByteBuffer.allocate(8192);
	
	public NioEchoClient(String hostAddress, int port) throws IOException{
		this.hostAddress = hostAddress;
		this.port = port;
		this.selector = Selector.open();
	}
	
	public SocketChannel createSocketChannel() throws IOException{
		SocketChannel client = SocketChannel.open(new InetSocketAddress(hostAddress, port));
		client.configureBlocking(false);
		return client;
	}
	
	@Override
	public void run(){
		SocketChannel client = null;
		try{
			client = createSocketChannel();
			
			if(!client.isConnected())
				client.finishConnect();
			
			System.out.println("서버에 전송할 자료를 입력하세요.");
			BufferedReader keyboard = new BufferedReader(
					new InputStreamReader(System.in));
			while((line=keyboard.readLine())!=null){
				if(line.equals("quit")) break;
				
				client.register(selector,  SelectionKey.OP_WRITE);
				
				while(selector.select(TIME_OUT)!=0){
					Iterator<SelectionKey> selectedKeys =
							selector.selectedKeys().iterator();
					while(selectedKeys.hasNext()){
						SelectionKey key = selectedKeys.next();
						selectedKeys.remove();
						
						if(!key.isValid()) continue;
						if(key.isReadable())
							readData(key);
						else if(key.isWritable())
							writeData(key);
					}
				}
			}
		}catch(EOFException e){
			
		}catch(IOException e){
			e.printStackTrace();
		}finally{
			try{
				client.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
		System.out.println("client exits");
	}
	
	private void writeData(SelectionKey key) throws IOException{
		SocketChannel client = (SocketChannel) key.channel();
		readBuffer.clear();
		readBuffer.put(line.getBytes());
		readBuffer.put(LF);   
		//원칙은 CR도 추가해야 하나 유닉스서버의 경우 LF만으로도 허용
		
		//데이터를 버퍼에 입력했다면 다시 읽어 전송시킬 준비를 한다.
		readBuffer.flip();
		client.write(readBuffer);
		
		//서버에서 전송받을 자료를 기다리기 위해 소켓채널에 OP_READ를 등록한다.
		client.keyFor(selector).interestOps(SelectionKey.OP_READ);
	}
	
	private void readData(SelectionKey key) throws IOException{
		SocketChannel client = (SocketChannel) key.channel();
		//서버로부터 데이터를 받기 위해 버퍼를 비운다.
		readBuffer.clear();
		
		int numRead, totalRead=0;
		while((numRead=client.read(this.readBuffer))>0){
			totalRead += numRead;
		}
		
		//read() 메서드가 -1을 반환하면 서버로부터 FIN 패킷을 받은 것을 의미한다.
		if(numRead<0){
			client.close();
			key.cancel();
			throw new EOFException();
		}
		
		readBuffer.flip();
		byte[] receivedData = new byte[totalRead];
		//버퍼를 allocate로 할당했기에 arraycopy 메서드 사용이 가능하다.
		System.arraycopy(readBuffer.array(), 0, receivedData, 0,totalRead);
		System.out.println("Client read " + new String(receivedData).trim());
	}
	
	
	public static void main(String[] args) {
		String host = args[0];
		try{
			NioEchoClient client = new NioEchoClient(host,PORT_NUMBER);
			Thread t = new Thread(client);
			t.start();
			t.join();
		}catch(Exception e){
			e.printStackTrace();
		}
		
	}

}

위 예제에서는 register(sel, OP_CONNECT)를 사용하여 서버의 연결여부를 확인하는 작업이 없다. 서버의 연결 작업 경우 오류가 발생할 시 다른 부분에서 문제 검출이 가능하기에 등록하지 않았다. 일반적으로 OP_READ와 OP_WRITE만 사용한다. 또한 OP_WRITE를 등록하면 select 메서드 호출 즉시 반환해 바로 데이터를 보낼 수 있다.


 ※ 서버 소켓 채널 구현 절차

: 소켓 채널 통신을 클라이언트에 적용하는 데 많이 복잡했고 별다른 이점은 없어 보였다. 소켓 채널 통신은 사실 서버를 위한 기능이여서 클라이언트보다 서버에서 많이 사용한다.

1. 서버소켓채널 객체를 생성하고 넌블록 모드로 만든다. 서버 소켓에 IP와 포트 번호를 bind한다.

ServerSocketChannel server = ServerSocketChannel.open();
ServerSocket socket = server.socket();
socket.bind(addr);
server.configureBlocking(false);

2. 채널관리자 객체를 생성하고 accept 작업을 등록한다.

Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

3. 채널 관리자가 채널을 감시하여 발생된 이벤트를 처리한다.

: 서버는 클라이언트와 달리 accept 이벤트를 처리한다. 클라이언트에 대한 연결 요청이 오면 다음과 같이 소켓채널 객체를 생성하고 해당 객체를 통해 클라이언트와 통신한다.

SocketChannel socketChannel = serverChannel.accept();

4. 작업이 완료되면 close() 메서드를 이용해 채널과 리소스를 해제한다.


ex)소켓 채널 서버 예

public class NioEchoServer extends Thread { private ServerSocketChannel serverChannel; private Selector selector; public static int PORT_NUMBER = 6000; private static final long TIME_OUT = 3000; //allocateDirect가 더 빠르지만 자바 객체와 호환을 위해 allocate를 더 선호한다. private final ByteBuffer buffer = ByteBuffer.allocate(8192); private Boolean loop; public NioEchoServer(int port){ SocketAddress isa = new InetSocketAddress(port); try{ serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); ServerSocket s = serverChannel.socket(); s.bind(isa); selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); loop = true; }catch(IOException e){ e.printStackTrace(); } } @Override public void run(){ try{ while(loop){ int n = selector.select(TIME_OUT); if(n==0) continue; //타임 아웃의 반환값은 0 Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while(it.hasNext()){ SelectionKey key = it.next(); if(!key.isValid()) continue; if(key.isAcceptable()){ acceptData(key); }else if(key.isReadable()){ processData(key); } it.remove(); } } }catch(Exception e){ e.printStackTrace(); } } protected void acceptData(SelectionKey key) throws Exception{ ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); channel.configureBlocking(false); Socket socket = channel.socket(); channel.register(selector, SelectionKey.OP_READ); } protected void processData(SelectionKey key) throws Exception{ if(key == null) return; try{ SocketChannel channel = (SocketChannel) key.channel(); buffer.clear(); int count = channel.read(buffer); //클라이언트가 접속을 끊기 위해 소켓 채널을 닫았을 때 read는 -1 반환 if(count<0){ Socket socket = channel.socket(); channel.close(); key.cancel(); return; } if(count>0){ buffer.flip(); byte[] receivedData = new byte[count]; System.arraycopy(buffer.array(), 0, receivedData, 0, count); String receivedStr = new String(receivedData); echo(channel, receivedStr); } }catch(Exception e){ e.printStackTrace(); try{ key.channel().close(); }catch(IOException ex){ ex.printStackTrace(); } key.selector().wakeup(); } } private void echo(SocketChannel channel, String echo) throws Exception{ buffer.clear(); buffer.put(echo.getBytes()); buffer.flip(); channel.write(buffer); } public static void main(String[] args) throws Exception { int port = PORT_NUMBER; NioEchoServer server = new NioEchoServer(port); server.start(); server.join(); } }


- 클라이언트 종료에 따른 서버의 인식

1. 정상 종료 : 클라이언트가 작업을 완료하여 close()를 통해 소켓 채널을 닫으면 FIN 패킷을 서버에 전송하고 응답패킷을 기다린다. 서버는 FIN 패킷을 받을 때 OP_READ 이벤트를 발생시키고 정상적인 종료로 인식해 read()를 -1 반환한다. 동시에 채널들과 자원들을 해제해야한다.

2. 비정상 종료 : 앱의 오작동, 메모리 부족으로 인해 커널이 강제 종료되거나 사용자에 의해서 'kill' 명령어 같이 의도적으로 종료될 시 RST 패킷을 서버로 전송한다. 그 때 read 메서드로부터 예외가 발생하게 된다. 


- 바이트와 문자열

: 소켓의 스트림과 버퍼 사이에 주고 받은 데이터에 차이가 존재한다. 스트림의 readLine()은 줄바꿈(캐리지리턴, 라인피드)을 제외하고 입력 받은 데이터를 읽어 String 객체로 반환한다. 즉, 줄바꿈의 문자를 통해 데이터를 구분한다. 만약 줄바꿈이 없다면 readLine()은 문자열을 반환하지 않고 계속 블록된다. PrintWriter의 println()의 경우 문장 마지막에 줄바꿈을 포함시켜서 데이터를 보낸다. PrintWriter이 아닌 경우 줄바꿈 데이터를 명시적으로 넣고 flush 메서드를 사용해야 한다. 그러나 소켓 채널의 경우 유니코드를 사용하지 않고 바이트 배열을 사용한다. 따라서 문장의 마지막을 CRLF가 아닌 null(0x00) 값으로 데이터의 끝을 인식한다. 만약 줄바꿈 문자를 포함해서 소켓 채널에 보냈다면 수신 받은 데이터에서 줄바꿈 현상이 발생할 것이다. 따라서 서버에서 trim()을 이용해 CR, LF를 삭제하고 다시 클라이언트 소켓으로 전송한다면 readLine() 메서드는 블록될 것이다. 이에 주의해서 구현해야 한다. 한글 윈도우는 'MS949', 자바는 'UTF-16', 안드로이드는 'UTF-8' 문자셋을 사용한다. 따라서 자바 클라이언트에서 안드로이드 서버로 데이터를 전송하려면 키보드로 받은 문자열을 'MS949' 디코드를 사용하여 자바에서 지원하는 'UTF-16'으로 바꿔주고 이를 안드로이드에서 사용하는 'UTF-8' 문자셋으로 바꾸어야 한다. 디코더와 인코더를 생성하는 방법은 Charset.forName("문자셋").newDecoder()/newEncoder()로 가능하다.


 ※ 안드로이드 소켓 채널 클라이언트 프로그램

: 안드로이드에서 소켓 채널 클라이언트 앱을 구현하려면 복잡하다. 위에서 보다시피 소켓 채널 통신은 주로 서버 목적으로 사용된다. 클라이언트에서 소켓 채널 통신으로 구현하면 더 복잡해지고 성능상 좋다고 느껴지지 않는다. 더군다나 안드로이드의 경우 소켓 채널 관련해서 제한 사항이 많다. 안드로이드 특정 버전 이하에서는 일부 메서드가 정상적으로 작동하지 않는다. write 메서드의 경우 메인 스레드에서 동작할 수 없어 별도의 스레드를 만들어야 한다. 즉, 일반적인 클라이언트에 소켓 채널 통신 방식을 사용하기 복잡한 데 안드로이드는 더 많은 제약사항이 있는 것이다. 그래서 안드로이드에서 클라이언트 통신 프로그램을 작성할 때는 소켓 채널보다 소켓의 사용을 권장한다.




+ Recent posts