In depth analysis of the working mechanism of Java I / O — reprint

< H2 id = "1. Basic architecture of Java I / O class library | outline" > basic architecture of Java I / O class library

I / O problem is an unavoidable problem in any programming language. It can be said that I / O problem is the core problem of the whole human-computer interaction, because I / O is the main channel for machines to obtain and exchange information. In today's era of data explosion, I / O problems are particularly prominent and can easily become a performance bottleneck. Because of this, Java has been making continuous optimization on I / O. for example, NiO has been introduced since 1.4 to improve the performance of I / O. NiO will be described in detail later.

Java's I / O operation classes are in the package Java Under IO, there are about 80 classes, but these classes can be divided into four groups:

The first two groups are mainly based on the data format of the transmitted data, and the last two groups are mainly based on the method of transmitting data, although the socket class is not in Java IO package, but I still divide them together, because I personally think the core problem of I / O is that either the data format affects the I / O operation, or the transmission mode affects the I / O operation, that is, what kind of data is written to where. I / O is only a means of interaction between human and machine or between machine and machine, except that they can complete this interaction function, We focus on how to improve its operation efficiency, and data format and transmission mode are the most key factors affecting efficiency. Our later analysis is also based on these two factors.

The input and output of byte based I / O operation interface are InputStream and OutputStream respectively. The class inheritance hierarchy of InputStream input stream is shown in the following figure:

The input stream is divided into several subclasses according to the data type and operation mode. Each subclass handles different operation types respectively. The class hierarchy of the output stream is also similar, as shown in the following figure:
We won't explain how to use each subclass in detail here. If you don't know, you can refer to the API documentation of JDK. Here we just want to explain two points. One is that the data operation method can be used in combination. For example, OutputStream out = new bufferedoutputstream (New objectoutputstream (New fileoutputstream ("filename")); Another point is that where the stream will eventually be written must be specified, either to the disk or to the network. In fact, from the above class diagram, we find that writing to the network is actually writing files, but the next step to write to the network is that the underlying operating system will transfer the data to other places instead of the local disk. Network I / O and disk I / O will be described in detail later.

Whether it is disk or network transmission, the smallest storage unit is bytes, not characters, so I / O operations are bytes, not characters, but why is there an I / O interface to operate characters? This is because the data usually operated in our program is in the form of characters. For the convenience of operation, of course, an I / O interface for writing characters directly should be provided, that's all. We know that characters to bytes must be encoded and converted, and this encoding is very time-consuming, and there will often be garbled problems, so I / O encoding is often a headache. Refer to another article on I / O coding. The following figure shows the classes involved in the I / O operation interface for writing characters. The writer class provides an abstract method. Write (char cbuf [], int off, int len) is implemented by subclasses.

The operation interface for reading characters also has a similar class structure, as shown in the following figure:
The character reading operation interface is also int read (char cbuf [], int len), which returns the number of N bytes read. Whether it is writer or reader, they only define the way to read or write data characters, that is, how to write or read, but they do not specify where to write data. Where to write data is the working mechanism based on disk and network to be discussed later.

In addition, data persistence or network transmission are carried out in bytes, so there must be character to byte or byte to character conversion. Character to byte conversion is required, and the conversion process of read is shown in the following figure:

Inputstreamreader class is a byte to character conversion bridge. The process of InputStream to reader should specify the coded character set, otherwise the default character set of the operating system will be adopted, and there may be garbled code. Streamdecoder is the implementation class for byte to character decoding. That is, when you read a file in the following way:
0){ str.append(buf); } str.toString(); } Catch (IOException E) {} FileReader class reads files according to the above working method. FileReader inherits inputstreamreader class. In fact, it reads the file stream and decodes it into char through streamdecoder, but the decoded character set here is the default character set. Writing is a similar process, as shown in the following figure:
The encoding process from character to byte is completed through the outputstreamwriter class, and the encoding process is completed by streamecoder. Earlier, we introduced the basic Java I / O operation interfaces. These interfaces mainly define how to operate data and introduce the way to operate two data structures: bytes and characters. Another key problem is where the data is written. One of the main ways is to persist the data to the physical disk. The following describes how to persist the data to the physical disk. We know that the only smallest description of data on the disk is the file, that is, the upper application can only operate the data on the disk through the file. The file is also the smallest unit for the interaction between the operating system and the disk drive. It is worth noting that the usual file in Java does not represent a real file object. When you specify a path descriptor, it will return a virtual object associated with the path, which may be a real file or a directory containing multiple files. Why is it designed like this? Because in most cases, we don't care whether the file really exists, but how the file operates. For example, we usually store the phone numbers of hundreds of friends in our mobile phones, but we usually care about whether I have the phone number of this friend or what the phone number is, but whether the phone number can get through or not, we don't check it all the time, but only when we really want to call him. That is, the number of times you use this phone record is much more than the number of times you make this phone call. When is it really necessary to check whether a file is saved or not? When you really want to read this file, for example, FileInputStream class is an interface for operating a file. Note that when you create a FileInputStream object, a filedescriptor object will be created. In fact, this object is the description of an existing file object, When we operate a file object, we can get the file description associated with the underlying operating system through the getfd () method. For example, you can call filedescriptor The sync () method forcibly flushes the data in the operating system cache to the physical disk. Next, take the program in Listing 1 as an example to introduce how to read a text character from the disk. As shown in the following figure: when a file path is passed in, a file object will be created according to the path to identify the file, and then an operation object to read the file will be created according to the file object. At this time, a file descriptor associated with the real disk file will be created, Through this object, you can directly control the disk file. Because we need to read the character format, we need the streamdecoder class to decode byte into char format. As for how to read a piece of data from the disk drive, the operating system will help us. As for how the operating system persists data to disk and how to establish data structure, you need to answer back and forth according to the file system used by the current operating system. For details about the file system, please refer to another article. The concept of socket does not correspond to a specific entity. It is an abstract function to describe the mutual communication between computers. For example, you can compare socket as a means of transportation between two cities. With it, you can shuttle back and forth between cities. There are many kinds of vehicles, and each vehicle also has corresponding traffic rules. Socket is the same. There are many kinds. In most cases, we use stream socket based on TCP / IP, which is a stable communication protocol. The following figure is a typical socket based communication scenario: in order for the application of host a to communicate with the application of host B, a connection must be established through the socket, and the establishment of a socket connection must require the underlying TCP / IP protocol to establish a TCP connection. Establishing a TCP connection requires the underlying IP protocol to address the hosts in the network. We know that the IP protocol used in the network layer can help us find the target host according to the IP address, but multiple applications may be running on a host. How to communicate with the specified application needs to be specified through the TCP or UPD address, that is, the port number. In this way, a socket instance can uniquely represent the communication link of an application on a host. When the client wants to communicate with the server, the client must first create a socket instance. The operating system will assign an unused local port number to the socket instance, and create a socket data structure containing local and remote addresses and port numbers. This data structure will be saved in the system until the connection is closed. Before the constructor that creates the socket instance returns correctly, the TCP three-time handshake protocol will be performed. After the TCP handshake protocol is completed, the socket instance object will be created, otherwise an IOException error will be thrown. The corresponding server will create a ServerSocket instance. ServerSocket creation is relatively simple. As long as the specified port number is not occupied, the general instance creation will succeed. At the same time, the operating system will also create an underlying data structure for the ServerSocket instance, which contains the specified listening port number and wildcards containing listening addresses, Usually it is "*", that is, listen to all addresses. Then, when the accept () method is called, it will enter the blocking state and wait for the client's request. When a new request arrives, a new socket data structure will be created for the connection. The address and port information contained in the socket data is the request source address and port. The newly created data structure will be associated with an incomplete connection data structure list of the ServerSocket instance. Note that at this time, the socket instance corresponding to the server has not been created, and the socket instance of the server will not return until the three handshakes with the client are completed, And move the data structure corresponding to this socket instance from the unfinished list to the completed list. Therefore, each data structure in the list associated with ServerSocket represents the established TCP connection with a client. Data transmission is the main purpose of establishing a connection. How to transmit data through socket will be described in detail below. When the connection has been established successfully, both the server and the client will have a socket instance. Each socket instance has an InputStream and OutputStream, which are used to exchange data. At the same time, we also know that network I / O is transmitted in byte stream. When the socket object is created, the operating system will allocate a buffer of a certain size for InputStream and OutputStream respectively. Data writing and reading are completed through this buffer. The writer writes the data to the sendq queue corresponding to the OutputStream. When the queue is full, the data will be sent to the recvq queue of the InputStream at the other end. If the recvq is full at this time, the write method of the OutputStream will be blocked until the recvq queue has enough space to accommodate the data sent by sendq. It is worth noting that the size of the buffer, the speed of the write end and the speed of the read end greatly affect the data transmission efficiency of the connection. Due to possible blocking, there is also a coordinated process between network I / O and disk I / O in data writing and reading. If data is transmitted on both sides at the same time, deadlock may occur, Avoiding this situation will be described in the NiO section later. Bio is blocking I / O. whether it is disk I / O or network I / O, data may be blocked when written to or read from InputStream. Once there is thread blocking, the right to use CPU will be lost, which is unacceptable under the current large-scale access and performance requirements. Although there are some solutions for current network I / O, such as one processing thread for one client, when blocking occurs, only one thread is blocked without affecting the work of other threads, and in order to reduce the overhead of system threads, thread pool is used to reduce the cost of thread creation and collection, there are still some usage scenarios that cannot be solved. For example, in some current situations that require a large number of HTTP long connections, such as the web Wangwang project currently used by Taobao, the server needs to maintain millions of HTTP connections at the same time, but these connections are not transmitting data all the time. In this case, it is impossible to create so many lines to maintain the connection at the same time. Even if the number of threads is not a problem, there are still some problems that cannot be avoided. In this case, we want to give higher service priority to some clients, which is difficult to complete by designing the priority of threads. In another case, we need to make the requests of each client access some competing resources on the server. Because these clients are in different threads, they need to be synchronized, It is often much more complex to implement these synchronous operations than to use a single thread. All of the above shows that we need another new I / O operation mode. Let's take a look at the Association class diagram involved in NiO, as follows: in the figure above, there are two key classes: channel and selector, which are two core concepts in NiO. We also use the urban transport in front to continue to compare NiO's working mode. The channel here is more specific than socket. It can be used as a specific transport, such as car or high-speed rail, and the selector can be used as a vehicle operation scheduling system at a station, It will be responsible for monitoring the current operation status of each vehicle: whether it has been in battle or on the road, etc., that is, it can poll the status of each channel. There is also a buffer class, which is more specific than stream. We can compare it as a seat on the car. If channel is a car, it is a seat on the car, and high-speed rail is a seat on the high-speed rail. It is always a specific concept, which is different from stream. Stream can only represent one seat. You can imagine what seat it is, that is, you don't know whether there are any seats on the car or what car you're on before you get on the bus, because you can't choose, These information have been encapsulated in the socket and are transparent to you. NiO introduces channel, buffer and selector to materialize these information and give programmers the opportunity to control them. For example, when we call write() to write data to sendq, when the data written at one time exceeds the length of sendq, it needs to be divided according to the length of sendq, In this process, you need to switch between user space data and kernel address space, which is not under your control. In the buffer, we can control the capacity of the buffer, and whether and how to expand the capacity can be controlled. After understanding these concepts, let's take a look at how they actually work. Here is a typical NiO Code: open(); ServerSocketChannel ssc = ServerSocketChannel. open(); ssc. configureBlocking(false);//
The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>