다음 일정에 있을 프로젝트 준비를 위해,
Mock-up 으로 만들어 둔 서버와 통신을 하는 안드로이드 테스트 클라이언트 어플리케이션을 구현했다.
이미 기본 인터페이스 및 패킷 구조는 C 기반으로 구현이 되어 있는 상황이라,
크게 준비할 것은 많이 없었다.
하지만 패킷을 송/수신 하는데 있어서 제일 걸리던 문제점은 Endian 차이였다.
자세한건 이전에 포스팅 한 내용을 참고 하자.
Big-endian / Little-endian 차이점
1.매니패스트 권한 추가
일단, 안드로이드에서 소켓통신을 하기 위해서는, 권한을 주어야 한다.
매니패스트에 다음을 추가하도록 하자.
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
2. 소켓 통신을 위한 클래스
import java.net.InetAddress; // 인터넷 주소 처리 import java.net.Socket; // Socket 클래스 사용 import java.nio.ByteBuffer; // Byte Allocation 및 처리 import java.io.DataInputStream; import java.io.DataOutputStream;
3. 소켓 이용
안드로이드에서의 Socket 은 스레드로 구현하였다.
(서버에 지속적으로 Connection 되어 있어야 하므로...)
Activity 에서 직접 소켓으로 통신을 시도하면, Exception 이 발생한다.
알아보니, ICS 이상? 부터인가 안정성을 위해서 그렇게 변경됬다고 하던... 정확히는 검색이 필요.
4. 핸들러 이용
소켓 클래스의 스레드에서 각 Activity와의 원활한 통신을 위해,
데이터의 송/수신에 대한 Flag를 미리 define 하여 핸들러로 처리하도록 하였다.
5. 패킷 생성 및 구성
여기서부터 중요.
MFC 기반의 서버에서는 구조체를 사용하여 미리 패킷의 구조를 정의할 수 있었지만,
Java 에는 구조체가 없다 !!! 젠장.. 그래서 다음과 같은 방식을 사용하였다.
5-1. 구조체와 동일한 클래스를 Serialize
import java.io.Serializable; public static class HeaderPacket implements Serializable { /** * */ private static final long serialVersionUID = 6370094634572066507L; public int ClientID; public int ClientType; public int Command; public int BodyLen; public HeaderPacket() { ClientID = 0; ClientType = 0; Command = 0; BodyLen = 0; } } public static final class HeaderPacketLen = 16;
위와 같이 MFC에서의 패킷 처리 구조체와 동일한 자료형을 지닌 클래스를 만든 후, Serialize 해준다.
또한, Serialize 된 클래스에 대해서 Serialize UID 를 부여해준다.
부여해주는 방법은 뭐.. 검색신공 +_+
그리고 클래스의 Entity의 크기에 대한 Define 또한 const... java 에서는 final 로 지정해준다.
5-2. 패킷 구성 시 클래스 Entity 별로 endian 변환
헤더 패킷을 보낼 때, 각 Entity 의 순서는 지키되, byte 의 순서를 Little-endian 으로 맞춰 줘야 한다.
그러기 위해서 구현 된 메소드는 다음과 같다.
import java.nio.ByteBuffer; import java.nio.ByteOrder; // Change int to Byte[] order by little-endian public static byte[] int2byte(int value) { ByteBuffer buf = ByteBuffer.allocate(Integer.SIZE / 8); buf.putInt(value); buf.order(ByteOrder.LITTLE_ENDIAN); byte[] byteArry = buf.array(); return ChangeByteOrder(byteArry); } // Change Byte Order Big Endian to Little Endian public static byte[] ChangeByteOrder(byte[] value) { int idx = value.length; byte[] temp = new byte[idx]; for (int i = 0; i < idx; i++) { temp[i] = value[idx - (i + 1)]; } return temp; }
int2byte 메소드에서, ByteBuffer 를 이용하여 int 자료형의 크기 (4 byte)를 먼저 allocation 해주고,
데이터를 putInf() 한 후, byte 형으로 이전한다.
그 이후 그 바이트 데이터의 Order를 바꿔준다.
예를 들어, int 형의 10000 이라는 데이터를 바꾸게 되면,
buf 에는 의 형태로 값이 저장된다. (Hex 값인건 알고 있으리라 본다.)
이후, bytearry 변수에도 똑같이 값이 복사 될 것이며,
ChangeByteOrder 메소드에서 각 배열의 값의 순서를 Swap 해준다.
그러면 MFC 에서 변환 없이 인식할 수 있는 Little-endian 형태의 int 형 byte가 된다.
(물론, 이 값을 Java 에서는 전혀 다른 값으로 인식한다. 270991360...)
5-3. 패킷 구성
아래와 같이 각 Entity에 대한 값을 모두 바꿔서 순서대로 넣어주도록 하자.
// Make Allocated ByteBuffer (Defined Header Size) ByteBuffer buf = ByteBuffer.allocate(HeaderPacketLen); // reforming byteOrder. buf.put(int2byte(_ClientId); buf.put(int2byte(_ClientType); buf.put(int2byte(_Command); buf.put(int2byte(_BodyLen); // Make Header byte[] headerbuf = buf.array(); // Send Header DataOutputStream outStream = new DataOutputStream(this.socket.getOutputStream()); outStream.write(headerbuf); outStream.flush();
이런 방식으로 일일히 데이터를 맞춰서 넣어줬다.
실제로는, 메소드를 모듈화 하여 위 내용을 편하게 수행하도록 구현하였다.
6. 패킷 수신
서버로부터 같은 형식의 Header 패킷을 수신하였을 떄의 처리를 해주자.
이번에는 반대로 Little-endian 으로 수신 된 데이터를 Big-endian으로 변환하여야 한다.
여기에 사용할 변환 메소드를 구현해 보았다.
// change byte[] to int public static int byte2int(byte[] value, int startpos, int endpos) { ByteBuffer buf = ByteBuffer.allocate(value.length); buf.put(value); byte[] tmpbuf = new byte[(endpos - startpos) + 1]; buf.position(startpos); buf.get(tmpbuf); String HexValue = byteArrayToHex(ChangeByteOrder(tmpbuf)); return Integer.parseInt(HexValue, 16); } // byte[] to hex public static String byteArrayToHex(byte[] arry) { if(arry == null || arry.length == 0) { return null; } StringBuffer sb = new StringBuffer(arry.length * 2); String tmphex; for(int i = 0; i < arry.length; i++) { tmphex = "0" + Integer.toHexString(0xff & arry[i]); sb.append(tmphex.substring(tmphex.length() - 2)); } return sb.toString(); }
byte2int 메소드는 수신된 byte 데이터 및, Entity 별 위치 오프셋 정보를 가지고 처리하도록 하였다.
Line 3: ByteBuffer 형의 buf 에 데이터를 담는다.
Line 6: byte Array를 필요한 크기만큼 (오프셋 만큼) 선언한다.
Line 8: buf 의 위치를 오프셋으로 지정한다.
Line 9: 지정된 위치부터 byte array 크기만큼 데이터를 가져온다.
Line 11: String 형태로 byte 데이터를 Order를 변환하여 저장한다.
Line 12: String 으로 저장된 Hex 값을 Int 형으로 반환한다.
이런 식으로 진행하면 위의 송신 시 변환 과정을 역으로 진행하게 된다.
단, byte 정보를 hex로 변환하여 String 으로 처리하는 것이 조금 다르다.
마지막으로 필요한 데이터의 위치만큼만 받아오도록 하면 된다.
여기서는 command 데이터만 받아와 보도록 하겠다.
// Recv Header DataInputStream inStream = new DataInputStream(this.socket.getInputStream()); byte[] readheader = new byte[HeaderPacketLen]; inStream.readFully(readheader); // Check Ack Command int ackCommand = Util.byte2int(readheader, 8, 11); if(ackCommand == 10000) { // Set Client ID this._ClientId = Util.byte2int(readheader, 0, 3); return true; }
이런식으로 필요한 데이터의 송/수신을 맞춰보았다.
안드로이드로 하는게 처음이라 어찌보면 빙빙 돌아가는걸수도 있고 불필요한 부분도 있겠지만,
하다보면 더욱 발전하리라 생각하며, 이렇게 구현하기까지 딱 맞게 필요한 정보를 찾기가 너무 힘들었다 ㅡㅜ
그래서 나중에라도 다시금 찾아볼 수 있도록 정리 하였다.
역시 인생은 공부의 연속이구나...
'Develope > Java / Android' 카테고리의 다른 글
ms949 to utf-8 convert (1) | 2015.03.17 |
---|