I/O早期的問題
Java 1.4之前的早期版本,Java對I/O的支持並不完善,開發人員在開發高性能I/O程序的時候,會面臨以下挑戰和困難
- 沒有數據緩衝區,I/O性能存在問題;
- 沒有C或者C++中的Channel概念,只有輸入和輸出流;
- 同步阻塞式I/O通信(BIO), 通常會導致通信線程被長時間阻塞;
- 支持的字符集有限,硬件可移植性不好;
Linux 網絡I/O模型簡介
Linux的內核將所有外部設備都看做一個文件來操作,對一個文件的讀寫操作會調用內核提供的系統命令,返回一個file descriptor (fd, 文件描述符)。 而對一個socket的讀寫也會有相應的描述符,稱爲socketfd (socket 描述符),描述符就是一個數字,它指向內核中的一個結構體(文件路徑,數據區等一些屬性)。根據UNIX網絡編程對I/O模型的分類,UNIX提供了5種I/O模型,分別如下:
阻塞I/O模型:最常用的I/O模型,缺省情形下,所有文件操作都是阻塞的。
以套接字接口爲例來講解此模型:在進程空間中調用recvfrom,其系統調用直到數據包到達且被複制到應用進程的緩衝區中或者發生錯誤時纔會返回,在此期間會一直等待,進程在從調用recvfrom開始到它返回的整段時間內都是被阻塞的,因此被稱爲阻寨I/O模型,如下圖:
非阻塞I/O模型: recvfrom 從應用層到內核的時候,如果該緩衝區沒有數據的話,就直接返回一個EWOULDBLOCK錯誤,一般都會對非阻塞I/O模型進行輪詢檢查這個狀態,看內核是不是有數據到來。如下圖:
I/O複用模型: Linux提供select/poll, 進程通過將一個或多個fd傳遞給select或poll系統調用,阻塞在select 操作上,這樣select/poll可以幫我們偵測多個fd是否處於就緒狀態。select/poll 是順序掃描fd是否就緒,而且支持的fd數量有限,因此它的使用受到了一些制約。Linux還提供了一個epoll系統調用,epoll使用基於事件驅動方式代替順序掃描,因此性能更高。當有fd就緒時,立即回調函數rollback,如下圖:
信號驅動I/O模型:首先開啓套接口信號驅動I/O功能,並通過系統調用sigaction執行一個信號處理函數(此係統調用立即返回,進程繼續工作,它是非阻塞的)。當數據準備就緒時,就爲該進程生成一個SIGIO信號,通過信號回調通知應用程序調用recvfrom,來讀取數據,並通知主循環函數處理數據,如下圖:
異步I/O:告知內核啓動某個操作,並讓內核在整個操作完成後(包括將數據從內核複製到用戶自己的緩衝區)通知我們。這種模型與信號驅動模型的主要區別是:信號驅動I/O由內核通知我們何時可以開始一個I/O操作;異步I/O 模型由內核通知我們I/O操作何時已經完成,如下圖:
I/O多路複用技術
在I/O編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或者I/O多路複用技術進行處理。I/O多路複用技術通過把多個I/O的阻塞複用到同一個select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路複用的最大優勢是系統開銷小,系統不需要創建和維護額外進程或者線程,降低了系統的工作量,節省系統資源,I/O多路複用的主要應用場景如下。
- 服務器需要同時處理多個處於監聽狀態或者多個連接狀態的套接字;
- 服務器需要同時處理多種網絡協議的套接字。
目前支持I/O多路複用的系統調用有select、 pselect、 poll、 epoll,在Linux網絡編程過程中,很長一段時間都使用select做輪詢和網絡事件通知,然而select 的一些固有缺陷導致了它的應用受到了很大的限制,最終Linux 選擇了epoll, epoll 與select 的原理比較類似,爲了克服select的缺點,epoll和selet的主要區別如下:
1.epoll支持一個進程打開的socket描述符( FD )不受限制(僅受限於操作系統的最大文件句柄數)。
select最大的缺陷就是單個進程所打開的FD是有一定限制的,它由FD_ SETSIZE設置,默認值是1024。 對於那些需要支持上萬個TCP連接的大型服務器來說顯然太少了。可以選擇修改這個宏然後重新編譯內核,不過這會帶來網絡效率的下降。也可以通過選擇多進程的方案(傳統的Apache方案)解決這個問題,儘管在Linux上創建進程的代價比較小,但仍舊是不可忽視的。另外,進程間的數據交換非常麻煩,對於Java 來說,由於沒有共享內存,需要通過Socket通信或者其他方式進行數據同步,這帶來了額外的性能損耗,增加了程序複雜度,而epoll並沒有這個限制,它所支持的FD上限是操作系統的最大文件句柄數,這個數字遠遠大於1024。例如,在1GB內存的機器上大約是10 萬個句柄左右,具體的值可以通過cat /proc/sys/fs/file- max察看,通常情況下這個值跟系統的內存關係比較大。
2.epoll的I/O效率不會隨着FD數目的增加而線性下降
傳統select/poll 的另一個致命弱點,就是當擁有一個很大的socket集合時,由於網絡延時或者鏈路空閒導致任一時刻只有少部分的socket是“活躍”的,但是select/poll 每次調用都會線性掃描全部的集合,導致效率呈現線性下降。而epoll只會對“活躍”的socket進行操作,這是因爲在內核實現中,epoll是根據每個fd上面的callback函數實現的。那麼,只有“活躍”的socket 纔會去主動調用callback函數,其他idle狀態的socket則不會。
3.epoll使用mmap加速內核與用戶空間的消息傳遞
無論是select、poll還是epoll都需要內核把FD消息通知給用戶空間,因此避免不必要的內存複製就顯得非常重要,epoll是通過內核和用戶空間mmap同一塊內存來實現的。
4.epoll 的API更加簡單
包括創建一個epoll描述符、添加監聽事件、阻塞等待所監聽的事件發生、關閉epoll描述符等。
傳統BIO編程
網絡編程的基本模型就是Client/Service模型,也就是兩個進程之間的通信,其中服務端提供IP地址和端口,客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連接,連接建立成功,雙方通過網絡套接字Socket通信
BIO通信模型圖
BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,當收到客戶端的連接請求後爲每一個客戶端創建一個新的線程進行鏈路處理,處理完成後通過輸出流應答給客戶端,線程銷燬,這就是典型的一請求一應答模型,具體如下:
BIO模型缺點:
缺乏彈性伸縮能力,當客戶端併發量增加後,服務端線程和客戶端數成1:1正比,導致線程數急劇增加,系統性能下降,發生線程堆棧溢出,新建線程失敗等問題,最終導致宕機或者死機
BIO的代碼實現
服務端代碼實現
//服務端
public static void main(String[] args) throws Exception{
ServerSocket server = new ServerSocket(9999); //創建服務器
while(true) {
final Socket socket = server.accept(); //接受客戶端的請求
new Thread() {
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("收到請求");
System.out.println(br.readLine());
ps.println("收到請求");
System.out.println(br.readLine());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
客戶端代碼實現:
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 9999); //創建Socket指定ip地址和端口號
InputStream is = socket.getInputStream(); //獲取輸入流
OutputStream os = socket.getOutputStream(); //獲取輸出流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
PrintStream ps = new PrintStream(os);
System.out.println(br.readLine());
ps.println("發送請求");
System.out.println(br.readLine());
ps.println("發送請求");
socket.close();
}
NIO編程
NIO官方的解釋意思是:New I/O,相對於JDK1.4之前的阻塞I/O,New I/O 類庫的目標就是讓java支持非阻塞I/O,所以也稱之爲非阻塞I/O(Non-block I/O),之所以稱之爲異步非阻塞I/O是因爲相比於之前的同步阻塞讀和寫,他是異步的,但是按照嚴格的UNIX網絡編程模型區分和JDK的實現來分,NIO不能稱之爲異步非阻塞I/O,在早期的JDK1.4版本之前,JDK的selector基於select/poll模型實現,它是基於I/O複用技術實現的非阻塞I/O,而不是異步I/O,在JDK1.5之後的版本中,優化使用epoll來替代select/poll,但是上層API沒有發生變化,沒有改變I/O模型,只是性能的優化,直到JDK1.7提供了NIO2.0新增了異步套接字通道,此時才真正實現異步I/O,在異步I/O操作的時候傳遞信號變量當操作完成後會回調相關方法,異步I/O也被稱爲AIO。
NIO類庫簡介
緩衝區Buffer
Buffer是一個對象,包含一些要寫入或者讀出的數據,在NIO類庫中,所有數據都是用緩衝區處理的,讀取數據時,它是直接讀到緩衝區中,寫入數據時,也是寫入緩衝區,任何時候訪問NIO中的數據都是通過緩衝區進行操作,緩衝區實質就是一個數組(通常是字節數組ByteBuffer),提供了對數據的結構化訪問以及維護讀寫位置等信息
最常用的緩衝區就是ByteBuffer,一個ByteBuffer提供了一組功能用於操作byte數組。基本每一種java基本類型(除了Boolean)都對應一種緩衝區
通道Channel
Channel是一個通道,網絡數據通過Channel讀取和寫入。通道與流的不同之處在於通道是雙向的,流是單向的,通道用於讀、寫或者二者同時進行,通道的頂層是Channel接口,實際上Channel可以分爲兩大類:用於網絡讀寫的SelectableChannel和用於文件操作的FileChannel,後面提到的ServerSockerChannel和SocketChannel都是SelectableChannel的子類
多路複用Selector
多路複用器提供了選擇已經就緒的任務的能力,簡單說:Selector會不斷輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作,一個多路複用Selector可以同時輪詢多個Channel,由於JDK使用了epoll()代替了傳統的select實現,所以它沒有最大連接句柄1024/2048的限制,這也就意味着只需要一個線程負責Selector的輪詢
NIO入門(以Netty權威指南中的時間服務器爲例)
NIO序列圖
NIO服務端開發
public class TimeServerHandle implements Runnable {
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
/**
* 初始化多路複用器、綁定監聽端口
*
* @param port
*/
public TimeServerHandle(int port) {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
// 多路複用器關閉後,所有註冊在上面的Channel和Pipe等資源都會被自動去註冊並關閉,所以不需要重複釋放資源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 處理新接入的請求消息
if (key.isAcceptable()) {
// 接收新的連接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 讀取數據
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server receive order :"
+ body);
String currentTime = "QUERY TIME ORDER"
.equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString()
: "BAD ORDER";
doWrite(sc, currentTime);
} else if (readBytes > 0) {
// 對端鏈路關閉
key.cancel();
sc.close();
} else
; // 讀到0字節,忽略
}
}
}
private void doWrite(SocketChannel channel, String response)
throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
public static void main(String[] args) {
TimeServerHandle timeServerHandle = new TimeServerHandle(9999);
new Thread(timeServerHandle," NIO-SERVER").start();
}
}
NIO客戶端開發
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
// 多路複用器關閉後,所有註冊在上面的Channel和Pipe等資源都會被自動去註冊並關閉,所以不需要重複釋放資源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 判斷是否連接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else
System.exit(1);// 連接失敗,進程退出
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is : " + body);
this.stop = true;
} else if (readBytes < 0) {
// 對端鏈路關閉
key.cancel();
sc.close();
} else
; // 讀到0字節,忽略
}
}
}
private void doConnect() throws IOException {
// 如果直接連接成功,則註冊到多路複用器上,發送請求消息,讀應答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
private void doWrite(SocketChannel sc) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining())
System.out.println("Send order 2 server succeed.");
}
public static void main(String[] args) {
TimeClientHandle timeClientHandle = new TimeClientHandle(null, 9999);
new Thread(timeClientHandle," NIO-CLIENT").start();
}
}
分別啓動服務端和客戶端,執行結果:
以上即是NIO編程的基本代碼,但是它並沒有考慮“半包讀”和“半包寫”。
NIO編程的優點
- 客戶端發起連接操作是異步的,可以通過在多路複用器註冊OP_CONNECT等待後續結果,客戶端不會被阻塞
- ScoketChannel的讀寫都是異步的,如果沒有可讀寫的數據,它不會同步等待,直接返回,這樣I/O通信線程可以處理其他鏈路
- JDK的Selector在Linux等主流操作系統上通過epoll實現,沒有連接句柄數的限制(只受限操作系統最大句柄數或單個進程的句柄限制),這意味着一個Selector線程可以同事處理成千上萬個客戶端連接,而且性能不會隨着客戶端的增加而線性下降,非常適合做高性能、高負載的網絡服務器
AIO編程
不同I/O模型的對比
同步阻塞I/O | 僞異步I/O | 非阻塞I/O | 異步I/O(AIO) | |
---|---|---|---|---|
客戶端個數:I/O線程數 |
1:1 |
M:N(其中M可以大於N) |
M:1(1個I/O線程處理多個客戶端連接) |
M:0(不需要啓動額外的I/O線程,被動回調) |
I/O類型(阻塞) |
阻塞I/O |
阻塞I/O |
非阻塞I/O |
非阻塞I/O |
I/O類型(同步) |
同步I/O |
同步I/O |
同步I/O(I/O多路複用) |
異步I/O |
API使用難度 |
簡單 |
簡單 |
非常複雜 |
複雜 |
調試難度 |
調試難度 |
調試難度 |
調試難度 |
調試難度 |
可靠性 |
非常差 |
差 |
高 |
高 |
吞吐量 |
低 |
中 |
高 |
高 |
Netty應用的搭建
什麼是Netty
Netty 是基於 Java NIO 的異步事件驅動的網絡應用框架,它的健壯性、功能、可定製性和可擴展性在同類框架中首屈一指,得到成百上千商用項目的驗證,Netty 提供了高層次的抽象來簡化 TCP 和 UDP 服務器的編程,使用Netty 可以快速和簡單的開發出一個網絡應用,避免使用複雜繁瑣的JDK NIO類庫。
Netty開發入門
引入Netty開發的相關依賴:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
Netty實現NIO服務端開發
public class NettyServer {
public void bind(int port){
//NioEventLoopGroup是一個線程組,包含一組NIO線程,用於網絡事件處理,實際上就是Reactor線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();//用於服務端接受客戶端的連接
EventLoopGroup workerGroup = new NioEventLoopGroup();//用於SocketChannel的網絡讀寫
try{
//ServerBootstrap對象是Netty用於啓動NIO服務端的輔助啓動類
ServerBootstrap bs = new ServerBootstrap();
bs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)//設置創建的channel
.option(ChannelOption.SO_BACKLOG, 1024)//設置NioServerSocketChannel的TCP參數
.childHandler(new ChildChannelHandler());//綁定I/O事件處理類ChildChannelHandler
ChannelFuture sync = bs.bind(port).sync();//綁定監聽端口並調用同步阻塞方法等待綁定操作完成
sync.channel().closeFuture().sync();//等待服務器鏈路關閉之後main函數才退出
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//釋放連接池資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
//事件處理類ChildChannelHandler,類似Reactor中的Handler
class ChildChannelHandler extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
class TimeServerHandler extends ChannelHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("Time Server receive order:" + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString(): "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//Netty把write方法並不直接將消息寫入到SocketChannel中,調用write方法只是把待發送的消息放到緩衝數組中,
// 調用flush方法纔將消息全部寫道SocketChanel
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//釋放相關句柄等資源
ctx.close();
}
}
public static void main(String[] args) {
new NettyServer().bind(9988);
}
}
Netty實現NIO客戶端開發
public class NetttClient {
public void connect(String host, int port){
//配置客戶端NIO線程組
EventLoopGroup group = new NioEventLoopGroup();
try{
//創建客戶端輔助啓動類Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,Boolean.TRUE)
.handler(new ChannelInitializer<SocketChannel>() {
//創建NioSocketChannel成功之後,進行初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
});
//發起異步連接操作
ChannelFuture sync = bootstrap.connect(host, port).sync();
//等待客戶端鏈路關閉
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//釋放NIO線程組資源
group.shutdownGracefully();
}
}
class TimeServerHandler extends ChannelHandlerAdapter {
private final ByteBuf firstMessage;
public TimeServerHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
//當客戶端和服務端TCP鏈路建立成功之後,Netty的NI線程會調用channelActive方法,發送查詢指定給服務端
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("now is :" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("Unexpected exception frm downstream:" + cause.getMessage());
ctx.close();
}
}
public static void main(String[] args){
new NetttClient().connect("127.0.0.1",9988);
}
}
分別啓動服務端和客戶端執行結果如下:
Time Server receive order:QUERY TIME ORDER
now is :Sat May 30 18:30:18 CST 2020
可以看出相比於傳統的NIO程序,使用Netty開發代碼更加簡潔,開發難度更低,擴展性也更好。在上面的Netty入門的代碼中並沒有考慮讀半包等問題。當系統壓力突增或者發送大報文之後,就會存在粘包/拆包的問題,可能導致解碼錯位甚至錯誤,導致程序不能正常工作。