Reproduced in the original text: Introduction to Netty and detailed explanation of NIO_ dzyls' notes - CSDN blog
catalogue
Basic description of I/O model
Basic description of I/O model
There are three network programming models I/O modes supported by Java:
Analysis of applicable scenarios of BIO, NIO and AIO
Relationship between NIO's three core components
Buffer class and its subclasses
Example 1 - local file write data:
Example 2 - local file read data:
Precautions and details about Buffer and Channel
Introduction to Netty
netty concept
- netty is an open source Java framework, which is now an independent project on github
- netty is an asynchronous, event driven network application framework. It is used to quickly develop high-performance and reliable network IO programs, which is to optimize and rewrite the native Java io
- netty is mainly aimed at high concurrency applications for Clients under TCP protocol, or applications with continuous transmission of a large amount of data in Peer-to-Peer scenario
- netty is essentially a NIO framework, which is applicable to a variety of application scenarios related to server communication
- To sum up, under the TCP/IP protocol, it is covered with Java Native IO and wrapped with NIO network development. netty is based on these
Noun concept
- Event driven: netty will generate corresponding event driven according to the behavior of the client. The simple understanding is to do some operations and call some methods. When the client sends a request, the server may call methods for corresponding processing;
- Asynchrony: asynchrony does not need to wait until the response comes back. The browser can do other things. Asynchrony in b/s architecture is mainly realized through ajax. The data after the response is processed through the callback function. That's the understanding. There is no need to wait for you to respond to the data. The browser can directly do other operations. After your data response comes back, it will be directly processed by the callback function
- Synchronization: in b/s architecture, the request is sent out, and the following work can be done only after the response comes back
netty application scenario
- Distributed system remote service invocation in distributed nodes is used by RPC framework as a high-performance communication framework and as a basic communication component
- The typical Alibaba distributed framework uses the Dubbo protocol. By default, netty is used as the basic communication component to realize the communication between nodes
- Game industry, high performance communication components
Basic description of I/O model
Basic description of I/O model
What kind of channel is used to send and receive data, which largely determines the performance of program communication
There are three network programming models I/O modes supported by Java:
BIO,NIO,AIO
JavaBIO(Java Native IO)
Synchronous blocking (traditional blocking type). Blocking here means that if no data is read, it will block the function or method. The server implementation mode is one connection and one thread, that is, when the client has a connection request, the server needs to start a thread for processing. If the connection does not do anything, it will cause unnecessary thread overhead, Java BIO is the traditional java io programming, and its related classes and interfaces are in java io
Model diagram
Disadvantages: if there are multiple clients, it means that there are many threads on the server. Threads have overhead and switching between threads. After the link is established, if there is no communication, the server should also maintain this thread.
Java NIO
Synchronous non blocking. The server implementation mode is that one thread processes multiple requests (connections), or multiple clients, that is, the connection requests sent by the clients will be registered on the multiplexer (simply understood as the selector), and the multiplexer will process the I/O requests when it polls the connection.
Simple schematic diagram
Features:
Where there is an I/O request, the client will be connected, which reflects the event driven;
A single thread handles multiple client and server read and write operations, reflecting multiplexing.
Question:
There must be an upper limit on how many connections a thread can maintain. Generally, multiple threads maintain multiple connections. The specific implementation is as follows:
Java AIO(NIO.2)
Asynchronous and non blocking. AIO introduces the concept of asynchronous channel and adopts the Proactor mode, which simplifies the programming and starts the thread only after an effective request. Its feature is that the operating system notifies the server program to start the thread for processing after it is completed. It is generally used in applications with a large number of connections and a long time. (a brief introduction is not the key point. Remember that netty5. X is based on its implementation, but the effect is not very good, so netty5. X is invalidated. Because of the Linux system itself)
Analysis of applicable scenarios of BIO, NIO and AIO
BIO mode is applicable to the architecture with small and fixed number of connections. This mode has high requirements for server resources, and concurrency is limited to applications. Jdk1 4 the only choice before, but the program is simple and easy to understand.
NIO mode is applicable to the architecture with a large number of connections and relatively short connections (light operation), such as chat server, bullet screen system, inter server communication, etc. the programming is complex, jdk1 4 start support.
AIO mode is applicable to the architecture with a large number of connections and long connections (re operation), such as album server. It fully calls the OS to participate in concurrent operation. The programming is complex, and JDK7 starts to support it.
I/O introduction
Working mechanism of Java BIO
Simple process
- Start a ServerSocket on the server side
- The client starts the Socket to communicate with the server. By default, the server needs to establish a thread to communicate with each request (client)
- After the client sends a request, it first asks the server whether there is a thread response. If not, it will wait or be rejected
- If there is a thread response, the client thread will wait for the end of the request before continuing to execute. It is a synchronous blocking programming mode
Application case
Example description (mainly verifying that a thread is started when there is a connection):
- Write a server side using BIO model, listen to port 6666, and start a thread to communicate with it when there is a client connection;
- It is required to improve the thread pool mechanism, which can connect multiple clients;
- The server can receive the data sent by the client;
code
public class BIOServer { public static void main(String[] args) throws IOException { //Thread pool mechanism //thinking //1. Create a thread pool //2. If there is a client connection, create a thread to communicate with it (write a separate method) //Create thread pool ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //Create a ServerSocket and allocate a port, which is understood here as the server ServerSocket serverSocket = new ServerSocket(6666); System.out.println("The server started"); //This cycle is to connect if there is a client to connect while (true){ //Listen and wait for the client connection to be defined as final, so that it cannot be changed //socket is used to communicate with clients //accept will block if you can't connect to the client final Socket socket = serverSocket.accept(); System.out.println("Connect to a client"); //Create a thread to communicate with newCachedThreadPool.execute(new Runnable() { public void run() { //Can communicate with clients handler(socket); } }); } } //Write a handler method to communicate with the client //Get the socket to communicate public static void handler(Socket socket){ //receive data byte[] bytes = new byte[1024]; //The input stream is obtained through socket, and the data in the pipeline can be read through the input stream try { InputStream inputStream = socket.getInputStream(); //Cycle to read the data sent by the client, which is to cycle to read the data while (true){ //When the data is read into the array, the returned value is the amount of data read. How much data has been read int read = inputStream.read(bytes); //No data is read here. It should be blocked if (read != -1){ //That means there's still data to read //Output the data sent by the client, because the data is in the array and then converted into a string String str = new String(bytes, 0, read); System.out.println(str); }else{ break; } } } catch (IOException e) { e.printStackTrace(); }finally { System.out.println("close client Connection of"); try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
Result display
A thread corresponds to a client
summary
-
Each request needs to create an independent thread to read, process and write data with the corresponding client
-
When the number of concurrency is large, a large number of threads need to be created to process the connection, which takes up a large amount of system resources
-
After the connection is established, if the current thread has no data to read temporarily, the thread will block the read operation, resulting in a waste of thread resources. If there is no data to read, the server can do other things
Introduction to Java NIO
-
The full name of Java NIO is java non blocking IO, which refers to the new API provided by JDK Since 4, Java has provided a series of improved input / output new features, which are collectively referred to as NiO (NEW IO), which is synchronous and non blocking
-
NIO related classes are placed in Java NIO package and sub package, and the original Java Many classes in the IO package are rewritten
-
NIO has three core parts: channel, buffer and selector
-
NIO is buffer oriented or block oriented programming. The data is read into a buffer for later processing and can be moved back and forth in the buffer when necessary, which increases the flexibility in the processing process. It can provide a non blocking high scalability network and realize the non blocking mechanism through the buffer
-
The non blocking mode of Java NIO enables a thread to send requests or read data from a channel, but it can only get the currently available data. If there is no data available, it will not get anything, rather than keep the thread blocked. Therefore, until the data becomes readable, the thread can continue to do other things. The same is true for non blocking write. A thread requests to write some data to a channel, but does not need to wait for it to write completely. This thread can do other things at the same time. That is, which channel has events that the selector is concerned about, that is, which channel to deal with (read, write, connect);
-
Popular understanding: NIO can handle multiple operations with one thread. Assuming 10000 requests come, according to the actual situation, 50 or 100 threads can be allocated for processing. Unlike the previous blocking IO, 10000 must be allocated
-
HTTP2.0 uses multiplexing technology to process multiple requests simultaneously in the same connection, and the number of concurrent requests is higher than that of http1 1. It is several orders of magnitude larger;
Simple schematic diagram
Simple example
public class basicBuffer { public static void main(String[] args) { //Give an example to illustrate the use of Buffer //Create a Buffer with a size of 5, which can store 5 int s IntBuffer intBuffer = IntBuffer.allocate(5); //Store data in buffer for (int i = 0; i < intBuffer.capacity(); i++) { intBuffer.put(i*2); } //How to read data from buffer //Convert the buffer and switch between reading and writing, that is, just write it in, and now you want to read it, and convert it intBuffer.flip(); //Determine whether there is any left in it while (intBuffer.hasRemaining()){ //Every time you get, the index moves back once System.out.println(intBuffer.get()); } } }
Comparison of NIO and BIO
- BIO processes data in stream mode, while NIO processes data in block mode. The efficiency of block I/O is much higher than that of stream I/O
- BIO is blocking and NIO is non blocking
- BIO operates based on byte stream and character stream, while NIO operates based on channel and buffer. Data is always read from the channel to the buffer or written from the buffer to the channel;
The selector is used to listen to the events of multiple channels (such as connection request, data arrival, etc.), so a single thread can listen to multiple client channels
Relationship between NIO's three core components
Sketch Map
Description of the relationship among Selector, Channel and Buffer:
- Each Channel corresponds to a Buffer
- A Selector will correspond to a thread
- One thread will correspond to multiple channels (connections)
- The figure above shows that three channels are registered with the Selector
- Which Channel the program switches to is determined by events. Event is a very important concept
- The Selector will switch on each channel according to different events
- Buffer is a memory block, and there is an array at the bottom
- Data is read and written through the Buffer, which is essentially different from BIO. BIO is either an input stream or an output stream, which cannot be bidirectional, but NIO's Buffer can be read or written, which needs to be switched by flip method
- The Channel is bidirectional and can return to the underlying operating system. For example, the Channel of the underlying operating system of Linux is bidirectional
Buffer buffer
Basic introduction
Buffer: a buffer is essentially a memory block that can read and write data. It can be understood as a container object (including an array). This object provides a set of methods to use the memory block more easily. The buffer object has built-in mechanisms to track and record the state changes of the buffer. Channel provides a channel for reading data from files and networks, but the data read or written must pass through the buffer, as shown in the figure
Friendly note: there are some internal mechanisms in buffer, which can track and record the state changes of buffer. Channel is the channel. It can be imagined that the binary stream transmitted by the network is people. Channel is a way to reach the destination, such as waterway, air flight and road (in the network, it is equivalent to protocols such as WS protocol and http protocol). This buffer is vehicles, ships, aircraft and cars.
public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer> { // These fields are declared here rather than in Heap-X-Buffer in order to // reduce the number of virtual method invocations needed to access these // values, which is especially costly when coding small buffers. // final int[] hb; // Non-null only for heap buffers final int offset; boolean isReadOnly;
Buffer part of the source code, you can see that the bottom layer of IntBuffer contains an array, which also accesses data through the int array
Buffer class and its subclasses
1) Buffer is used to buffer read-write data. Common buffers include, except for Boolean type, other types:
Store various types of data in the buffer
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
2) The Buffer class defines four attributes that all buffers have to provide information about the data elements they contain:
public abstract class Buffer { /** * The characteristics of Spliterators that traverse and split elements * maintained in Buffers. */ static final int SPLITERATOR_CHARACTERISTICS = Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; // Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
attribute | describe |
Capacity | The capacity, that is, the maximum amount of data that can be accommodated, is set when the buffer is created and cannot be changed |
Limit | Indicates the current end point of the buffer. Read and write operations cannot be performed on the position where the buffer exceeds the limit, and the limit can be modified |
Position | Each time the index is read or written, the value of the element will be prepared for the next reading or writing |
Mark | sign |
- capacity(): equivalent to the initial capacity given by the array, which is always the same
- position(): it is equivalent to a pointer. It moves continuously. The initial value is 0
- limit(): the limit position of the pointer movement, that is, the final position of the written data.
- The value of - mark: can be read from the specified position by adding a mark of - 1 to the specified position
Sample code debug
- When creating a buffer
- When reading data
When you enter it to the end, you will find that the position and limit values are the same and cannot exceed the limit value
- flip method
public final Buffer flip() { limit = position;//Set where you read, and set limit to this value position = 0;//position is set to 0 mark = -1; return this; }
When reading data, position also continues to move one by one
3) Buffer class related methods
ByteBuffer
It can be seen from the above that for the basic data types in Java (except boolean), there is a Buffer type corresponding to it. The most commonly used is naturally the ByteBuffer class (binary data). The main methods of this class are as follows:
Channel
Basic introduction
1) NIO channels are similar to streams, but some differences are as follows:
- Channels can read and write at the same time, while streams can only read or write
- The channel can read and write data asynchronously
- The channel can read data from the buffer or write data to the buffer
2) The stream in BIO is unidirectional. For example, the FileInputStream object can only read data, while the channel in NIO is bidirectional and can be read or written
3) Channel is a public interface in NIO China. Channel extensions are closed {}
4) Common Channel classes include FileChannel, datagram Channel, ServerSocketChannel and SocketChannel
5) FileChannel is used for file data reading and writing, datagram channel is used for UDP data reading and writing, and ServerSocketChannel and socketChannel are used for TCP data reading and writing
Common channels
- FileChannel: dedicated to reading files
- Datagram channel: dedicated to UDP
- SocketChannel: the client corresponding to network transmission, which is used for TCP transmission
- ServerSocketChannel: the server corresponding to network transmission, which is used for TCP transmission
FIleChannel class
FileChannel is mainly used to perform I/O operations on local files. Common methods include
- public int read(ByteBuffer dst), which reads data from the channel and puts it into the buffer
- public int write(ByteBuffer src) writes the buffer data to the channel
- public long transferFrom(ReadableByteChannel src,long position,long count) copies data from the target channel to the current channel
- public long transferTo(long position,long count,WritableByteChannel target) copies data from the current channel to the target channel
Example 1 - local file write data:
1) Use the ByteBuffer and filechannel learned above to write "hello, Silicon Valley" to file01 Txt
2) Create if file does not exist
Code display
public class NIOFileChannel01 { public static void main(String[] args) throws Exception{ String str = "hello Shang Silicon Valley"; //Create an output stream, which will be wrapped in the channel FileOutputStream fileOutputStream = new FileOutputStream("D:\\temp\\file01.txt"); //By obtaining the corresponding file FileChannel, the real type is FileChannelImpl FileChannel fileChannel = fileOutputStream.getChannel(); //Create a buffer ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //Put str into buffer byteBuffer.put(str.getBytes()); //Flip the byteBuffer because it was written before. position refers to the limit. Now you need to flip it to read byteBuffer.flip(); //Write byteBuffer data to fileChannel fileChannel.write(byteBuffer); fileOutputStream.close(); } }
debug
When the put method is run, there are 6 letters + 3 Chinese characters and 9 bytes, a total of 15 bytes
After flipping, position becomes 0 and limit becomes 15
fileChannelImpl contained in fileOutPutStream
Realization process
Example 2 - local file read data:
1) Use the ByteBuffer and filechannel learned above to set file01 Txt is read into the program and displayed on the console screen
2) Assume that the file already exists
Code display
public class NIOFileChannel02 { public static void main(String[] args) throws Exception{ //Create input stream for file File file = new File("D:\\temp\\file01.txt"); FileInputStream fileInputStream = new FileInputStream(file); //Get the corresponding FileChannel through fileInputStream. The actual type is FileChannelImpl FileChannel fileChannel = fileInputStream.getChannel(); //Create buffer ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); //Read the data of the channel into the buffer fileChannel.read(byteBuffer); //Converts the bytes of the buffer to a string System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } }
Overall idea:
Example 3 - use a buffer to read and write files
The first two examples use two buffer s for reading and writing respectively
- Use filechannel and read and write methods to copy files
- Copy a text file 1 Txt, just put it under the project
Code demonstration
public class NIOFIleChannel03 { public static void main(String[] args) throws Exception{ File file = new File("D:\\temp\\file02.txt"); File file01 = new File("D:\\temp\\file03.txt"); FileInputStream fileInputStream = new FileInputStream(file); FileOutputStream fileOutputStream = new FileOutputStream(file01); FileChannel fileChannel01 = fileInputStream.getChannel(); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); fileChannel01.read(byteBuffer); byteBuffer.flip(); fileChannel02.write(byteBuffer); fileInputStream.close(); fileOutputStream.close(); } }
This does not take into account that if the data exceeds 1024, the excess part cannot be copied
In fact, I think it's OK here, because the length of the cache I allocate is the length of the file
Another code presentation
public class NIOFIleChannel03 { public static void main(String[] args) throws Exception{ File file = new File("D:\\temp\\file02.txt"); File file01 = new File("D:\\temp\\file03.txt"); FileInputStream fileInputStream = new FileInputStream(file); FileOutputStream fileOutputStream = new FileOutputStream(file01); FileChannel fileChannel01 = fileInputStream.getChannel(); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); while (true){ //Clearing the buffer is mainly to clear the position to 0, otherwise it will always be equal to the limit. If the two values are the same, the read value will be 0, resulting in the failure to end the subsequent cycle byteBuffer.clear(); // public final Buffer clear() { // position = 0; // limit = capacity; // mark = -1; // return this; // } int read = fileChannel01.read(byteBuffer); if (read == -1){ break; } byteBuffer.flip(); fileChannel02.write(byteBuffer); } fileInputStream.close(); fileOutputStream.close(); } }
Directly encapsulated file copying method
package com.atguigu.nio; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.channels.FileChannel; public class NIOFileChannel04 { public static void main(String[] args) throws Exception { //Create correlation flow FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg"); FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg"); //Get the filechannel corresponding to each stream FileChannel sourceCh = fileInputStream.getChannel(); FileChannel destCh = fileOutputStream.getChannel(); //Use transferForm to complete the copy destCh.transferFrom(sourceCh,0,sourceCh.size()); //Close relevant channels and streams sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); } }
Example 5
Friendly tip: transferTo is highly efficient. The bottom layer will be optimized by using the zero copy of the operating system. This method can only transfer 2g memory data at a time
package cn.itcast.nio.c3; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; public class TestFileChannelTransferTo { public static void main(String[] args) { try ( FileChannel from = new FileInputStream("data.txt").getChannel(); FileChannel to = new FileOutputStream("to.txt").getChannel(); ) { // High efficiency. The bottom layer will use the zero copy of the operating system for optimization. This method can only transfer 2g memory data at a time long size = from.size(); // The left variable represents how many bytes remain for (long left = size; left > 0; ) { System.out.println("position:" + (size - left) + " left:" + left); // The following methods can be passed multiple times, left -= from.transferTo((size - left), left, to); } } catch (IOException e) { e.printStackTrace(); } } }
Precautions and details about Buffer and Channel
- ByteBuffer supports typed put and get. For the data type put into the room, get should use the corresponding data type to get it, otherwise there may be BufferUnderflowException
- You can convert a normal Buffer into a read-only Buffer
- NIO also provides MappedByteBuffer, which allows files to be modified directly in memory (memory outside the heap). The operating system does not need to copy once, but NIO can complete how to synchronize files
- The read and write operations mentioned above are all completed through one Buffer. NIO also supports reading and write operations through multiple buffers (i.e. Buffer array), i.e. Scattering and Gathering
MappedByteBuffer
- Friendly tip: the code execution is completed, and the data viewed in idea has not changed. Go to the file management system of the computer and check it. The data in it is changed.
- NIO also provides MappedByteBuffer, which allows files to be modified directly in memory (memory outside the heap). NIO completes the synchronization of files without one copy. Note that here is the meaning of 5, and only 5 bytes can be modified at most
package com.atguigu.nio; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; /* explain 1. MappedByteBuffer The file can be modified directly in memory (off heap memory), and the operating system does not need to copy it once */ public class MappedByteBufferTest { public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw"); //Get the corresponding channel FileChannel channel = randomAccessFile.getChannel(); /** * Parameter 1: filechannel MapMode. READ_ Read / write mode used by write * Parameter 2:0: the starting position can be modified directly * Parameter 3: 5: is the size mapped to memory (not the index position), i.e. 1 How many bytes of TXT are mapped to memory * The range that can be directly modified is 0-5. Note that 5 is not included * Actual type DirectByteBuffer */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 15); //Change the first position to capital H, and so on mappedByteBuffer.put(0, (byte) 'H'); mappedByteBuffer.put(3, (byte) '9'); mappedByteBuffer.put(14, (byte) 'Y');//IndexOutOfBoundsException randomAccessFile.close(); System.out.println("Modified successfully~~"); } }
Scattering and Gathering
NIO also supports reading and writing through multiple buffers (i.e. Buffer arrays), mainly because the internal parameters of CHANLE's read and write methods can make the Buffer array object
- Scattering: when writing data to buffer, buffer array can be used to write [scattered] successively
- Gathering: when reading data from the buffer, you can use the buffer array to read data in sequence
package com.atguigu.nio; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; /** * Scattering: When writing data to buffer, buffer array can be used to write [scattered] successively * Gathering: When reading data from the buffer, you can use the buffer array to read data in sequence */ public class ScatteringAndGatheringTest { public static void main(String[] args) throws Exception { //Using ServerSocketChannel and SocketChannel network ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); //Bind the port to the socket and start serverSocketChannel.socket().bind(inetSocketAddress); //Create buffer array ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(3); //Wait for client connection (telnet) SocketChannel socketChannel = serverSocketChannel.accept(); int messageLength = 8; //Assume 8 bytes are received from the client //Cyclic reading while (true) { int byteRead = 0; while (byteRead < messageLength ) { // Read data from client long l = socketChannel.read(byteBuffers); byteRead += l; //Cumulative bytes read System.out.println("byteRead=" + byteRead); //Use stream printing to see the position and limit of the current buffer Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println); } //flip all buffer s Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //Read and display the data to the client long byteWirte = 0; while (byteWirte < messageLength) { long l = socketChannel.write(byteBuffers); // byteWirte += l; } //clear all buffer s Arrays.asList(byteBuffers).forEach(buffer-> { buffer.clear(); }); System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messagelength" + messageLength); } } }
The results show that