TCP/IP Network Programming Chapter 4: Implementing TCP-Based Server/Client

Enter the waiting connection state

In the previous chapters, we have learned how to create a socket, and how to bind the IP address and port to the socket. Next, we need to enter the connection waiting state by calling the listen function. Only after calling the listen function, the client can call the connect function.

#include<sys/socket.h>
int listen (int sock,int backlog);//成功时返回0,失败时返回-1
    sock    //希望进入等待连接请求状态的套接字文件描述符,传递的套接字参数称为服务器端监听套接字
    backlog //连接请求等待队列的长度      

Let’s explain the above-mentioned nouns first. The first is the server-side listening socket. In fact, it is easy to understand. The connection request from the client is also a kind of data coming from the network. We always have a socket at the beginning to accept this type of network data before the next data transmission. Let's explain the length of the waiting queue. The waiting queue is the queue of client requests, and the length is the maximum number of client requests that can be in this queue.

If the client asks the server: "Can I initiate a connection?" the server-side socket will kindly answer: "Hello! Of course, but the system is busy, please go to the waiting room and wait for a number. Your connection will be accepted immediately when you are ready." At the same time, please send the connection request to the waiting room. This gatekeeper (server-side socket) can be generated by calling the listen function. The second parameter of the listen function determines the size of the waiting room. The waiting room is called the connection request waiting queue. After the server-side socket and the connection request waiting queue are ready, the state that can receive the connection request is called the waiting connection request state.

Accept client connection requests

After accepting the request, you can enter the state of data exchange. So what is the actual data exchange? Isn't it the socket? Then it must not be the listening socket itself for data exchange, because it is also responsible for the next client request acceptance. Then another socket is needed at this time, and the next function will create the socket by itself and connect to the client that initiated the request.

#include<sys/socket.h>
int accept(int sock,struct sockaddr* addr,socketlen_t *addrlen);
//成功时返回创建的套接字文件描述符,失败时返回-1
    sock       //服务器端监听套接字的文件描述符
    addr       //保存发起请求的客户端地址信息的变量地址值
    addrlen    //保存客户端地址长度,即第二个变量的长度

After the function call is successful, the accept function will generate a socket for data I/O and return its file descriptor.

Default function call order for TCP clients

Compared with the server side, the difference lies in "request connection", which is a connection request initiated to the server side after the client socket is created. This is done by calling the following function.

#include<sys/socket.h>
int connect (int sock,struct socketaddr* servaddr,socketlen_t addrlen);
//成功时返回0,失败时返回-1
    sock     //客户端套接字文件描述符
    servaddr //保存目标服务器地址信息的变量地址值
    addrlen  //第二个结构体参数servadddr的变量地址长度(以字节为单位)

The above function will not return until one of the following conditions is encountered (complete function call)

Case 1: The server side accepts the connection request

Situation 2: The connection request is interrupted due to abnormal conditions such as network disconnection

Implement iterative server/client

Before we implemented the server-side/client-side code, after completing a connection, it was directly disconnected. This behavior is obviously unreasonable. As a server, why do you only need to connect once? The iterative server/client we implement next is actually just repeatedly accepting clients in the waiting queue. Note that what is realized at this time is that the server can only serve one client at a time, and multi-process and multi-thread will be described in the next chapter.

iterate echo server/client

Here's how the program basically works:

□The server is only connected to one client at the same time and provides echo service.

□ The server provides services to 5 clients in sequence and exits.

□The client receives the string entered by the user and sends it to the server.

□The server sends the received string data back to the client, that is, "echo".

□The string echo between the server and the client is executed until the client enters Q.

Below is the server side code

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

#defind BUF_SIZE 1024
void error_handling(char*message);

int main(int argc,char* argv[]){
    int serv_sock,clnt_sock;
    char message[BUF_SIZE];
    int str_len,i;
   
    struct socketaddr_in serv_addr,clnt_addr;
    socklen_t clnt_addr_sz;

    if(argc!=2){
        printf("Usage : %s <port>\n",argv[0]);
        exit(1);
    }

    serv_sock=socket(PF_INET,SOCE_STREAM.0);
    if(serv_sock==-1)error_handling("socket() argv[0]");
  
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
       error_handling("bind() error");

    if(listen(serv_sock,5)==-1)
       error_handling("listen() error");

    clnt_addr_sz=sizeof(clnt_addr);
    for(int i=0;i<5;++i){
        clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
        if(clnt_sock==-1)
           error_handling("accept() error");
        else
           printf("Conneted client %d \n",i+1);

    while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)
        write(clnt_sock,message,str_len);

    close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
voie error_handling(char*message){
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}




 

Look at the client code again

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

#defind BUF_SIZE 1024
void error_handling(char *message);

int main(int argc,char*argv[]){
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_addr;

    if(argc!=3){
       printf("Usage : %s <IP> <port>\n",argv[0]);
       exit(1);
    }

    sock=socket(PF_INET,SOCK_STREAM,0);
    if(sock==-1)
        error_handling("socket() error");

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
       error_handling("connect() error!");
    else 
       puts("Connected.........");

    while(1){
       fputs("Input message(Q to quit):",stdout);
       fgets(message, BUF_SIZE, stdin);
       if(!strcmp(message,"q\n") || !strcmp(message,"o\n"))
       break;

       write(sock,message,strlen(message));
       str_len=read(sock, message, BUF_SIZE-1);
       message[str_len]=0;
       printf("Message from server: %s", message);
       close(sock);
    }
    return 0;
}

void error_handling(char *message)
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(1);
}

Problems with the echo client

write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len]=0;
printf("Message from server: %s",message);

The above code has a wrong assumption: "Every time the read and write functions are called, the actual I/O operation will be performed in units of strings." Of course,
each call to the write function will pass a string, so this assumption is reasonable to some extent. But do you still remember the content of "TCP does not have data boundaries" in Chapter 2? Also need to consider the following situation on the server side: "The string is too long and needs to be sent in 2 packets!"

The server hopes to transfer data by calling the write function once, but if the data is too large, the operating system may divide the data into multiple data packets and send them to the client. In addition, during this process, the client may call the read function before receiving all the data packets.
"But doesn't the above example work?"
Of course, our echo server/client gives correct results. But that's just luck! Just because the data sent and received is small, and the operating environment is the same computer or two adjacent computers, no error occurs, but in fact there is still the possibility of error.

Windows-based implementation

Windows based echo server side

In order to convert the examples under the Linux platform to the Windows platform examples, the following 4 points need to be remembered

□Initialize and clear socket-related libraries through WSAStartup and WSACleanup functions.

□ Switch data types and variable names to Windows style.
□ Use recv and send functions instead of read and write functions in data transmission.
□ Use the closesocket function instead of the close function when closing the socket.

Guess you like

Origin blog.csdn.net/Reol99999/article/details/131676335