在使用ByteBuffer时,使用UTF-8的中文乱码问题
2019-08-19 本文已影响0人
九思而行
场景
在nio使用中,要使用ByteBuffer来接受信息,但是当nio传过来的ByteBuffer大于接受ByteBuffer时,要分多次接受,然后统一转为字符串,但是发现在传输中文时,出现部分中文乱码。
原因
经过debug调查,是在临界值(接受的ByteBuffer的size)的位置,可能会发生中文乱码,我们都知道,在使用UTF-8时因为中文一般情况下会占据三个字节,有个个别陌生字符可能还会更多(4-6的字节),所以在接受byteBuffer最后的一个或多个字节可能不完整,也就是一个完整的汉字因为被分成多个字节的原因,又因为获取时不是全部获取,导致中文不完整,从而在转成char类型时,出现乱码。
解决
既然知道了原因,那么解决也就不难了(对于大牛来说,我不是!),上网查了好久都没有找到一个完整说清这个问题的帖子,最后只能自己来了。
一开始我想自己实现的.......,但是被复杂的进制转换和如何确定当前中文的具体字节给拦住了0.0!
后来才发现伟大的jdk已经实现了\笑着哭\:
package sun.nio.cs
//sun.nio.cs.UTF_8:
private CoderResult decodeBufferLoop(ByteBuffer src,CharBuffer dst){
int mark = src.position();
int limit = src.limit();
while (mark < limit) {
int b1 = src.get();
if (b1 >= 0) {
// 1 byte, 7 bits: 0xxxxxxx
if (dst.remaining() < 1)
return xflow(src, mark, 1); // overflow
dst.put((char) b1);
mark++;
} else if ((b1 >> 5) == -2 && (b1 & 0x1e) != 0) {
// 2 bytes, 11 bits: 110xxxxx 10xxxxxx
if (limit - mark < 2|| dst.remaining() < 1)
return xflow(src, mark, 2);
int b2 = src.get();
if (isNotContinuation(b2))
return malformedForLength(src, mark, 1);
dst.put((char) (((b1 << 6) ^ b2)
^
(((byte) 0xC0 << 6) ^
((byte) 0x80 << 0))));
mark += 2;
} else if ((b1 >> 4) == -2) {
// 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx
int srcRemaining = limit - mark;
if (srcRemaining < 3 || dst.remaining() < 1) {
if (srcRemaining > 1 && isMalformed3_2(b1, src.get()))
return malformedForLength(src, mark, 1);
return xflow(src, mark, 3);
}
int b2 = src.get();
int b3 = src.get();
if (isMalformed3(b1, b2, b3))
return malformed(src, mark, 3);
char c = (char)
((b1 << 12) ^
(b2 << 6) ^
(b3 ^
(((byte) 0xE0 << 12) ^
((byte) 0x80 << 6) ^
((byte) 0x80 << 0))));
if (Character.isSurrogate(c))
return malformedForLength(src, mark, 3);
dst.put(c);
mark += 3;
} else if ((b1 >> 3) == -2) {
// 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
int srcRemaining = limit - mark;
if (srcRemaining < 4 || dst.remaining() < 2) {
b1 &= 0xff;
if (b1 > 0xf4 ||
srcRemaining > 1 && isMalformed4_2(b1, src.get() & 0xff))
return malformedForLength(src, mark, 1);
if (srcRemaining > 2 && isMalformed4_3(src.get()))
return malformedForLength(src, mark, 2);
return xflow(src, mark, 4);
}
int b2 = src.get();
int b3 = src.get();
int b4 = src.get();
int uc = ((b1 << 18) ^
(b2 << 12) ^
(b3 << 6) ^
(b4 ^
(((byte) 0xF0 << 18) ^
((byte) 0x80 << 12) ^
((byte) 0x80 << 6) ^
((byte) 0x80 << 0))));
if (isMalformed4(b2, b3, b4) ||
// shortest form check
!Character.isSupplementaryCodePoint(uc)) {
return malformed(src, mark, 4);
}
dst.put(Character.highSurrogate(uc));
dst.put(Character.lowSurrogate(uc));
mark += 4;
} else {
return malformed(src, mark, 1);
}
}
return xflow(src, mark, 0);
}
上面就是jdk1.8对UTF-8的实现,这个是核心,还有一些其他的方法调用,有兴趣的同学可以看看源码的实现。
但是我们要调用的并不是这个方法,而是他的父类CharsetDecoder的decode方法:
package java.nio.charset;
//java.nio.charset.CharsetDecoder
public final CoderResult decode(ByteBuffer in, CharBuffer out,boolean endOfInput){
int newState = endOfInput ? ST_END : ST_CODING;
if ((state != ST_RESET) && (state != ST_CODING)
&& !(endOfInput && (state == ST_END)))
throwIllegalStateException(state, newState);
state = newState;
for (;;) {
CoderResult cr;
try {
cr = decodeLoop(in, out);
} catch (BufferUnderflowException x) {
throw new CoderMalfunctionError(x);
} catch (BufferOverflowException x) {
throw new CoderMalfunctionError(x);
}
if (cr.isOverflow())
return cr;
if (cr.isUnderflow()) {
if (endOfInput && in.hasRemaining()) {
cr = CoderResult.malformedForLength(in.remaining());
// Fall through to malformed-input case
} else {
return cr;
}
}
CodingErrorAction action = null;
if (cr.isMalformed())
action = malformedInputAction;
else if (cr.isUnmappable())
action = unmappableCharacterAction;
else
assert false : cr.toString();
if (action == CodingErrorAction.REPORT)
return cr;
if (action == CodingErrorAction.REPLACE) {
if (out.remaining() < replacement.length())
return CoderResult.OVERFLOW;
out.put(replacement);
}
if ((action == CodingErrorAction.IGNORE)
|| (action == CodingErrorAction.REPLACE)) {
// Skip erroneous input either way
in.position(in.position() + cr.length());
continue;
}
assert false;
}
}
上面是我们要调用的方法,该方法确定了相应的错误机制和对应处理办法,并且会返回一个CoderResult结果集给我们。
好了,废话不多说,直接上代码了:
public static void main(String[] args) throws Exception {
Charset charset = null;
CharsetDecoder decoder = null;
String charsetName = "UTF-8";
int capacity = 10;
charset = Charset.forName(charsetName);
decoder = charset.newDecoder();
String s ="客户端发送dsad德生科技电脑fdas上考虑迪士尼年少弗拉门发生ofjam打什么的即破发麦克 ‘;打, 饭哦按都";
byte[] bytes = s.getBytes(charsetName);
//模拟接收的ByteBuffer size 10
ByteBuffer byteBuffer = ByteBuffer.allocate(capacity);
//用于临时存放Bytebuffer转换后的字符
CharBuffer charBuffer = CharBuffer.allocate(capacity);
//用于连接展示字符串
StringBuilder sb = new StringBuilder();
int i = 0;
while(true){
byteBuffer.put(bytes[i]);
i++;
if(byteBuffer.remaining()==0||i==bytes.length){
byteBuffer.flip();
CoderResult coderResult;
if(i!=bytes.length){
coderResult = decoder.decode(byteBuffer,charBuffer,false);
}else{
coderResult = decoder.decode(byteBuffer,charBuffer,true);
}
//有错误
if(coderResult.isError()){
coderResult.throwException();
}
charBuffer.flip();
sb.append(charBuffer);
charBuffer.clear();
byteBuffer.compact();
}
//退出循环
if(i == bytes.length){
break;
}
}
System.out.println(sb);
}
上面是一个小例子,相信小伙伴能够根据这个小例子实现自己的功能啦!