Implement TCP-based server/client

According to different data transmission methods, sockets based on network protocols are generally divided into TCP sockets and UDP sockets.

Because TCP sockets are connection-oriented, they are also called stream-based sockets. TCP is the abbreviation of Transmission Control Protocol (Transmission Control Protocol), which means "control of the data transmission process".

1. The default function call sequence of the TCP server

The figure below shows the default function call sequence of the TCP server, and most of the TCP servers are called in this order.

insert image description here

Call the socket function to create a socket, declare and initialize the address information structure variable; call the bind function to assign an address to the socket. this 2 2The two stages have been discussed before, and the next few processes will be explained below.

1.1 Enter the state of waiting for connection request (listen function)

We have called the bind function to assign an address to the socket, and then we will enter the state of waiting for a connection request by calling the listen function. Only when the listen function is called, the client can enter the state where it can issue a connection request. In other words, the client can only call the connect function at this time (if called earlier, an error will occur).

#include <sys/socket.h>

int listen(int sockfd, int backlog);

// 成功时返回0,失败时返回-1
// sockfd:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数称为服务器端套接字(监听套接字)
// backlog:连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列

"The server is in the state of waiting for a connection request" means that when the client requests a connection, it keeps the request in the waiting state until the connection is accepted. The figure below shows this process.

insert image description here

From the figure above, we can see the purpose of the file descriptor socket passed as the first parameter of the listen function. The client connection request itself is also a kind of data received from the network, and a socket is needed to receive it. This task is done by the server-side socket. A server-side socket is a gatekeeper or door that receives connection requests.

If the client asks the server: "May I initiate a connection?" the server socket will respond kindly: "Hello! Of course, but the system is busy. Please wait in the waiting room. When you are ready Your connection will be accepted immediately." At the same time, please send the connection request to the waiting room. This gatekeeper (server-side socket) is created by calling the listen function, whose second parameter 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.

The value of the second parameter of the listen function is related to the characteristics of the server side, such as the web server side that frequently receives requests should be at least 15 1515 . Also, the size of the connection request queue is always based on experimental results.

1.2 Accept client connection requests (accept function)

After calling the listen function, if there are new connection requests, they should be accepted sequentially. Accepting a request means entering a state where data is accepted. As you may have guessed, the part needed to get into this state is of course a socket! You may think that you can use server-side sockets, but server-side sockets are gatekeepers. If a gatekeeper is used in the data exchange with the client, who will guard the gate? So another socket is needed, but it is not necessary to create it yourself. The accept function will automatically create a socket and connect to the requesting client.

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 成功时返回创建的套接字文件描述符,失败时返回-1
// sockfd:服务器端套接字的文件描述符
// addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息
// addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度

The accept function accepts the pending client connection requests in the connection request queue. When the function call is successful, the accept function will generate a socket for data I/O and return its file descriptor. It should be emphasized that the socket is created automatically and automatically establishes a connection with the client that initiates the connection request. The figure below shows the accept function call process.

insert image description here

The above figure shows "take 1 1 from the waiting queue1 connection request, create a socket and complete the connection request" process. The socket created separately on the server side and the client end establish a connection for data exchange.

1.3 Review HelloWorld server side

Let's analyze the HelloWorld server-side program code hello_server.c that was not understood before .

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

void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size;

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

	// 服务器端实现过程中先要创建套接字。第28行创建套接字,但此时的套接字尚非真正的服务器端套接字。
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	
	if (serv_sock == -1)
	{
    
    
		error_handling("socket() error");
	}

	// 为了完成套接字地址分配,初始化结构体变量并调用bind函数。
	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");
	}

	// 调用listen函数进入等待连接请求状态。连接请求等待队列的长度设置为5。此时的套接字才是服务器端套接字。
	if (listen(serv_sock, 5) == -1)
	{
    
    
		error_handling("listen() error");
	}
	
	clnt_addr_size = sizeof(clnt_addr);
	
	// 调用accept函数从队头取1个连接请求与客户端建立连接,并返回创建的套接字文件描述符。
	// 另外,调用accept函数时若等待队列为空,则accept函数不会返回,直到队列中出现新的客户端连接。
	clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
	
	if (clnt_sock == -1)
	{
    
    
		error_handling("accept() error");
	}

	// 调用write函数向客户端传输数据,调用close函数关闭连接。
	write(clnt_sock, message, sizeof(message));

	close(clnt_sock);
	close(serv_sock);
	
	return 0;
}

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

2. The default function call sequence of the TCP client

Next, explain the implementation sequence of the client side, which is much simpler than the server side. Because creating sockets and requesting connections is what the client is all about, as shown in the diagram below.

insert image description here

2.1 Initiate a connection request (connect function)

Compared with the server side, the difference lies in the "request connection", which is a connection request initiated to the server side after the client socket is created. After the server calls the listen function, a connection request waiting queue is created, and then the client can request a connection. So how to initiate a connection request? This is done by calling the connect function.

#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *servaddr, socklen_t addrlen);

// 成功时返回0,失败时返回-1
// sockfd:客户端套接字文件描述符
// servaddr:保存目标服务器端地址信息的变量地址值
// addrlen:以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度

After the client calls the connect function, one of the following situations will occur before returning (the function call is completed):

  • The server side receives the connection request
  • The connection request is interrupted due to abnormal conditions such as network disconnection

It should be noted that the so-called "receiving connection" does not mean that the server calls the accept function, but the server records the connection request information in the waiting queue. Therefore, data exchange is not performed immediately after the connect function returns.

Q: Where is the client socket address information?

One of the necessary processes to implement the server is to assign an IP and port number to the socket. However, there is no socket address allocation in the implementation process of the client, but the connect function is called immediately after the socket is created. Don't client sockets need to assign IP and port?

of course not! Network data exchange must assign IP and port.

That being the case, when, where, and how are client sockets assigned addresses?

  • when? When the connect function is called.
  • where? The operating system, more precisely in the kernel.
  • how? The IP uses the IP of the computer (host), and the port is random.

The IP address and port of the client are automatically allocated when calling the connect function, and there is no need to call the marked bind function for allocation.

2.2 Review HelloWorld client

Let's analyze the previously incomprehensible HelloWorld client program code hello_client.c.

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

void error_handling(char *message);

int main(int argc, char* argv[])
{
    
    
	int sock;
	struct sockaddr_in serv_addr;
	char message[30];
	int str_len;
	
	if (argc != 3)
	{
    
    
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	// 创建准备连接服务器端的套接字,此时创建的是TCP套接字。
	sock = socket(PF_INET, SOCK_STREAM, 0);

	if (sock == -1)
	{
    
    
		error_handling("socket() error");
	}

	// 结构体变量serv_addr中初始化IP和端口信息。初始化值为目标服务器端套接字的IP和端口信息。
	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]));

	// 调用connect函数向服务器端发送连接请求
	if (connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
	{
    
    
		error_handling("connect() error!");
	}

	// 完成连接后,接收服务器端传输的数据
	str_len = read(sock, message, sizeof(message) - 1);
	
	if (str_len == -1)
	{
    
    
		error_handling("read() error!");
	}

	printf("Message from server: %s\n", message);

	// 接收数据后调用close函数关闭套接字,结束与服务器端的连接
	close(sock);

	return 0;
}

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

3. Function call relationship between server and client based on TCP

The implementation sequence of the TCP server and client is explained above. In fact, the two are not independent of each other. The interaction process between them is shown in the figure below.

insert image description here

The overall process of the above figure is organized as follows: After the server creates a socket, it continuously calls the bind and listen functions to enter the waiting state, and the client initiates a connection request by calling the connect function. It should be noted that the client can only call the connect function after the server calls the listen function. At the same time, it should be clear that before the client calls the connect function, the server may first call the accept function. Of course, at this time, the server enters a blocking state when calling the accept function until the client calls the connect function.

Guess you like

Origin blog.csdn.net/qq_42815188/article/details/129507416