Today, when the Internet is quite popular, chatting on the Internet is already common for many "Online worms. Chat Room programs are the simplest multi-point communication program on the Internet. There are many implementation methods for chat rooms, but they all use the so-called "multi-user space" to exchange information, with a typical multi-channel I/O architecture. From the programmer's point of view, a simple chat room is to implement multi-to-many communication between multiple I/O endpoints. Its architecture 1 is shown in. In users' eyes, this is a sentence that can be obtained by other users after a character is entered by anyone in the chat room. This "multi-user space" architecture is widely used in other multi-point communication programs. Its core is multi-channel I/O communication. Multi-channel I/O communication, also known as I/O multiplexing (I/O multiplexing), is generally used in the following scenarios:
The customer program needs to handle the I/O multiplexing problem when interactive input and network connection between the same server;
The client must respond to multiple network connections at the same time (this is rare );
The TCP server needs to process the socket in the listening status and multiple connection statuses at the same time;
The server needs to process sockets with multiple network protocols;
The server must process different network services and protocols at the same time.
The chat room is faced with the first and third situations. We will establish a simple chat room on top of the TCP/IP protocol to give you a better understanding of multi-channel I/O and its implementation methods.
The chat room features we want to discuss are very simple. Interested friends can extend their functions into a chat room with complete functions, such as adding user authentication, user nickname, and secret information, semote and other functions.
First, it is a Client/Server structure program. First, start the server, and then the user uses the client to connect. the advantage of the Client/Server structure is that it is fast, but the disadvantage is that when the server is updated, the client must also be updated.
Network initialization
First, initialize the server so that the server enters the listening State: (for the sake of conciseness, the referenced program is slightly different from the actual program, the same below)
Sockfd = socket (af_inet, sock_stream, 0 );
// First create a socket. The family is af_inet and the type is sock_stream.
// Af_inet = ARPA Internet protocols uses the TCP/IP protocol family
// The sock_stream type provides sequential, reliable, and full-duplex connection based on byte streams.
// Because there is only one protocol in this protocol family, the third parameter is 0
BIND (sockfd, (struct sockaddr *) & serv_addr, sizeof (serv_addr ));
// Bind the socket with an address.
// Serv_addr includes sin_family = af_inet protocol family and Socket
// Sin_addr.s_addr = all others accepted by the htonl (inaddr_any) Server
// The Connection established by the address request.
// Sin_port = port listened by the htons (serv_tcp_port) Server
// In this program, the server IP address and the listening port are stored in the config file.
Listen (sockfd, max_client );
// After the address is bound, the server enters the listening status.
// Max_client is the total number of clients that can establish connections at the same time.
After the server enters the listen status, wait for the client to establish a connection.
Before establishing a connection, the client also needs to initialize the connection:
Sockfd = socket (af_inet, sock_stream, 0 ));
// Similarly, the client also sets up a socket with the same parameters as the server.
Connect (sockfd, (struct sockaddr *) & serv_addr, sizeof (serv_addr ));
// The client uses connect to establish a connection.
// Set the variables in serv_addr:
// Sin_family = af_inet protocol family is the same as socket
// Sin_addr.s_addr = inet_addr (serv_host_addr) the address is Server
// The address of the computer to which it belongs.
// Sin_port = htons (serv_tcp_port) port is the port listened by the server.
When the client initiates a new connection request to the server, the server uses accept to accept the connection:
Accept (sockfd, (struct sockaddr *) & cli_addr, & cli_len );
// When the function returns, the information of the connected peer is retained in cli_addr.
// Includes the IP address of the other party and the port used by the other party.
// Accept returns a new file descriptor.
After the server enters the listen status, because multiple users are online, the program needs to perform operations on these users at the same time and exchange information between them. This is called I/O multiplexing. Multiplexing generally involves the following methods:
Non-blocking communication method: sets the file pipeline to a non-blocking communication method through fcntl (), and polls them at every end to determine whether read/write operations can be performed. The disadvantage of this method is that the cost is too high, and most resources are wasted on polling.
Sub-process method: multiple sub-processes are applied, and each sub-process is blocked by a single ticket. All child processes communicate with the parent process through IPC. The parent process is in charge of all information. The disadvantage of this method is that the implementation is complicated, and the portability is reduced because the IPC is not completely consistent on different operating systems.
Asynchronous I/O Method for signal-driven (sigio): first, asynchronous I/O is based on the signal mechanism and is not reliable. Second, a single signal is insufficient to provide more information sources. It is difficult to implement it by other means.
Select () method: in BSD, select () is a method that can block Multiple I/O queries (). It provides a method for blocking queries on multiple I/O descriptors at the same time. With this method, we can conveniently implement multiplexing. POSIX also adopts this method according to the unified UNIX protocol, so we can use the select method in most operating systems.
Use dedicated I/O multiplexing: in "UNIX? The System V programmer's Guide: streams describes in detail how to construct and use multiplead. I will not go into detail here.
We will discuss the two implementation methods of multi-channel I/O:
1. Non-blocking communication method
A file or device specified by a file descriptor can work in two ways: blocking and non-blocking. Blocking means that when you try to read and write the file descriptor, the program enters the waiting state if there is nothing to read at the time or it cannot be written at the moment, until something is readable or writable. For non-blocking states, if nothing is readable or not writable, the read/write function returns immediately without waiting. By default, the file descriptor is blocked. When implementing the chat room, the server needs to query the socket established with each client in turn. Once readable, the server reads the characters in the socket and sends them to all other clients. In addition, the server also needs to check whether a new client tries to establish a connection at any time, so that if the server is blocked in any place, the content sent by other clients will be affected, server response is not received. The attempt by the new client to establish a connection is also affected. Therefore, we cannot use the default blocking method for file operations here. Instead, we need to change the method of file operations to non-blocking. In UNIX, The fcntl () function can be used to change the operation mode of file I/O operations. The function description is as follows:
Fcntl (sockfd, f_setfl, o_nonblock );
// Sockfd is the file descriptor to change the state.
// F_setfl indicates the state of the file descriptor to be changed
// O_nonblock indicates that the file descriptor is not blocked.
To save space, we use natural language to describe the chat room server:
While (1 ){
If a new connection then is established and the new connection is recorded;
For (all valid connections)
Begin
If the connection contains characters that can be read by then
Begin
Read the string;
For (all other valid connections)
Begin
Send the string to the connection;
End;
End;
End;
End.
Since it determines whether a new connection exists and whether the connection is readable is non-blocking, every time it is determined, no matter whether there is a new connection or not, it will return immediately. in this way, any client that sends characters to the server or tries to establish a new connection will not affect the activity of other clients.
For the client, after establishing a connection, you only need to process two file descriptors, one is the socket descriptor that establishes the connection, and the other is the standard input. like server, if blocking is used, it is easy to affect the reading of another one because one of them has no input .. therefore, convert them into non-blocking ones, and then the client performs the following actions:
While (do not want to exit)
Begin
If (the connection to the server is readable)
Begin
Read from the connection and output it to the standard output.
End;
If (standard input readable)
Begin
Read from the standard input and output it to the connection with the server.
End;
End.
The preceding two functions are called:
Read (userfd [I], line, max_line );
// Userfd [I] refers to the file descriptor connected to the I client.
// Line refers to the location where the read characters are stored.
// Max_line is the maximum number of characters read at a time.
// The returned value is the number of characters actually read.
Write (userfd [J], line, strlen (line ));
// Userfd [J] is the file descriptor of the J client.
// Line is the string to be sent.
// Strlen (line) is the length of the string to be sent.
By analyzing the above programs, we can know that both the server and client continuously take turns to query each file descriptor, and read and process each file descriptor once it is readable. such programs are constantly being executed. As long as there are CPU resources, they will not be spared. Therefore, the consumption of system resources is very large. When the server or client is executed separately, about 98% of the CPU resources are occupied. System resources are greatly consumed.
Select Method
Therefore, although we do not want to block other users when a user does not respond, we should stop running the program without any response from the user, the system resource is blocked. Is there any such method? The select method is provided in the current UNIX system. The specific implementation method is as follows:
In the select method, all file descriptors are blocked. select is used to determine whether a set of file descriptors contains a readable (write). If not, it is blocked until one is awakened. let's first look at the implementation of a relatively simple client:
Because the client only needs to process two file descriptors, it is necessary to determine whether there are two file descriptors that can be read and written:
Fd_zero (sockset );
// Clear the sockset
Fd_set (sockfd, sockset );
// Add sockfd to the sockset
Fd_set (0, sockset );
// Add 0 (standard input) to the sockset
Then the client handles the following:
While (do not want to exit ){
Select (sockfd + 1, & sockset, null );
// At this time, the function will be blocked until there is a readable value in the standard input or sockfd.
// The first parameter is 0 and the maximum value in sockfd plus one
// The second parameter is the read set, that is, sockset.
// Third, the four parameters are the write set and the exception set, which are empty in this program.
// The Fifth parameter is the time-out period, that is, if the specified time is not readable, an error occurs.
// And return. When this parameter is null, the timeout value is set to an infinite length.
// When the SELECT statement is returned because it is readable, all that is included in the sockset statement is readable.
// File descriptors.
If (fd_isset (sockfd, & sockset )){
// Fd_isset the macro to determine whether sockfd is a readable file descriptor
Read from sockfd and output to standard output.
}
If (fd_isset (0, & sockset )){
// Fd_isset the macro to determine whether sockfd is a readable file descriptor
Read from standard input and output to sockfd.
}
Reset sockset. (To empty sockset and add sockfd and 0)
}
The following describes the server situation:
Set sockset as follows:
Fd_zero (sockset );
Fd_set (sockfd, sockset );
For (all valid connections)
Fd_set (userfd [I], sockset );
}
Maxfd = maximum file description symbol + 1;
Server:
While (1 ){
Select (maxfd, & sockset, null );
If (fd_isset (sockfd, & sockset )){
// There is a new connection
Create a new connection and add the connection descriptor to sockset.
}
For (all valid connections ){
If (fd_isset (userfd [I], & sockset )){
// The connection contains characters that are readable.
Read characters from the connection and send them to other valid connections.
}
}
Reset sockset;
}
Performance Comparison
Because the select mechanism is used, when no characters are readable, the program is blocked, and the CPU resources are used to the minimum extent. When one server and several clients are executed on the same machine, the system load is only about 0.1, and the original non-blocking communication method is used to run only one server, and the system load can reach about 1.5. therefore, select is recommended.