Develope/Java / Android2013. 5. 23. 10:41

다음 일정에 있을 프로젝트 준비를 위해,


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
Posted by AsCarion