16, About IO stream separation

Posted by wemustdesign on Sun, 26 Dec 2021 07:25:58 +0100

1. What is I/O stream separation?

Recall the program we wrote before. In the first program, as long as we obtain a socket and successfully connect to the server, we can realize the data exchange with the server, and this data exchange is to send and receive data from the perspective of the client; The process of sending and receiving data is called streaming. I/O stream separation refers to the separation between the stream receiving data and the stream sending data. Here, separation refers to the isolation of operations from each other. The closure of one stream does not affect the use of another stream. This is the real understanding of I/O flow separation. In fact, we have implemented I/O stream separation several times before. One is the chapter of implementing multithreaded server echo server with fork(), but the input stream and output stream are distributed in different processes; The other is in Chapter 15. By converting the socket FILE descriptor into two FILE structure pointers (one is responsible for writing and the other is responsible for reading), the diversion between read and write streams is realized. However, it is worth noting that there are some differences in the separation of the two streams, mainly due to the different purposes of stream separation:

The purpose of using multi process to realize flow separation is to simplify the code and reduce the difficulty by separating the input process and the output process; I/O shunting is realized by using FILE structure pointer, and the performance of buffer is improved by distinguishing I/O buffer.

2. Why is I/O shunting required

This section briefly describes the following in the above section, which mainly has the following advantages:

  • Separate the input process from the output process to reduce the difficulty of implementation.
  • Input independent output operations can increase speed.
  • The difficulty of implementation can be reduced by distinguishing between read and write modes.
  • Improve buffering performance by differentiating I/O buffers.

Of course, the problem caused by flow separation is not small, mainly how to realize semi closing? Previously, we used the shutdown function to realize semi shutdown, but this is a semi shutdown without using the standard I/O stream. How to realize semi shutdown in the case of Chapter 15 (i.e. converting the socket into a FILE structure pointer)? We can try to guess: semi shutdown? There is a fclose() function in the standard I/O function, This function can be called by the FILE structure pointer and semi closed?

This guess, let's go to experiment to see if it can realize semi closing as we think.

//sep_serv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
int main(int argc, char const *argv[])
{
    int serv_sock,clnt_sock;
    FILE* readfp;
    FILE* writefp;

    struct sockaddr_in serv_adr,clnt_adr;
    socklen_t clnt_adr_sz;
    char buf[BUF_SIZE]={0,};
    serv_sock=socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_adr,0,sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));

    listen(serv_sock,5);

    clnt_adr_sz=sizeof(clnt_adr);
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);

    readfp=fdopen(clnt_sock,"r");
    writefp=fdopen(clnt_sock,"w");
    fputs("FROM SERVER: Hi~ client? \n",writefp);
    fputs("I love all of the world \n",writefp);
    fputs("You are awesome! \n",writefp);
    fflush(writefp);
    //Use the fclose() function to close the flow and see if it can be closed.
    fclose(writefp);
    fgets(buf,sizeof(buf),readfp);
    fputs(buf,stdout);
    fclose(readfp);
    return 0;
}




//sep_client.c
 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024

int main(int argc, char const *argv[])
{
    int sock;
    struct sockaddr_in serv_adr;
    char buf[BUF_SIZE];
    FILE* readfp;
    FILE* writefp;

    sock=socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_adr,0,sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_adr.sin_port=htons(atoi(argv[2]));
    connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
    
    readfp=fdopen(sock,"r");
    writefp=fdopen(sock,"w");
    while(1)
    {
        if(fgets(buf,sizeof(buf),readfp)==NULL)break;
        fputs(buf,stdout);
        fflush(stdout);
    }
    fputs("FROM CLIENT: Thank you~\n",writefp);
    fflush(writefp);
    fclose(writefp);
    fclose(readfp);

    return 0;
}

Test results:

//Client output:
FROM SERVER: Hi~ client? 
I love all of the world 
You are awesome! 
//The server did not receive any content from the client

Result analysis:

It can be seen that the fclose() function cannot be semi closed. The reason is that when the server calls Fclose () to try to close the writefp, the whole set of connection has been terminated, so the message from the client cannot be received.

3. The reason why we can't realize semi closing with fclose()

Before that, we need to clarify a fact that is easy to ignore. By "creating a socket" we mean creating a structure inside the computer, This structure (data format) can realize the exchange of network data, similar to creating a file. There is only one socket, but there can be multiple socket file descriptors, just like a file can have multiple links (soft link) similar. Without a file descriptor, the socket cannot be used, even if it still exists in the computer. Well, understand the above paragraph, let's look at a set of pictures to explain why the previous guess is wrong.

The relationship between the read / write FILE structure pointer and the FILE descriptor set in the above program is as shown in the following figure.

When we use fclose() to turn off the write mode, we will also close the relevant file descriptor, as shown below.

The reason why even sockets are unregistered is that the system will automatically unregister sockets without FILE descriptors. Now you know why? The reason is that the pointers to the read-write FILE structure point to the same FILE descriptor. When we want to close one mode without affecting the use of the other mode, we can complete the purpose of semi closing. Therefore, to find out the reason, the solution is obvious, that is, copy one more FILE descriptor to point to the same socket, and different sockets point to their respective FILE descriptors.

When we turn off one of these modes, a file descriptor is deleted, but the socket will not disappear because other descriptors of the socket are not completely deleted.

File descriptor copy function

#include<unistd.h>
int dup(int fildes);//Successfully returned the copied file descriptor (automatically allocated by the system)
int dup2(int fildes,int fildes2)
    //fildes: copy file description master
    //fildes2: specify the desired file descriptor explicitly (i.e. it is up to the developer to choose)

Maybe I don't understand. Let's see it through a small program

//dup.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    int cdf1,cfd2;
    char str1[]="Hi~ \n";
    char str2[]="It's nice day~ \n";

    cdf1=dup(1);
    cfd2=dup2(cdf1,8);
    printf("cfd1=%d   cfd2=%d  \n",cdf1,cfd2);
    write(cdf1,str1,sizeof(str1));
    write(cfd2,str2,sizeof(str2));

    close(cdf1);
    close(cfd2);
    write(1, str1,sizeof(str1));
    close(1);
    write(1,str2,sizeof(str2)); 
    return 0;
}

Operation results:

cfd1=3   cfd2=8  
Hi~ 
It's nice day~ 
Hi~ 

4. Achieve true semi closing

Now that we know the solution, let's look directly at the code.

//sep_serv2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

int main(int argc, char const *argv[])
{
    int serv_sock,clnt_sock;
    FILE* readfp,*writefp;
    struct sockaddr_in serv_adr,clnt_adr;
    
    socklen_t clnt_adr_sz;
    char buf[BUF_SIZE]={0,};

    serv_sock=socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_adr,0,sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
    
    listen(serv_sock,5);
    
    clnt_adr_sz=sizeof(clnt_adr);
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);

    readfp=fdopen(clnt_sock,"r");
    /**
     * @brief  Copying file descriptors with dup
     * @note   
     * @retval 
     */
    writefp=fdopen(dup(clnt_sock),"w");

    fputs("FROM SERVER: Hi~ client? \n",writefp);
    fputs("I love all of the world \n",writefp);
    fputs("You are awesome! \n",writefp);
    fflush(writefp);
    //When the write mode is turned off, the fileno function converts the FILE pointer into the corresponding FILE descriptor, which is the method of sending EOF (termination of connection) to the client. It is worth noting that no matter how many FILE descriptors are copied, once the shutdown function is called, it will enter the semi closed state.
    shutdown(fileno(writefp),SHUT_WR);
    fclose(writefp);
    fgets(buf,sizeof(buf),readfp);
    fputs(buf,stdout);
    fclose(readfp);
    return 0;
}
//The client is still the above sep_client.c

Operation results

//Client returned results
FROM SERVER: Hi~ client? 
I love all of the world 
You are awesome! 
//The server returns the result
FROM CLIENT: Thank you~

After the server sends the string, the writer is closed, but the reader still receives the information from the client and displays it on the terminal.

Remember: "no matter how many file descriptors are copied, you should call the shutdown function to send EOF and enter the semi closed state".

Topics: C Linux socket Network Communications