后台核心技术开发与应用实践读书笔记(六)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40028201/article/details/89513400

后台核心技术开发与应用实践读书笔记(六)

第6章 TCP协议

6.1 TCP协议

  1. 网络模型

    七层(IOS) 五层 四层(TCP/IP)
    应用层
    表示层
    会话层 应用层 应用层
    传输层 运输层 传输层
    网络层 网络层 网间层
    数据链路层 数据链路层 网络接口(链路层)
    应用层 物理层

    ​ 当然讨论最多的是TCP/IP模型

    在这里插入图片描述

  2. TCP头部

    在这里插入图片描述

    1. 16 位端口号( port number ):告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。
    2. 32 位序号( sequence number ):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。
    3. 32 位确认号(acknowledgement number ):用作对另一方发送来的 TCP 报文段的响应。其值是收到的TCP报文段的序号值加1。
    4. 4位头部长度(header length ):标识该TCP头部有多少个 32bit ( 4 Byte )。
    5. 6位标志位
      • URG:紧急指针是否有效
      • ACK: 确认号是否有效——>确认报文段
      • PSH: 提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据
      • RST: 要求对方重新建立连接——>复位报文段
      • SYN: 请求建立一个连接——>同步报文段
      • FIN: 通知对方本端要关闭——>结束报文段
    6. 16位窗口大小:滑动窗口流量控制,告诉对方本段的接收缓冲区还有多少
    7. 16位校验和: 由发送端填充,接收端对其进行CRC算法,建议TCP报文段在传输过程中是否损坏。
    8. 16位紧急指针:正的偏移量。它的序号字段的值相加表示最后一个紧急数据的下一个字节的序号
  3. TCP状态流转

    1. 三次握手与四次挥手

      在这里插入图片描述

      建立连接的过程:

      第一次握手:客户端发送SYN包到服务器,进入到SYN_SEND状态,等待对确认
      第二次握手:服务器收到SYN包,确认客户的包以及发送一个自己的SYN包(SYN+ACK),此时进入SYN_RECV状态
      第三次握手:客户端收到服务器发来的包,发送确认ACK包。于是双方进入到了ESTABLISH状态

      结束连接的过程:
      ​ 由于TCP的链接是全双工的,必须双方都要断开连接(客户端发送FIN给对方后服务端发送一个ACK给客户端(此时处于半关闭),同理断开服务端到客户端的连接反之),这个就是四次挥手。

    2. TCP状态流转图

      在这里插入图片描述

      在这里插入图片描述

  4. TCP超时重传

    重传超时时间RTO设置,时间设置过长,重发就慢,效率低;过短会发生网络拥塞,导致更多超时。1s、2s、4s、8s、16s符合Karm算法

  5. TCP滑动窗口

    有两个作用:

    1. 提供TCP的可靠性
    2. 提供TCP的流控特性
  6. TCP拥塞控制

    ​ 拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载 拥塞控制是 个全局性的过程,和流量控制不同,流量控制指点对点通信量的控制

    四个核心算法:

    1. 慢开始:由小到大逐渐增加拥塞窗口大小
    2. 拥塞避免:让窗口缓慢增长,经过往返时间,窗口加1
    3. 快速重传:要求对方收到失序的报文段后发出重复确认(发送方收到3个就重传),注意与超时重传的区别
    4. 快速恢复

6.2 TCP网络编程API

​ Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。

在这里插入图片描述

6.3 实现一个TCPserver

参考

​ 服务端:

#include <WinSock2.h>
#include <stdio.h>
#include <stdlib.h>
 
#pragma comment(lib, "ws2_32.lib")
 
void main()
{
	WSADATA wsaData;
	int port = 5099;
 
	char buf[] = "Server: hello, I am a server....."; 
 
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		printf("Failed to load Winsock");
		return;
	}
 
	//创建用于监听的套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
 
	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(port); //1024以上的端口号
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 
	int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
	if(retVal == SOCKET_ERROR){
		printf("Failed bind:%d\n", WSAGetLastError());
		return;
	}
 
	if(listen(sockSrv,10) ==SOCKET_ERROR){
		printf("Listen failed:%d", WSAGetLastError());
		return;
	}
 
	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);
 
	while(1)
	{
		//等待客户请求到来	
		SOCKET sockConn = accept(sockSrv, (SOCKADDR *) &addrClient, &len);
		if(sockConn == SOCKET_ERROR){
			printf("Accept failed:%d", WSAGetLastError());
			break;
		}
 
		printf("Accept client IP:[%s]\n", inet_ntoa(addrClient.sin_addr));
 
		//发送数据
		int iSend = send(sockConn, buf, sizeof(buf) , 0);
		if(iSend == SOCKET_ERROR){
			printf("send failed");
			break;
		}
 
 		char recvBuf[100];
 		memset(recvBuf, 0, sizeof(recvBuf));
// 		//接收数据
 		recv(sockConn, recvBuf, sizeof(recvBuf), 0);
 		printf("%s\n", recvBuf);
 
		closesocket(sockConn);
	}
 
	closesocket(sockSrv);
	WSACleanup();
	system("pause");
}

​ 客户端:

#include <WinSock2.h>
#include <stdio.h>
 
#pragma comment(lib, "ws2_32.lib")
 
void main()
{
	//加载套接字
	WSADATA wsaData;
	char buff[1024];
	memset(buff, 0, sizeof(buff));
 
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		printf("Failed to load Winsock");
		return;
	}
 
	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(5099);
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
 
	//创建套接字
	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
	if(SOCKET_ERROR == sockClient){
		printf("Socket() error:%d", WSAGetLastError());
		return;
	}
 
	//向服务器发出连接请求
	if(connect(sockClient, (struct  sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET){
		printf("Connect failed:%d", WSAGetLastError());
		return;
	}else
	{
		//接收数据
		recv(sockClient, buff, sizeof(buff), 0);
		printf("%s\n", buff);
	}
 
	//发送数据
	char buff = "hello, this is a Client....";
	send(sockClient, buff, sizeof(buff), 0);
 
	//关闭套接字
	closesocket(sockClient);
	WSACleanup();
}

6.5 TCP协议选项

​ 具体见课本

6.6 网络字节序与主机序

(1)字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
(2)主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。常见的有两种:a) Little-Endian就是低位字节排放在内存的低地址端;b) Big-Endian就是高位字节排放在内存的低地址端。
(3)网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
(4)所以,当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成为网络字节序后在进行传输。

6.7 封包与解包

(1)TCP/IP 网络数据以流的方式传输,数据流是由包组成,如何判定接收方收到的包是否是一个完整的包就要在发送时对包进行处理,这就是封包技术。封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了
(2)包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包
(3)利用底层的缓冲区来进行拆包时,由于TCP也维护了一个缓冲区,所以可以利用TCP的缓冲区来缓存发送的数据,这样一来就不需要为每一个连接分配一个缓冲区了.对于利用缓存区来拆包,就是循环不停地接收包头给出的数据,直到收够为止,这就是一个完整的TCP包。

参考博客

1.C++基于TCP/IP简单的客户端、服务器通信程序实例
2.《后台开发核心技术与应用实践》(三)

猜你喜欢

转载自blog.csdn.net/qq_40028201/article/details/89513400