在网络编程中经常会遇到乱码问题。这是一个非常常见的问题,这一篇将从原理上彻底分析这个原因
我们都知道网络编程中,数据只能通过byte形式进行传递,所以在发送数据的时候先要将数据进行编码,也就是将需要发送的信息转换成byte数组。在接收数据时候需要进行解码,也就是将byte数组还原成信息的过程。
说到编解码就不得不提编码集,目前已有的编码集如下
ASCII:美国标准信息交换表:7bit来表示一个字符,总共可以表示128种字符 2的七次方,由于字符数不够就扩展出下面的
ISO8859-1:拉丁码表,欧洲码表:8bit表示一个字符,即一个字节,共计256个字符 2的八次方 由于不能表示中文,所以就演化出下面的
GB2312:中国的中文编码表: 2个字节来表示一个汉字
GBK:中国的中文编码表升级,包含了生僻字
GB18030:GBK的取代版本
BIG5:通用于香港、台湾地区的繁体字编码方案
Unicode:国际标准码,融合了多种文字,所有的文字都用两个子节来表示,Java语言使用的就是该码表Unicode虽然可以表示世界上全部的文字,但是容量太大,于是诞生了UTF
UTF 全称:Unicode Translation Format
UTF本质是一种存储方式
UTF-8 UTF-16 UTF-32都是unicode的一种实现方式
UTF-8会用三个字节来表示一个汉字
下面有一段程序:
-------------------------------------
RandomAccessFile randomAccessFile = new RandomAccessFile("in.txt","r");
RandomAccessFile randomAccessFile1 =new RandomAccessFile("out.txt","rw");
long inputLength = new File("in.txt").length();
FileChannel inputFileChannel = randomAccessFile.getChannel();
FileChannel outputFileChannel = randomAccessFile1.getChannel();
MappedByteBuffer inputData = inputFileChannel.map(FileChannel.MapMode.READ_ONLY,0,inputLength);
System.out.println("--------------");
Charset.availableCharsets().forEach((k,v)->{
System.out.println(k + ":" + v);
});
Charset charset = Charset.forName("iSO8859-1"); ------------------1
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();
CharBuffer charBuffer = decoder.decode(inputData);-----------------2
ByteBuffer outputData = encoder.encode(charBuffer);-----------------3
outputFileChannel.write(outputData);
randomAccessFile.close();
randomAccessFile1.close();
-----------------------------------
in.txt 是utf-8格式,里面有『你好』两个中文,上述程序out.txt是不会出现乱码的,即使把1中的utf-8改成其他编码格式也不会出现乱码。下面来分析下原因
在int.txt中,你好两个中文 的编码是 AB AC AD AE AF AG
因为utf-8中试三个字符表示一个中文
CharsetDecoder decoder = charset.newDecoder();对这六个字符进行编码,也就是把AB 当做一个文字,AC当做一个文字,这里肯定是乱码的,如果在2之后把charbuffer中的内容打印出来肯定是乱码的两个字符集
但是在网络中传输,会把六个字符都传过去,并用iSO8859-1进行编码,这时候也是乱码的,但是我们打开utf-8编码的out.txt文件时候,AB AC AD 被表示成你 AE AF AG被表示成好,所以不会乱码。相反,如果我们把out.txt格式改成GBK,那肯定是乱码
如果将3改Charset.forName("utf-8").encode(charBuffer).那么又会出现乱码,因为解码的时候用和编码不匹配的字符集进行,所以会出现乱码。
-------------------------------------------------------------
接下去说下零拷贝:
传统IO中,数据要从硬盘中发送出去,需要经过四次拷贝和四次上下文切换
JVM发出read() 系统调用。
OS上下文切换到内核模式(第一次上下文切换)并将数据读取到内核空间缓冲区。(第一次拷贝:hardware ----> kernel buffer)
OS内核然后将数据复制到用户空间缓冲区(第二次拷贝: kernel buffer ——> user buffer),然后read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)。
JVM处理代码逻辑并发送write()系统调用。
OS上下文切换到内核模式(第三次上下文切换)并从用户空间缓冲区复制数据到内核空间缓冲区(第三次拷贝: user buffer ——> kernel buffer)。
write系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换)。将内核空间缓冲区中的数据写到hardware(第四次拷贝: kernel buffer ——> socketBuffer)。
第五次拷贝:socketBuffer ----->hardware
总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。显然在这个用例中,从内核空间到用户空间内存的复制是完全不必要的,因为除了将数据转储到不同的buffer之外,我们没有做任何其他的事情。所以,我们能不能直接从hardware读取数据到kernel buffer后,再从kernel buffer写到目标地点不就好了。为了解决这种不必要的数据复制,操作系统出现了零拷贝的概念。注意,不同的操作系统对零拷贝的实现各不相同。在这里我们介绍linux下的零拷贝实现。
-------------------------------------
Java Nio采用了上述图的形式。 省去了两次用户空间到操作系统内核空间的拷贝。
具体过程:
发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard driver ——> kernel buffer)。
然后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer ——> socket buffer)。
sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)。
java nio中有
DirectByteBuffer 和
MappedByteBuffer
数据不在堆内,而是堆外内存,也就是内核空间
上述过程就只有两次上下文切换和三次拷贝
通过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。
你可能会说操作系统仍然需要在内核内存空间中复制数据(kernel buffer —>socket buffer)。 是的,但从操作系统的角度来看,这已经是零拷贝,因为没有数据从内核空间复制到用户空间。 内核需要复制的原因是因为通用硬件DMA访问需要连续的内存空间(因此需要缓冲区)。 但是,如果硬件支持scatter-and-gather,这是可以避免的。
--------------------------------
带有DMA收集拷贝功能的sendfile实现的I/O
发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。
sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。
带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。
下面有个问题可以参考下
https://www.zhihu.com/question/57374068