计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制,TCP传输控制协议,RTT和RTO

一.阻塞和非阻塞

1.我们举一下生活中的例子来更深刻的了解一下阻塞和非阻塞

阻塞:烧水,直到水烧完了,再继续接下来的事情

非阻塞:烧水,在烧水的同时,去做别的事情,每隔一段时间查看水是否烧完了

2.将套接字默认的阻塞变为非阻塞(这里以接收函数为例)

我们在前面的博客中分别写了UDP的客户端和服务端,但是我们发现服务端和客户端的接收函数recvfrom(),只有在接收到数据的时候才会继续执行后面的内容,这就是阻塞(注意是因为套接字默认是阻塞的,所以这个函数才是阻塞的)

这里我们对之前博客(计网----七层网络模型-CSDN博客)写的客户端进行一下修改,使其客户端接收数据变为不是阻塞的

(1)我们将默认的阻塞的套接字设置成非阻塞的套接字(使用ioctlsocket()函数),在调用recvfrom()之前完成该操作

//设置套接字为非阻塞
	u_long iMode = 1;
	ioctlsocket(sock, FIONBIO, &iMode);//第一个参数是设置哪个套接字
									//,第二个参数是命令,是固定好的就是FIONBIO
									//,第三个参数是命令的参数,等于0就是阻塞的,不等于0就是非阻塞的

(2)对接收数据的代码进行一下修改(因为如果没有接收到数据就进行下一步那么就会出现10035的错误,但是这时没有接收到数据是我们所期待的,所以这里我们需要把这个错误排除掉)

//4.接受数据
		recv=recvfrom(sock, recvData,sizeof(recvData),0, (sockaddr*)&recvaddr, &recvaddrlength);
		if (recv > 0) {
			//打印ip地址和接受的数据
			cout << "IP:" << inet_ntoa(recvaddr.sin_addr)/*这里因为recvaddr.sin_addr这个变量村的ip地址是u_long类型的,看不懂,所以我们要转成字符串类型*/ << "     " << "SAY: " << recvData << endl;
		}
		else if(WSAGetLastError()!= 10035){//失败
			cout << "recv failed  "<< WSAGetLastError() << endl;
			break;
		}

3.发送函数的阻塞和非阻塞

当发送缓冲区空间不足够的时候发送函数才会有阻塞和非阻塞

阻塞:当发送缓冲区空间不足够的时候,等待空间足够大再往发送缓冲区中拷贝数据

非阻塞:当发送缓冲区空间不足够的时候,有多少空间拷贝多少数据进去,剩余没拷贝的数据由应用程序自己决定(重发一次完整的数据或者发那部分没发送的数据)

4.阻塞的好处

(1)非阻塞会一直占着CPU,所以正常情况下我们都是使用阻塞模式并配合使用线程(线程是由系统自动调度的,不会一直占着CPU)来实现数据的互相传输(这里的传输是可以一方一直输出数据的,不用等对方接收之后才能再次输出)

(2)阻塞对数据反应快,非阻塞对数据反应慢

二.虚拟内存

应用程序不运行的时候存在磁盘里,当运行的时候就会被调用到内存空间里,每个程序会被分配给4个G的虚拟内存(虚拟的意思:实际分配的不是4个g,假设如果最开始是500M的话,不够了会继续给你分配内存,但最多只能分配4个G)

1.虚拟内存被分为两部分

1> 0~2G(内核空间)

大家一起共同使用的

2> 2~4G(用户空间)

每个应用程序自己的空间

2.数据是从哪来到哪去的

当我们在应用程序里创建一个socket的时候,操作系统会在内核空间里分配两个缓冲区,一个缓冲区叫发送缓冲区,一个缓冲区叫接收缓冲区

1.从哪来

当其他设备通过网络给这个应用发送数据,操作系统收到之后会写到接收缓冲区里,当我们调用recvfrom()这个函数时,应用程序就会把接收缓冲区里的数据拷到当前进程的变量中去

2.到哪去

当应用程序发送数据的时候,应用程序会先把要发送的数据写到当前进程的变量中去,当我们调用sendto()这个函数时,就会把数据从当前进程的变量发送缓冲区中去,然后操作系统会通过网卡继续发送

这里我们还可以知道阻塞的好处就是反应快,如果是非阻塞无法立刻与数据做出反应

3.获取接收缓冲区和发送缓冲区的大小(我们在之前博客(计网----七层网络模型-CSDN博客)写的UDP客户端中进行操作)

使用getsocketopt()函数

//获取发送缓冲区和接收缓冲区的大小
	int rebuf = 0;//接收数据大小的变量
	int sebuf = 0;//发送数据大小的变量
	int size = sizeof(rebuf);//接收数据的那个变量类型的大小
	getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rebuf, &size);
	//第一个参数是设置哪个套接字
	//第二个参数是操作的等级
	//第三个参数是操作的名字
	//第四个参数是接收数据大小的变量
	//第五个参数是接收数据的那个变量类型的大小
	getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sebuf, &size);
	//进行输出操作,得出发送缓冲区和接收缓冲区都是65536个字节=64KB
	cout << "rebufsize:  " << rebuf << "     " << "sebufsize:  " << sebuf << endl;

因为发送缓冲区和接收缓冲区都是65536个字节(64KB)

所以当我们发送一个大文件的时候,需要分开发

当我们接收一个大文件的时候,需要分开收,全部收到之后再组合到一起

三.UDP的特点

1.面向非链接,接收数据时,可以收发任意设备的数据,可以是一对一,也可以是一对多(广播、组播)

2.通讯方式:数据报文的通讯方式,数据包时不可拆分的

3.传输效率高(跟TCP做对比,UDP协议头只有源端口和目的端口,占8个字节)

4.会产生丢包,因为没有校验检查,可能会出现乱序

四.以太网帧结构

以太网帧结构由报头/起始帧分解符 MAC头部 IP头部 TCP头部 数据 FCS组成

MTU:IP头部,TCP头部和数据这三部分的最大长度就是MTU

​ 一个网络包的最大长度,以太网中一般为1500个字节

MISS:数据的最大长度就是MISS

​ 除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度

​ MISS的最大大小为1500-20(ip头)-20(tcp头)=1460

五.IP协议格式

图中的一行表示32位,4个字节

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第1张图片

1.IP协议分为两部分

1.固定部分(必须要有的)

1> 版本:用的IPV4还是IPV6,用的哪个版本的IP地址

2> 首部长度:记录IP头的首部总长度,用来取出IP头

3> 总长度:IP数据包的总长度

当数据的大小超过了MTU的最大长度1500个字节那么要进行分片(4,5,6是在分片的基础上使用的)

4> 标识:给每个数据报加一个标识,使发送的数据按照应有顺序进行处理

5> 标志:判断可不可以分片,是否是当前最后一片

6> 片偏移:用来组合分片的数据报

7> 生存时间:实际上就是一个计时器,经过一定时间(经过几个路由器)后还没有到达目的地,那么就要丢掉这个包

8> 协议:传输层使用的哪个协议

9> 首部检验和:通过一种算法把首部所有的数据算出一个数,这个就是检验和,保证整个首部在传输中数据没有被修改,如果收到了发现算出来的数和首部检验和的存的那个数不一致,那么当前包就是无效包了,丢弃

2.可变部分(最大长度是40)

1> 可选字段:可选字段数据的添加:如果要往里可选字段里添加数据,必须是4个字节的倍数数据

​ 可选字段中数据的传输规则:采用TVL(Type Value Length)格式的)

注意:当对端设备没有应用层时,如果要给它传数据,就需要在网络层传所以就用到了ip头中的可选字段

六.写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议)

1.看下图,理解使用TCP协议的C/S模型的数据通信的过程

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第2张图片

2.使用TCP协议的C/S模型的数据通信的过程

1.服务端

1.加载库(库为ws2_32.lib)

2.创建套接字(所用到的函数为socket())

3.绑定IP地址和端口号

4.监听(所用到的函数为listen())

5.接受连接(所用到的函数为accept()),注意这里的函数成功后会产生一个新的socket,我们之后要进行关闭

6.接收数据(所用到的函数为recv())

7.发送数据(所用到的函数为send())

8.关闭连接产生的套接字((所用到的函数为closesocket()))

9.关闭套接字(所用到的函数为closesocket())

10.卸载库(所用到的函数为WSACleanup())

2.客户端

1.加载库(库为ws2_32.lib)

2.创建套接字(所用到的函数为socket())

3.连接

4.发送数据

5.接收数据

6.关闭套接字(所用到的函数为closesocket())

7.卸载库(所用到的函数为WSACleanup())

3.使用代码实现使用TCP协议的C/S模型的数据通信
创建一个服务端
#include
#include

using namespace std;
#pragma comment(lib,"Ws2_32.lib")
int main() {

	//1.加载库
	WORD version=MAKEWORD(2,2);
	WSADATA data;
	int err = WSAStartup(version, &data);
	if (err != 0) {
		cout << "WSAStartup error" << endl;
		WSACleanup();
		return 1;
	}
	//判断版本号是否正确
	if (HIBYTE(data.wVersion) != 2 || LOBYTE(data.wVersion) != 2) {
		cout << "WSAStartup Versione error" << endl;
		WSACleanup();
		return 1;
	}
	else {
		cout << "WSAStartup success" << endl;
	}
	//2.创建套接字
	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock == INVALID_SOCKET) {
		cout << "sock error" << endl;
		cout << WSAGetLastError() << endl;
		WSACleanup();
		return 1;
	}
	else {
		cout << "sock success" << endl;
	}
	//3.绑定ip地址和端口号
	sockaddr_in addServe;
	addServe.sin_family = AF_INET;
	addServe.sin_port = htons(666666);//转换成网络字节序
	addServe.sin_addr.S_un.S_addr = INADDR_ANY;//绑定任意网卡
	err=bind(sock, (sockaddr*)&addServe, sizeof(addServe));
	if (err == SOCKET_ERROR) {
		cout << "bind error" << endl;
		cout << WSAGetLastError() << endl;
		closesocket(sock);
		WSACleanup();
		return 1;
	}
	else {
		cout << "bind success" << endl;
	}


	//4.监听
	err = listen(sock, 10);/*第一个参数是使用哪一个socket监听
							第二个参数是等待队列的最大长度,等待队列是用来存等待连接的数据
							*/
	if (err == SOCKET_ERROR) {
		cout << "listen error" << endl;
		cout << WSAGetLastError() << endl;
		closesocket(sock);
		WSACleanup();
		return 1;
	}
	else {
		cout << "listen success" << endl;
	}
	while (true) {

		//5.接受连接
		sockaddr_in addaccept;
		int size = sizeof(addaccept);
		SOCKET sockLIS = accept(sock, (sockaddr*)&addaccept, &size);/*第一个参数是使用哪一个socket接受连接
																	第二个参数是sockaddr类型的结构体指针,里面存的是连接服务端的应用程序的ip地址和端口号
																	第三个参数是sockaddr结构体类型的大小
																	如果成功了返回的是一个新的socket,这个返回的socket只能和他连接的客户端就行通信
																	如果失败了返回的是INVALID_SOCKET
																	*/
		
		if (sockLIS != INVALID_SOCKET) {
			//连接成功,打印来连接的客户端ip
			cout << "ip" << inet_ntoa(addaccept.sin_addr) << endl;
		}
		else {
			cout << "accept error" << endl;
			cout << WSAGetLastError() << endl;
			break;
		}
		int rv = 0;
		int sd = 0;
		char recvbuf[1024] = "";
		char sendbuf[1024] = "";

		while (true) {

			//6.接收数据
			rv = recv(sockLIS, recvbuf, sizeof(recvbuf), 0);/*第一个参数是使用哪一个socket通信
															第二个参数是用来接收数据的空间
															第三个参数是用来接收数据的空间的长度
															第四个参数是用来接收数据的空间的长度第四个参数是标志位,如果有特殊套接字的话要设置标志位,这里无特殊的,不需要使用标志位,填0
															*/
			if (rv == SOCKET_ERROR) {
				cout << "recv error" << endl;
				cout << WSAGetLastError() << endl;
				break;
			}
			else {
				cout << "IP: " << inet_ntoa(addaccept.sin_addr) << "   " << "Client Say: " << recvbuf << endl;;
			}


			//7.发送数据
			gets_s(sendbuf);
			sd = send(sockLIS, sendbuf, sizeof(sendbuf), 0);
			if (sd == SOCKET_ERROR) {
				cout << "send error" << endl;
				cout << WSAGetLastError() << endl;
				break;
			}
			
		}

		//8.关闭连接产生的套接字
		closesocket(sockLIS);

	}
	
	//9.关闭套接字
	closesocket(sock);

	//10.卸载库
	WSACleanup();
	return 0;
}
创建一个客户端
#include
#include
using namespace std;
#pragma comment(lib,"Ws2_32.lib")
int main() {
	//1.加载库
	WORD version = MAKEWORD(2, 2);
	WSADATA adddata;
	int err = WSAStartup(version, &adddata);
	if (err != 0) {
		cout << "WSAStartup error" << endl;
	}

	if (HIBYTE(adddata.wVersion) != 2 || LOBYTE(adddata.wVersion) != 2) {
		cout << "WSAStartup  Version  error" << endl;
		WSACleanup();
	}

	//2.创建套接字
	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock == INVALID_SOCKET) {
		cout << "socket error" << endl;
		cout << WSAGetLastError() << endl;
		WSACleanup();
	}
	else {
		cout << "socket success" << endl;
	}
	sockaddr_in serve;
	serve.sin_family = AF_INET;
	serve.sin_port = htons(666666);
	serve.sin_addr.S_un.S_addr = inet_addr("192.168.3.111");
	/*serve.sin_port = htons(67856);
	serve.sin_addr.S_un.S_addr = inet_addr("192.168.3.178");*/

	//3.连接
	err=connect(sock, (sockaddr*)&serve, sizeof(serve));/*
														第一个参数是用哪个套接字连接
														第二个参数是一个sockaddr的结构体(存服务端的IP地址和端口号)
														第三个参数是第二个参数结构体的大小
														*/
	if (err == SOCKET_ERROR) {
		cout << "connect error" << endl;
		cout << WSAGetLastError() << endl;
		closesocket(sock);
		WSACleanup();
	}
	else {
		cout << "connect success" << endl;
	}
	int rv = 0;
	int sd = 0;
	char recvbuf[1024] = "";
	char sendbuf[1024] = "";

	while (true) {
		//4.发送数据
		gets_s(sendbuf);
		sd=send(sock, sendbuf, sizeof(sendbuf), 0);
		if (sd == SOCKET_ERROR) {
			cout << "send error" << endl;
			cout << WSAGetLastError() << endl;
			closesocket(sock);
			WSACleanup();
		}

		//5.接收数据
		rv = recv(sock, recvbuf, sizeof(recvbuf), 0);
		if (rv > 0) {
			cout << "Serve Say: " << recvbuf << endl;
		}
		else {
			cout << "recv error" << endl;
			cout << WSAGetLastError() << endl;
			closesocket(sock);
			WSACleanup();
		}
	}
	//6.关闭套接字
	closesocket(sock);
	//7.卸载库
	WSACleanup();
	return 0;
}

七.TCP协议头

看下面图进行了解

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第3张图片

1.序号

功能:保持接收方接受数据有序

2.确认号

功能:告诉发送方收到了这个数据(假设收到了编号为10号的数据包,那ack就会返回11,告诉发送方该发送第11号数据包了)

3.首部长度

功能:记录TCP头总共有多长,方便在数据包中把这个TCP头截取出来,然后把用户数据传给应用层

4.大写的字母

功能:一般在协议头中都是用来做标志位的(每一个标志位后面都对应着一个功能,标志位 置1了就代表要使用这个功能,置0了就代表不使用这个功能)

注意:如果要同时使用几个功能,那么将这些功能的标志位异或起来

FIN标志位代表断开连接的功能

SYN标志位代表建立连接的功能

RST标志位代表重新建立连接的功能

PSH标志位代表快速处理此数据包的功能

ACK标志位代表使用确认号的功能(如果当前包是一个确认包就要使用该功能)

URG标志位代表的使用紧急指针的功能

5.校验和

功能:确保当前协议头里面的数据在传输的过程中没有被篡改

6.可变部分选项和IP的可变部分一样

八.ACK机制----确认应答机制

1.功能

保证发送接收数据准确可靠

看下图进行理解

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第4张图片

这里是先从客户端向服务端发送数据包seq(序号)是1~100,

然后服务端把TCP头中的ACK置为1,ack赋值为101,然后发送到客户端,标识101编号之前的数据包都已经收到了

之后客户端收到之后继续发送数据包seq(序号)是101~200,

最后服务端把TCP头中的ACK置为1,ack赋值为201,然后发送到客户端,标识201编号之前的数据包都已经收到了

2.超时重传机制

看下图进行理解

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第5张图片

1.分析左边的(客户端发到服务端的时候数据包丢了的情况)

这里是先从客户端向服务端发送数据包seq(序号)是1~100的

发送完之后会有一个定时器,开始倒计时,如果在一定的时间内客户端没有收到ack,那么就代表数据包丢了,

那么客户端就再重新发一次数据包seq(序号)是1~100的到服务端

2.分析右边的(服务端返回客户端的时候数据包丢了的情况)

这里是先从客户端向服务端发送数据包seq(序号)是1~100的,

发送完之后会有一个定时器,开始倒计时,如果在一定的时间内客户端没有收到ack,那么就代表数据包丢了,

那么客户端就再重新发一次数据包seq(序号)是1~100的到服务端,服务端如果已经有了,那么就丢弃这个包(重复的包进行丢弃),然后服务端回一个对应序号的ack

九.TCP传输控制协议

1.三次握手(进行连接)

看下图进行理解

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第6张图片

1> 一开始,客户端和服务端都处于CLOSE状态。先是服务端主动监听某个窗口,处于LISTEN状态

2> 然后客户端主动发起连接SYN,之后处于SYN-SEND状态

3> 服务端收到发起的连接,返回SYN,并且ACK客户端的SYN,之后处于SYN-RCVD状态

4> 客户端收到服务端发送的SYN和ACK之后,发送ACK的ACK,之后处于ESTABLISHED状态,因为它一发一收成功了

4> 服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它也一发一收了

关于握手的问题:为什么时三次握手,不是两次或者是四次
为什么两次不行:

因为每次收到一个包如果有除ACK标志位的功能之外还有别的功能那么就要都要给对端回相应的ACK(只有ACK的包不需要回ACK)

建立连接时,客户端向服务端发一个SYN,这时服务端要回一个SYN和ACK,然后因为服务端回了一个SYN,那客户端就必须再回一个ACK,所以不能是两次

为什么四次不行:

建立连接时,客户端向服务端发一个SYN,这时服务端要回一个SYN和ACK,这两个功能可以在一个数据包中实现,然后客户端再回一个ACK,所以不是四次

2.四次挥手(断开连接)

1.注意:因为断开连接,可以是服务端发出的,也可以是客户端发出的,所以这里是主动方和被动方的关系
2.看下图进行理解

计网----阻塞和非阻塞,虚拟内存,UDP的特点,以太网帧结构,IP协议格式,写一个C/S模型的数据通信(客户端和服务端的协议用的是TCP协议),TCP协议头,ACK机制----确认应答机制等_第7张图片

1> 主动方打算关闭连接,此时会发送一个TCP首部FIN标志位被置为1的报文,即FIN报文,之后主动方进入FIN_WAIT_1 状态

2> 被动方收到该报文后,就向主动方发送ACK应答报文,接着被动方发送ACK应答报文,接着被动方进入CLOSED_WAIT状态

3> 主动方收到被动方的ACK应答报文后,之后进入FIN_WAIT_2状态

4> 等待被动方处理完数据后,也向主动方发送FIN报文,之后被动方进入LAST_WAIT状态。

5> 主动方收到被动方的FIN报文后,回一个ACK应答报文,之后进入TIME_WAIT状态

6> 被动方收到了ACK应答报文后,就进入了CLOSED状态,至此被动方已经完成连接的关闭

7> 主动方经过2MSL一段时间后,自动进入CLOSED状态,至此主动方也完成连接的关闭

3.提示:2MSL就是两倍的报文段最大存活时间
4.关于四次挥手的的第一个问题:为什么要等待2MSL

是为了保证最后一个ACK被动方能够收到

因为如果ACK丢失了,那么主动方就会在2MSL中再次接收到FIN报文

如果ACK没有丢失,那么主动方就不会在2MSL中再次接收到FIN报文

5.关于四次挥手的的第二个问题:被动方收到一个FIN报文后要立刻回一个ACK,为什么不立刻回复一个FIN

1> 立刻回一个ACK是为了防止超时重传

2> 不立刻回复一个FIN是因为收到FIN报文之后之前的数据可能还没有处理完,在处理的过程中还要发消息,所以我们要等之前数据处理完之后再发FIN报文

十.RTT和RTO

1.RTT(Round-Trip Time 往返时延)

1.什么是RTT

在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。

2.RTT的决定因素

RTT由三个部分决定:即链路的传播时间、末端系统的处理时间以及路由器的缓存中的排队和处理时间。

注意:路由器的缓存中的排队和处理时间对RTT的影响最大

1.RTO(Retransmission Timeout 超时重传时间)

1.什么是RTO

TCP每发送一个报文段,就对此报文段设置一个超时重传计时器。此计时器设置的超时重传时间RTO应当略大于TCP报文段的平均往返时延RTT,一般可取RTO=2RTT

但是,也可以根据具体情况认为调整RTO的值,例如可以设置此超时重传时间RTO=90秒。当超过了规定的超时重传时间还未收到对此报文段的预期确认消息,则必须重新传输此TCP报文段

你可能感兴趣的:(计算机网络,tcp/ip,计算机网络)