Reflex Client-Server Model (4)

Keywords: socket network

Because TCP is a byte-stream-based transmission, it belongs to no-boundary transmission, so it can not deal with the boundary problem between messages, so there is a sticky packet problem.

As shown in the following figure:


M1 and M2 are two messages transmitted from Host A to Host B. There may be several kinds of transmission in the middle:

a. The two messages are transmitted separately and completely.

b. One part of M1 and M2 is transmitted first, and the other part of M2 is transmitted separately.

c. First, part of M1 is transmitted, and then the other part of M1 is transmitted with M2.


Causes of sticking:


There are several reasons leading to sticking problems:

a. The size of the application process buffer is larger than the size of the socket interface buffer, so the data in the buffer can not be sent at one time, resulting in sticky packets.

B. The maximum limit of data segment transmitted by TCP layer is MSS, so if the data of high level exceeds this value, it also needs to be partitioned.

c. The maximum transmission unit in the link layer is MTU. Therefore, when the size of the data packet in the upper layer exceeds this value, it needs to be fragmented.

d. Other packet sticking problems, such as TCP traffic control, congestion control, delay sending mechanism and so on, may lead to packet sticking problems.


Glue package solution:

a. Maintaining the boundaries of messages and messages at the application level is essential.

b. Sending fixed-length packets (allowing data to be sent and received in a fixed-length manner);

c. Packet tail with r\n and other partitioners;

d. Design more complex application layer protocols;


Next, we send and receive data packets in the way of fixed-length packets, mainly encapsulating readn and writen functions.

Server side: echosrv.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)

struct package{
	int len;
	char buf[1024];
};

ssize_t readn(int fd, void* buf, size_t count)
{
	//Because there is no guarantee that count s can be read at a time
	//So we need to read in a loop.
	//Until the number of bytes read is count
	size_t nleft = count;
	ssize_t nread;
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)//If the signal is interrupted
				continue;
			return -1;
		}
		if(nread == 0)
			//Represents peer closure, returned directly here
			return count-nleft;
		nleft -= nread;//The number of bytes left after each read
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	//The number of bytes we want to write each time is count
	size_t nleft = count;
	ssize_t nwritten;
	char* bufp = (char*)buf;
 	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			//Nothing happened.
			continue;
		nleft -= nwritten;//The number of bytes left to write after each write
		bufp += nwritten;
	}
	return count;
}

void do_service(int conn)
{
	struct package recvbuf;
	int n;
	while(1)
	{
		memset(&recvbuf, 0, sizeof(recvbuf));
		//Packet Head and Packet Body Read Separately
		//First read 4 bytes of the packet header, and then determine the length of the package.
		int ret = readn(conn,  &recvbuf.len, 4);
		if(ret == -1)
			ERR_EXIT("read failure");
		else if(ret < 4)
		{
			printf("client close\n");
			break;
		}

		//Read the enclosure again
		//The length of inclusion n is stored in len of the structure
		n = ntohl(recvbuf.len);
		ret = readn(conn, recvbuf.buf, n);
		if(ret == -1)
			ERR_EXIT("read failure");
		else if(ret < n)
		{
			printf("client close\n");
			break;
		}
		fputs(recvbuf.buf, stdout);
		writen(conn, &recvbuf, 4+n);
	}
}

int main(void)
{
	//Create a socket
	int listenfd;
	if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
//	If ((listenfd = socket (AF_INET, SOCK_STREAM, 0)) < 0// / Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
		ERR_EXIT("socket_failure");
		//Initialization address
		struct sockaddr_in servaddr;
		memset(&servaddr, 0, sizeof(servaddr));
		servaddr.sin_family = AF_INET;//Address family
		servaddr.sin_port = htons(5188);

		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//Represents the arbitrary address of the machine, recommended for use (converted into network byte order)
		//You can also specify explicitly by yourself
//		servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		//perhaps
//		inet_aton("127.0.0.1", &servaddr.sin_addr);
		
		//Open address reuse before binding
		int on = 1;
		if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
			ERR_EXIT("setsockopt_failure");

		//Next, bind the socket to a local address
		//The IPv4 address structure needs to be mandatory to be converted to a universal address structure
		if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
			ERR_EXIT("bind_failure");//Binding failed
		
		//The next step is listening, which changes the socket from close state to listen state to accept the connection.
		if(listen(listenfd, SOMAXCONN) < 0)
			ERR_EXIT("listen_failure"); 

		//Define the address of the other party
		struct sockaddr_in peeraddr;
		socklen_t peerlen = sizeof(peeraddr);
		int conn; //A new socket, called a connected socket (active socket)

		pid_t pid;
		while(1)
		{
			if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)				
				ERR_EXIT("accept_failure");
			//The address and port of the output client		
			printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
			//Once you have a connection, create a process
			pid = fork();
			if(pid == -1)
				ERR_EXIT("fork_failure");
			if(pid == 0)
			{
				//Let subprocesses handle existing communication processes
				//No longer need to listen socket interface
				close(listenfd);
				do_service(conn);
				//Once the do_service function returns, the process has no value in existence.
				exit(EXIT_SUCCESS);//At this point, the process of opening up for clients is also destroyed.
			}
			else
				//The parent process accept s
				//There is no need to connect socket interfaces anymore, namely conn (parent-child process shared file descriptor)
				close(conn);
				
		}

		//Implementing a Reflex Client/Server Model
		//That is, the client obtains data from standard input, sends it to the server, and then echoes back to the server.
		
		return 0;
}


Client: echocli.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)

struct package{
	int len;
	char buf[1024];
};

ssize_t readn(int fd, void* buf, size_t count)
{
	size_t nleft = count; //The number of bytes remaining to read
	ssize_t nread; //Number of bytes read at a time
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nread == 0)//Peer closure
			return count-nleft;
		nleft -= nread;
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	size_t nleft = count; //The number of bytes remaining to read
	ssize_t nwritten; //Number of bytes read at a time
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			continue;
		nleft -= nwritten;
		bufp += nwritten;
	}
	return count;	
}

int main(void)
{
	int sock;//Create a socket
	if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
//	If ((sock = socket (AF_INET, SOCK_STREAM, 0)< 0)// / Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
		ERR_EXIT("socket_failure");
		
		struct sockaddr_in servaddr;
		memset(&servaddr, 0, sizeof(servaddr));
		servaddr.sin_family = AF_INET;//Address family
		servaddr.sin_port = htons(5188);

		//Explicitly specify the server-side address yourself
		servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		
		//Clients do not need bind or listen.
		//Just connect directly to the past.
		if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
			ERR_EXIT("connect_failure");

		//If the connection is successful, you can communicate
		struct package sendbuf;
		struct package recvbuf;
		memset(&sendbuf, 0, sizeof(sendbuf));
		memset(&recvbuf, 0, sizeof(recvbuf));
		int n;
		while(fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
		{
			n = strlen(sendbuf.buf);
			sendbuf.len = htonl(n);
			writen(sock, &sendbuf, 4+n);
			//Read the header four bytes first
			int ret = readn(sock, &recvbuf.len, 4);
			if(ret == -1)
				ERR_EXIT("read error");
			else if(ret == 0)
			{
				printf("peer close\n");
				break;
			}
			//Read the package and store the head length in recvbuf.len
			n = ntohl(recvbuf.len);
			ret = readn(sock, recvbuf.buf, n);
			if(ret == -1)
				ERR_EXIT("read error");
			else if(ret == 0)
			{
				printf("peerclose\n");
				break;
			}
			//Show up
			fputs(recvbuf.buf, stdout);
			//Here we need to clear the buffer
			memset(&sendbuf, 0, sizeof(sendbuf));
			memset(&recvbuf, 0, sizeof(recvbuf));			
		}
		//Close socket interface
		close(sock);
		
		return 0;
}
Note: For writing writen, we send a packet with a total length of 4+n, including a 4-byte header and an n-byte body.

For reading readn, we read the header and the enclosure separately, because we store the length n of the enclosure in the len variable of the structure when sending, so we need to read the header first, then we can get the length of the enclosure.


Here's another way to solve the sticky package problem. We encapsulate a readline function, which reads messages by line.

Look at the code first:

Server side: ehcosrv.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)


ssize_t readn(int fd, void* buf, size_t count)
{
	//Because there is no guarantee that count s can be read at a time
	//So we need to read in a loop.
	//Until the number of bytes read is count
	size_t nleft = count;
	ssize_t nread;
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)//If the signal is interrupted
				continue;
			return -1;
		}
		if(nread == 0)
			//Represents peer closure, returned directly here
			return count-nleft;
		nleft -= nread;//The number of bytes left after each read
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	//The number of bytes we want to write each time is count
	size_t nleft = count;
	ssize_t nwritten;
	char* bufp = (char*)buf;
 	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			//Nothing happened.
			continue;
		nleft -= nwritten;//The number of bytes left to write after each write
		bufp += nwritten;
	}
	return count;
}

ssize_t recv_peek(int sockfd, void* buf, size_t len)
{
	//This function can receive data from a socket interface
	//But data is not removed from the buffer
	while(1)
	{
		int ret = recv(sockfd, buf, len, MSG_PEEK);
		//Read the data and return, otherwise return
		if(ret == -1 && errno == EINTR)
			continue;
		return ret;
	}
}

ssize_t readline(int sockfd, void*buf, size_t maxline)
{
	//The read process does not necessarily read maxline bytes
	//Just encounter n to return
	int ret;
	int nread;
	char* bufp = buf;
	int nleft = maxline;
	while(1)
	{
		ret = recv_peek(sockfd, bufp, nleft);
		if(ret < 0)
			return ret;
		else if(ret == 0)
			return ret;
		nread = ret;
		int i;
		for(i = 0; i < nread; ++i)
		{
			if(bufp[i] == '\n')
			{
				//Our recv_peek is just peeping at the data.
				//There is no data left.
				//So here we use readn to remove peeped data from the buffer
				ret = readn(sockfd, bufp, i+1);
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//No encounter \n
		if(nread > nleft)
			exit(EXIT_FAILURE);
		//Remove read data nread bytes from the buffer
		nleft -= nread;
		ret = readn(sockfd, bufp, nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		//The next peep needs to be offset.
		bufp += nread;
	}
	return -1;
}

void do_service(int conn)
{
	char recvbuf[1024];
	int n;
	while(1)
	{
		memset(&recvbuf, 0, sizeof(recvbuf));
		int ret = readline(conn,  recvbuf, 1024);
		if(ret == -1)
			ERR_EXIT("read failure");
		if(ret == 0)
		{
			printf("client close\n");
			break;
		}
		fputs(recvbuf, stdout);
		writen(conn, recvbuf, strlen(recvbuf));
	}
}

int main(void)
{
	//Create a socket
	int listenfd;
	if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
//	If ((listenfd = socket (AF_INET, SOCK_STREAM, 0)) < 0// / Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
		ERR_EXIT("socket_failure");
		//Initialization address
		struct sockaddr_in servaddr;
		memset(&servaddr, 0, sizeof(servaddr));
		servaddr.sin_family = AF_INET;//Address family
		servaddr.sin_port = htons(5188);

		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//Represents the arbitrary address of the machine, recommended for use (converted into network byte order)
		//You can also specify explicitly by yourself
//		servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		//perhaps
//		inet_aton("127.0.0.1", &servaddr.sin_addr);
		
		//Open address reuse before binding
		int on = 1;
		if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
			ERR_EXIT("setsockopt_failure");

		//Next, bind the socket to a local address
		//The IPv4 address structure needs to be mandatory to be converted to a universal address structure
		if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
			ERR_EXIT("bind_failure");//Binding failed
		
		//The next step is listening, which changes the socket from close state to listen state to accept the connection.
		if(listen(listenfd, SOMAXCONN) < 0)
			ERR_EXIT("listen_failure"); 

		//Define the address of the other party
		struct sockaddr_in peeraddr;
		socklen_t peerlen = sizeof(peeraddr);
		int conn; //A new socket, called a connected socket (active socket)

		pid_t pid;
		while(1)
		{
			if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)				
				ERR_EXIT("accept_failure");
			//The address and port of the output client		
			printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
			//Once you have a connection, create a process
			pid = fork();
			if(pid == -1)
				ERR_EXIT("fork_failure");
			if(pid == 0)
			{
				//Let subprocesses handle existing communication processes
				//No longer need to listen socket interface
				close(listenfd);
				do_service(conn);
				//Once the do_service function returns, the process has no value in existence.
				exit(EXIT_SUCCESS);//At this point, the process of opening up for clients is also destroyed.
			}
			else
				//The parent process accept s
				//There is no need to connect socket interfaces anymore, namely conn (parent-child process shared file descriptor)
				close(conn);
				
		}

		//Implementing a Reflex Client/Server Model
		//That is, the client obtains data from standard input, sends it to the server, and then echoes back to the server.
		
		return 0;
}


Client: echocli.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)

ssize_t readn(int fd, void* buf, size_t count)
{
	size_t nleft = count; //The number of bytes remaining to read
	ssize_t nread; //Number of bytes read at a time
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nread == 0)//Peer closure
			return count-nleft;
		nleft -= nread;
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	size_t nleft = count; //The number of bytes remaining to read
	ssize_t nwritten; //Number of bytes read at a time
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			continue;
		nleft -= nwritten;
		bufp += nwritten;
	}
	return count;	
}

ssize_t recv_peek(int sockfd, void* buf, size_t len)
{
	while(1)
	{
		int ret = recv(sockfd, buf, len, MSG_PEEK);
		if(ret == -1 && errno == EINTR)
			continue;
		//Sneak at the data and return it directly
		return ret;
	}
}

ssize_t readline(int sockfd, void*buf, size_t maxline)
{
	char* bufp = buf;
	int nleft = maxline;
	int nread;
	int ret;
	while(1)
	{
		ret = recv_peek(sockfd, bufp, nleft);
		if(ret < 0)
			return ret;
		if(ret == 0)
			return ret;
		nread = ret;
		int i;
		for(i = 0; i < nread; ++i)
		{
			if(bufp[i] == '\n')
			{
				ret = readn(sockfd, bufp, i+1);
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//No encounter \n
		if(nread > nleft)
			exit(EXIT_FAILURE);
		nleft -= nread;
		ret = readn(sockfd, bufp, nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		//Keep on peeping
		bufp += nread;
	}
	return -1;
}


int main(void)
{
	int sock;//Create a socket
	if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
//	If ((sock = socket (AF_INET, SOCK_STREAM, 0)< 0)// / Both AF_INET and PF_INET can be used. The first two parameters can be determined to be TCP, so the third parameter can be set to 0.
		ERR_EXIT("socket_failure");
		
	struct sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;//Address family
	servaddr.sin_port = htons(5188);

	//Explicitly specify the server-side address yourself
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	
	//Clients do not need bind or listen.
	//Just connect directly to the past.
	if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
		ERR_EXIT("connect_failure");

	//Connect successfully, view the local port and address
	struct sockaddr_in localaddr;
	socklen_t addrlen = sizeof(localaddr);
	if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
		ERR_EXIT("getsockname error");
	
	printf("IP=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));

	//If the connection is successful, you can communicate
	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};
	while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
	{
		writen(sock, sendbuf, strlen(sendbuf));
		//Read the header four bytes first
		int ret = readline(sock, recvbuf, 1024);
		if(ret == -1)
			ERR_EXIT("read error");
		else if(ret == 0)
		{
			printf("peer close\n");
			break;
		}
		//Show up
		fputs(recvbuf, stdout);
		//Here we need to clear the buffer
		memset(sendbuf, 0, sizeof(sendbuf));
		memset(recvbuf, 0, sizeof(recvbuf));			
	}
	//Close socket interface
	close(sock);
	
	return 0;
}

Explanation: First, we encapsulate a recv_peek function by using recv function. The recv function only receives data from the socket buffer to the buffer, but it does not remove the data from the buffer, while the read function will remove the data from the buffer after reading the data. Therefore, we can use the recv_peek function to "peep" the contents of the buffer first, then we can know whether there is a separator in the "peek" content (we are here n), "peep" and then use readn to read and remove the read data from the buffer, so that we can read the data in line.

Posted by mizz key_me on Thu, 11 Apr 2019 21:24:31 -0700