(Network programming notes): TCP connection establishment process, sliding window, function packaging ideas, sticky packets and concurrent cases

table of Contents

Three handshake and four waves

Window size: refers to the buffer size

Sliding window (TCP flow control)

mss 和 MTU

Function packaging ideas

Sticky bag concept

Highly concurrent server

Three handshake and four waves

  • Three-way handshake: a three-way handshake process is required to establish a connection

  • Four waves: Disconnecting requires four waves
  • TCP is connection-oriented secure data transmission
    • When the client and the server are established, they need to go through a three-way handshake process. When the client and the server are disconnected, they need to go through four waved hands. The following figure shows the three-way handshake between the client and the server to establish a connection, data The whole process of transmitting and disconnecting four waves.
  • TCP timing:

  • The meaning of the graph
    • SYN: means request
    • ACK: means confirmation
    • mss: Represents the maximum segment size. If a segment is too large and exceeds the maximum frame length of the link layer after being encapsulated into a frame, it must be fragmented at the IP layer. To avoid this situation, the client declares its maximum segment size. It is recommended that the segment sent from the server side should not exceed this length.
  • The SYN sent by the server and the SYN sent by the client itself will also occupy 1 bit
    • In the above figure, ACK represents the confirmation sequence number, and the value of the confirmation sequence number is the sequence number value sent by the other party + the length of the data, 
    • Special attention is that SYN and FIN themselves will also occupy one
  • Note:
    • SYS----->synchronous
    • ACK----->acknowledgement
    • FIN------>finish
  • The three-way handshake and four-time wave of hands are implemented in the kernel

TCP datagram format

Window size: refers to the buffer size

  • The SYN flag is no longer needed when communicating, it is only needed when requesting a connection
  • The random sequence number seq when transmitting data is the random sequence number value of the last ACK sent to the other party, and the ACK sent to the other party is the value of the ACK just sent to the other party last time.

  • The ACK confirmation packet sent in the figure represents an acknowledgment of sending data to the other party, indicating that I have received all the data you sent, and telling the other party to send the data starting with the sequence number next time
  • Since every time you send data, you will receive the confirmation packet from the other party, so you can confirm whether the other party has received it. If you don't receive the confirmation packet from the other party, it will retransmit
    • mss: Maximum message length, tell the other party how much I can receive at most at one time, you cannot exceed this length
    • win: means to tell the other party what is the maximum cache size here

Sliding window (TCP flow control)

  • Main function: The sliding window is mainly for flow control
  • See the figure below: If the sending end sends faster, the receiving end will process the data slowly after receiving the data, and the size of the receiving buffer is fixed, which will cause the receiving buffer to be full and lose data. The TCP protocol solves this problem through the "Sliding Window" mechanism.

  1. The sender initiates a connection and declares that the maximum segment size is 1460, the initial sequence number is 0, and the window size is 4K, which means "my receiving buffer has 4K bytes free, and the data you send should not exceed 4K". The receiving end responds to the connection request and declares that the maximum segment size is 1024, the initial sequence number is 8000, and the window size is 6K. The sender answers and the three-way handshake ends.
  2. The sender sends out segments 4-9, each with 1K data. The sender knows that the buffer of the receiver is full according to the window size, so it stops sending data.
  3. The application program at the receiving end picks up 2K data, and the receiving buffer has 2K free again, and the receiving end sends out segment 10, and declares that the window size is 2K while replying that it has received 6K data.
  4. The application program at the receiving end takes away 2K data again, the receiving buffer has 4K free, and the receiving end sends out segment 11 to redeclare the window size as 4K.
  5. The sender sends out segments 12-13, each with 2K data, and segment 13 also contains the FIN bit.
  6. The receiving end responds to the received 2K data (6145-8192), plus the FIN bit occupies a sequence number of 8193, so the response sequence number is 8194, the connection is in a half-closed state, and the receiving end also declares that the window size is 2K.
  7. The application at the receiving end takes away 2K data, and the receiving end re-declares the window size as 4K.
  8. The application at the receiving end takes the remaining 2K data, the receiving buffer is completely empty, and the receiving end re-declares the window size as 6K.
  9. The application at the receiving end decides to close the connection after taking away all the data. The sending segment 17 contains the FIN bit, and the sending end responds and the connection is completely closed.
  10. The above figure uses small squares at the receiving end to indicate 1K data. The solid small squares indicate the received data, and the dashed box indicates the receiving buffer. Therefore, the hollow squares in the dashed box indicate the window size, as can be seen from the figure , As the application picks up data, the dashed box slides to the right, so it is called a sliding window.
  • It can also be seen from this example that the sender sends data at one K and one K, while the application on the receiving end can withdraw data at two K and two K. Of course, it is also possible to withdraw 3K or 6K data at a time, or only at a time. Several bytes of data. In other words, the data seen by the application is a whole, or a stream. In the underlying communication, these data may be split into many data packets to be sent, but how many bytes does a data packet have for the application The program is not visible, so the TCP protocol is a stream-oriented protocol. UDP is a message-oriented protocol. Each UDP segment is a message. The application must extract data in units of messages, and cannot extract any byte of data at a time. This is very different from TCP.
  • In the figure, win means telling the other party what the size of the buffer is on this side, mss means telling the other party how much data can be received at most once, and you'd better not exceed this length.
  • When the client sends a package to the server, it does not necessarily have to wait until the server returns a response packet. Since the client knows the window size of the server, it can continue to send multiple times. When the data sent reaches the window size of the other party, it will not be sent again. You need to wait for the other party to process, and the other party can continue to send after processing.

mss 和 MTU

  • MTU : Maximum transmission unit
  • MTU: Communication term Maximum Transmission Unit (Maximum Transmission Unit, MTU)
    • Refers to the maximum data packet size (in bytes) that can pass through a certain layer of a communication protocol.
    • The parameter of the maximum transmission unit is usually related to the communication interface (network interface card, serial port, etc.). If this value is set too large, it will cause a larger amount of data to be retransmitted during packet loss and retransmission. The maximum value in the figure is 1500. In fact, It is an experience value.

  • mss: maximum packet length , just in time to establish a connection , tell each other how much data I can receive a maximum , in the process of data communication in a no mss.

Function packaging ideas

  • The idea of ​​function encapsulation-handling exceptions
    • Combine man-page and errno for encapsulation.
    • When encapsulating the name, you can capitalize the letter of the first function name. For example, socket can be encapsulated into Socket, so you can press shift+k to search, shift+k is not case-sensitive when searching for function descriptions, and you can use man page Check, the man page is not case sensitive.
  • Functions that can cause blocking like accept and read
    • If it is interrupted by a signal, because the priority of the signal is higher, the signal will be processed first. After the signal processing is completed, the accept or read will be unblocked and then returned. At this time, the return value is -1, set errno=EINTR;
    • errno=ECONNABORTED indicates that the connection was interrupted and abnormal .
  • errno宏
    • The /usr/include/asm-generic/errno.h file contains all the macros of errno and the corresponding error description information.
  • wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif
  • wrap.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>

void perr_exit(const char *s)
{
	perror(s);
	exit(-1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
	int n;

again:
	if ((n = accept(fd, sa, salenptr)) < 0) {
		if ((errno == ECONNABORTED) || (errno == EINTR))
			goto again;
		else
			perr_exit("accept error");
	}
	return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    int n;

	if ((n = bind(fd, sa, salen)) < 0)
		perr_exit("bind error");

    return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    int n;

	if ((n = connect(fd, sa, salen)) < 0)
		perr_exit("connect error");

    return n;
}

int Listen(int fd, int backlog)
{
    int n;

	if ((n = listen(fd, backlog)) < 0)
		perr_exit("listen error");

    return n;
}

int Socket(int family, int type, int protocol)
{
	int n;

	if ((n = socket(family, type, protocol)) < 0)
		perr_exit("socket error");

	return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = read(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = write(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

int Close(int fd)
{
    int n;
	if ((n = close(fd)) == -1)
		perr_exit("close error");

    return n;
}

/*参三: 应该读取的字节数*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
	size_t  nleft;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	char   *ptr;

	ptr = vptr;
	nleft = n;

	while (nleft > 0) {
		if ((nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;
			else
				return -1;
		} else if (nread == 0)
			break;

		nleft -= nread;
		ptr += nread;
	}
	return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
	size_t nleft;
	ssize_t nwritten;
	const char *ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (nwritten < 0 && errno == EINTR)
				nwritten = 0;
			else
				return -1;
		}

		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

static ssize_t my_read(int fd, char *ptr)
{
	static int read_cnt;
	static char *read_ptr;
	static char read_buf[100];

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return -1;
		} else if (read_cnt == 0)
			return 0;
		read_ptr = read_buf;
	}
	read_cnt--;
	*ptr = *read_ptr++;

	return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t n, rc;
	char    c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c  == '\n')
				break;
		} else if (rc == 0) {
			*ptr = 0;
			return n - 1;
		} else
			return -1;
	}
	*ptr  = 0;

	return n;
}

int tcp4bind(short port,const char *IP)
{
    struct sockaddr_in serv_addr;
    int lfd = Socket(AF_INET,SOCK_STREAM,0);
    bzero(&serv_addr,sizeof(serv_addr));
    if(IP == NULL){
        //如果这样使用 0.0.0.0,任意ip将可以连接
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    }else{
        if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
            perror(IP);//转换失败
            exit(1);
        }
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(port);
    Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    return lfd;
}

Sticky bag concept

  • Sticky packet: multiple data transmissions, end-to-end connection, the receiving end cannot correctly distinguish how much is sent for the first time and how much is sent for the second time.
  • Analysis and solution of sticky package problem?
    • Scheme 1: Packet header + data
      • Such as 4-bit data length + data -----------> 00101234567890 
      • Where 0010 represents the data length, and 1234567890 represents 10 bytes of data.
      • In addition, the sender and receiver can negotiate a more complex message structure, which is equivalent to an agreement agreed upon by both parties.
    • Option 2: Add end markers.
      • For example, the last character at the end is \n \$, etc.
    • Scheme 3: Fixed length of data packet
      • If the sender and receiver agree to send only 128 bytes of content each time, the receiver can receive a fixed length of 128 bytes.

Highly concurrent server

  • How to support multiple clients---support multiple concurrent servers
  • Since both accept and read functions are blocked, for example, when reading, accept cannot be called to accept new connections, and when accept is blocked waiting, you cannot read data.
  • Solution : Using multiple processes, you can let the parent process accept new connections, and let the child process handle communication with the client
    • Idea: Let the parent process accept to accept the new connection, then fork the child process, let the child process handle the communication, and exit after the child process is processed. The parent process uses the SIGCHLD signal to recycle the child process.
  • Processing flow:
1 创建socket, 得到一个监听的文件描述符lfd---socket()
2 将lfd和IP和端口port进行绑定-----bind();
3 设置监听----listen()
4 进入while(1)
  {
  	//等待有新的客户端连接到来
  	cfd = accept();
  	
  	//fork一个子进程, 让子进程去处理数据
  	pid = fork();
  	if(pid<0)
  	{
  		exit(-1);
  	}
  	else if(pid>0)
  	{
  		//关闭通信文件描述符cfd
  		close(cfd);
  	}
  	else if(pid==0)
  	{
  		//关闭监听文件描述符
  		close(lfd);
  		
  		//收发数据
  		while(1)
  		{
  			//读数据
  			n = read(cfd, buf, sizeof(buf));
  			if(n<=0)
  			{
  				break;
  			}
  			
  			//发送数据给对方
  			write(cfd, buf, n);
  		}
  		
  		close(cfd);
  		
  		//下面的exit必须有, 防止子进程再去创建子进程
  		exit(0);
  	}
  }
  close(lfd);
  • Features that need to be added: The parent process uses the SIGCHLD signal to complete the recycling of the child process
  • Note: The accept or read function is a blocking function and will be interrupted by the signal. It should not be regarded as an error at this time, errno=EINTR

Sample code

//多进程版本的网络服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	
	//设置监听
	Listen(lfd, 128);
	
	pid_t pid;
	int cfd;
	char sIP[16];
	socklen_t len;
	struct sockaddr_in client;
	while(1)
	{
		//接受新的连接
		len = sizeof(client);
		memset(sIP, 0x00, sizeof(sIP));
		cfd = Accept(lfd, (struct sockaddr *)&client, &len);
		printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
		
		//接受一个新的连接, 创建一个子进程,让子进程完成数据的收发操作
		pid = fork();
		if(pid<0)
		{
			perror("fork error");
			exit(-1);
		}
		else if(pid>0)
		{
			//关闭通信文件描述符cfd
			close(cfd);			
		}
		else if(pid==0)
		{
			//关闭监听文件描述符
			close(lfd);
			
			int i=0;
			int n;
			char buf[1024];
			
			while(1)
			{
				//读数据
				n = Read(cfd, buf, sizeof(buf));
				if(n<=0)
				{
					printf("read error or client closed, n==[%d]\n", n);
					break;
				}
				//printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
				printf("[%d]---->:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf);
				
				//将小写转换为大写
				for(i=0; i<n; i++)
				{
					buf[i] = toupper(buf[i]);
				}
				//发送数据
				Write(cfd, buf, n);
			}
			
			//关闭cfd
			close(cfd);
			exit(0);
		}
	}
	
	//关闭监听文件描述符
	close(lfd);
	
	return 0;
}
  • Solution: use multithreading
  • Let the main thread accept new connections, and let the child threads handle communication with the client; use multi-threading to set the thread to the separation attribute, and let the thread reclaim resources after exiting.
  • Multi-threaded version of the server development process
{
	1 创建socket, 得到一个监听的文件描述符lfd---socket()
	2 将lfd和IP和端口port进行绑定-----bind();
	3 设置监听----listen() 
	4 while(1)
	  {
	  	//接受新的客户端连接请求
	  	cfd = accept();
	  	
	  	//创建一个子线程
	  	pthread_create(&threadID, NULL, thread_work, &cfd);
	  	
	  	//设置线程为分离属性
	  	pthread_detach(threadID);
	  	
	  }
	  
	  close(lfd);
}	  	
	  
子线程执行函数:
	void *thread_work(void *arg)
	{
		//获得参数: 通信文件描述符
		int cfd = *(int *)arg;
		
		while(1)
		{
			//读数据
			n = read(cfd, buf, sizeof(buf));
			if(n<=0)
			{
				break;
			}
			
			//发送数据
			write(cfd, buf, n);
		}
		
		close(cfd);
	}
  • Can child threads close lfd?
    • The child thread cannot close the listening file descriptor lfd, because the child thread and the main thread share the file descriptor instead of copying it.
  • Can the main thread close cfd?
    • The main thread cannot close the cfd. The main thread and the child threads share a cfd instead of copying it. After the close, the cfd will be actually closed.
  • Multiple child threads share cfd, causing cfd to be the value of the last connection, and the previous value is overwritten. Solving ideas
struct INFO
{
	int cfd;
	pthread_t threadID;
	struct sockaddr_in client;
};
struct INFO info[100];

//初始化INFO数组
for(i=0; i<100; i++)
{
	info[i].cfd=-1;
}


for(i=0; i<100; i++)
{
	if(info[i].cfd==-1)
	{
		//这块内存可以使用
	}
}

if(i==100)
{
	//拒绝接受新的连接
	close(cfd);
}

Sample code

//多线程版本的高并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <pthread.h>
#include "wrap.h"

//子线程回调函数
void *thread_work(void *arg)
{
	int cfd = *(int *)arg;
	printf("cfd==[%d]\n", cfd);
	
	int i;
	int n;
	char buf[1024];
	
	while(1)
	{
		//read数据
		memset(buf, 0x00, sizeof(buf));
		n = Read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			printf("read error or client closed,n==[%d]\n", n);
			break;
		}
		printf("n==[%d], buf==[%s]\n", n, buf);
		
		for(i=0; i<n; i++)
		{
			buf[i] = toupper(buf[i]);
		}
		//发送数据给客户端
		Write(cfd, buf, n);	
	}
	
	//关闭通信文件描述符
	close(cfd);
	
	pthread_exit(NULL);
}
int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//设置端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	
	//设置监听
	Listen(lfd, 128);
	
	int cfd;
	pthread_t threadID;
	while(1)
	{
		//接受新的连接
		cfd = Accept(lfd, NULL, NULL);
		
		//创建子线程
		pthread_create(&threadID, NULL, thread_work, &cfd);
		
		//设置子线程为分离属性
		pthread_detach(threadID);
	}

	//关闭监听文件描述符
	close(lfd);
	
	return 0;
}
  • [Note]: Refer to the dark horse linux C++ tutorial

Guess you like

Origin blog.csdn.net/baidu_41388533/article/details/109083939