[Java] Android에서 mqtt 수신시 문자열 깨짐
요약
자바에서 new String(byte[]) 생성자로 String을 만들 때 인코딩을 정확히 적어주는게 몸에 좋다. 요즘은 거의 UTF-8을 쓰니 그냥 아래 형식으로 고정을 하자.
private final Charset CHARSET = StandardCharsets.UTF_8;public byte[] converter(String raw){return raw.toBytes(CHARSET);}public String converter(byte[] raw){return new String(raw, CHARSET);}
문제 현상
mqtt payload를 서버(스프링-톰캣)에서 MQTT(Mosquitto)를 클라이언트(android)로 던질 때 문자열 깨지는 현상이 발생했다. 모든 환경에서 발생하는 것은 아니로 언제나처럼 로컬 테스트에서는 정상 동작하다가 스테이징에 배포 후 앱으로 메세지를 던지면 깨졌다. 서버와 클라이언트에 배포된 바이너리가 동일한 상태. 당황스러운 부분은 앱을 켜두고 로컬 톰캣과 스테이징 톰캣에서 동시에 날리면 로컬에서 던진 메세지는 정상 동작하고, 스테이징에서 던진 메세지는 깨진다는 점이었다. 꺠먹은 소스 중 핵심 부분은 다음과 같다.
server - Spring
MqttClient client;...MqttMessage message = new MqttMessage();message.setPayload(msg.getBytes());client.publish(topic, message);
client - Android
public void messageArrived(String topic, MqttMessage message) throws Exception {String recvMsg = message.toString(); // <- recvMsg가 깨짐
검토
MqttMessage의 toString()을 보면 publisher가 뿌린 데이터를 payload(byte[])로 수신하는데, 해당 payload를 String 생성자의 파라미터로 보내는 것을 알 수 있다.
public String toString() {return new String(payload);}
new String(byte[]) 생성자의 설명은 아래와 같다. 플랫폼의 기본 캐릭터셋을 이용한다는데, 동일한 클라이언트가 다른 캐릭터셋을 디폴트 캐릭터 셋으로 가질 수 있나 하는 의문이 있지만, 뒤쪽 문장이 찝찝하게 서술되어 있다. The behavior of this constructor when the given bytes are not valid in the default charset is unspecified. 안정성을 위해 CharsetDecoder를 쓸 수도 있겠지만 서버/클라이언트를 직접 만질 수 있으니 다른 사고가 나지 않도록 UTF-8로 강제로 밀어 넣기로 하였다.
path public String(byte[] bytes) Constructs a new String by decoding the specified array of bytes using the platform's default charset. The length of the new String is a function of the charset, and hence may not be equal to the length of the byte array. The behavior of this constructor when the given bytes are not valid in the default charset is unspecified. The CharsetDecoder class should be used when more control over the decoding process is required.
수정
메세지를 보내는 서버와 받는 클라이언트에 아래와 같이 캐릭터 셋을 강제로 밀어 넣었다. 정확한 구현을 알 수 없는 MqttMessage.toString() 대신 MqttMessage.getPayload()로 페이로드 바이트 배열을 바로 땡겨서 인코딩 후 사용하였다.
server : Spring
MqttClient client;...MqttMessage message = new MqttMessage();message.setPayload(msg.getBytes(StandardCharsets.UTF_8));client.publish(topic, message);
client : Android
public void messageArrived(String topic, MqttMessage message) throws Exception {String recvMesgUTF = new String( message.getPayload(), StandardCharsets.UTF_8);Log.e("메세지 확인( UTF-8 ): ", "Message Arrived : " + recvMesgUTF);
결론
잘 돌아가는 것 같다. public String(byte[] bytes) 링크 말고 public String(byte[] bytes, Charset charset) 링크 를 쓰자. JDK 버전을 봐도 전자는 1.1, 후자는 1.6부터 지원한다. ㅋㅋㅋ
참참고
한글 "ㅁㄴㅇㄹ"를 인코딩하면 아래와 같은 바이트 스크림을 얻을 수 있다. 심심하면 만들어보자.
String msg = "ㅁㄴㅇㄹ";byte[] valid_utf_8 = msg.getByte("UTF-8");byte[] invalid_utf_8 = new String(msg.getByte("UTF-8"), "EUC-KR").getBytes(); //UTF-8문자열을 EUC-KR로 강제 인코딩System.out.println("Valid Message = " + Arrays.toString(valid_utf_8) );System.out.println("Invalid Message = " + Arrays.toString(invalid_utf_8) );출력Valid Message = [-29, -123, -127, -29, -124, -76, -29, -123, -121, -29, -124, -71]Invalid Message = [-17, -65, -67, -17, -65, -67, -17, -65, -67, -17, -65, -67, -17, -65, -67, -17, -65, -67, -17, -65, -67, -17, -65, -67]
끝
끝.
참고
- 링크 : 큰 흐름은 요정도만 이해하면 충분.