一、网络协议
1.1 ISO/OSI 7层模型
应用层:所有的应用程序
协议 | 名称 | 端口号 |
HTTP | 超文本传输协议 | 80 |
HTTPS | 比http多了ssl加密算法 | 443 |
FTP | 文件传输协议 | 21 |
DNS | 域名解析协议 | 53 |
DHCP | 动态主机配置协议 | 68 |
Telnet | 23 | |
Smtp | 简单邮件传输协议 | 25 |
SSH | 安全外壳协议 | 22 |
表示层:数据加解密 数据解压缩 图片/视频编译码
会话层:session会话管理 服务器验证用户登录 断点续传
传输层:
TCP 面向连接的、可靠的(文件传输)、基于字节流的传输层通信协议
UDP 无连接的、高效率(实时性)、低可靠性的数据报传输服务
线程
端口(与应用程序的关系是一对多) ip地址保证到达设备 端口号保证到达哪个程序
socket 套接字,支持TCP/IP协议的网络通信的基本操作单元。包含进行网路通信的必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远地主机的IP地址、远地进程的协议端口
网络层(IP数据报):防火墙 IP地址 路由器 ARP RARP
数据链路层(数据帧):网卡 交换机 MAC地址
物理层(01比特流):01信号转换成光信号/物理信号
1.2 TCP/IP 4层模型
二、UDP
2.1 C/S B/S
Client/Server 客户端/服务器架构
1.每个应用程序都有自己单独的客户端,互不影响
2.任意协议通信(一般tcp udp)
Browser/Server 浏览器/服务器架构
1.客户端不固定
2.协议固定(http https)
2.2 UDP服务端
2.1.1 加载库
// 1、加载库
WORD mVersionRequested = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(mVersionRequested, &data);
if (err != 0) {
cout << "加载失败" << endl;
return 1;
}
//加载库成功,检查版本号
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "加载库版本错误" << endl;
//卸载库,错误的库也要卸载
WSACleanup();
return 1;
}
else {
cout << "加载库成功" << endl;
}
2.1.2 创建套接字
2.1.3 绑定ip和端口号
// 3、绑定ip和端口号
sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
//大端小端存储,网络字节序——大端,htons:转换成网络字节序
addrServer.sin_port = htons(12345);
/*
IP分为两种数据类型:“192.169.3.123”——十进制四等分字符串类型
ulong类型
inet_addr:字符串转ulong
inet_ntoa:ulong转字符串
*/
addrServer.sin_addr.S_un.S_addr = INADDR_ANY; //绑定所有网卡
err = bind(sock, (sockaddr*)&addrServer, sizeof(addrServer);
if (err == SOCKET_ERROR) {
cout << "绑定失败:" << WSAGetLastError << endl;
WSACleanup();
return 1;
}
else {
cout << "绑定成功" << endl;
}
2.1.4 接收数据 发送数据
int nRecvNum = 0;
int nSendNum = 0;
char RecvBuf[1024] = "";
char SendBuf[1024] = "";
sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
while (1) {
// 4、接收数据
//阻塞函数,原因是socket是阻塞的,所以发送和接收都是阻塞的
nRecvNum = recvfrom(sock, RecvBuf, sizeof(RecvBuf), 0, (sockaddr*)&addrClient, &addrClientSize);
if (nRecvNum > 0) {
//接收数据成功,打印接收到的数据
cout << inet_ntoa(addrClient.sin_addr) << "recv:" << RecvBuf << endl;
}
else if (nRecvNum == 0) {
cout << "连接断开" << endl;
break;
}
else {
cout << "接收数据失败:" << WSAGetLastError() << endl;
break;
}
// 5、发送数据
gets_s(SendBuf);
nSendNum = sendto(sock, SendBuf, sizeof(SendBuf), 0, (sockaddr*)&addrClient, addrClientSize);
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}
}
2.1.5 关闭套接字 卸载库
closesocket(sock);
WSACleanup();
2.1.6 UDP服务端完整代码
//UDP服务端完整代码
#include <iostream>
#include <WinSock2.h>
using namespace std;
//导入依赖库
#pragma comment(lib,"Ws2_32.lib")
int main() {
// 1、加载库
WORD mVersionRequested = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(mVersionRequested, &data);
if (err != 0) {
cout << "加载失败" << endl;
return 1;
}
//加载库成功,检查版本号
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "加载库版本错误" << endl;
//卸载库,错误的库也要卸载
WSACleanup();
return 1;
}
else {
cout << "加载库成功" << endl;
}
// 2、创建套接字
SOCKET sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if (sock==INVALID_SOCKET) {
cout << "创建套接字失败:"<<WSAGetLastError() << endl;
WSACleanup();
return 1;
}
else {
cout << "创建套接字成功" << endl;
}
// 3、绑定ip和端口号
sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
//大端小端存储,网络字节序——大端,htons:转换成网络字节序
addrServer.sin_port = htons(12345);
/*
IP分为两种数据类型:“192.169.3.123”——十进制四等分字符串类型
ulong类型
inet_addr:字符串转ulong
inet_ntoa:ulong转字符串
*/
addrServer.sin_addr.S_un.S_addr = INADDR_ANY; //绑定所有网卡
err = bind(sock, (sockaddr*)&addrServer, sizeof(addrServer));
if (err == SOCKET_ERROR) {
cout << "绑定失败:" << WSAGetLastError() << endl;
//关闭套接字
closesocket(sock);
WSACleanup();
return 1;
}
else {
cout << "绑定成功" << endl;
}
//把socket设置为非阻塞的
//u_long iMode = 1;
//ioctlsocket(sock, FIONBIO, &iMode);
//缓冲区
//获取发送缓冲区和接收缓冲区大小
//int recvSize = 0;
//int sendSize = 0;
//int size = sizeof(recvSize);
//getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recvSize, &size);
//getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sendSize, &size);
//cout << "SO_RCVBUF=" << recvSize << ",SO_SNDBUF=" << sendSize << endl; //65536
int nRecvNum = 0;
int nSendNum = 0;
char RecvBuf[1024] = "";
char SendBuf[1024] = "";
sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
while (1) {
// 4、接收数据
//阻塞函数,原因是socket是阻塞的,所以发送和接收都是阻塞的
nRecvNum = recvfrom(sock, RecvBuf, sizeof(RecvBuf), 0, (sockaddr*)&addrClient, &addrClientSize);
if (nRecvNum > 0) {
//接收数据成功,打印接收到的数据
cout << inet_ntoa(addrClient.sin_addr) << "recv:" << RecvBuf << endl;
}
else if (nRecvNum == 0) {
cout << "连接断开" << endl;
break;
}
else {
cout << "接收数据失败:" << WSAGetLastError() << endl;
break;
}
// 5、发送数据
gets_s(SendBuf);
nSendNum = sendto(sock, SendBuf, sizeof(SendBuf), 0, (sockaddr*)&addrClient, addrClientSize);
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}
}
// 6、关闭套接字
closesocket(sock);
// 7、卸载库
WSACleanup();
return 0;
}
2.3 UDP客户端
//UDP客户端完整代码
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
int main() {
//1、加载库
WORD mVersion = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(mVersion, &data);
if (err != 0) {
cout << "加载失败" << endl;
return 1;
}
if (LOBYTE(data.wVersion )!= 2 || HIBYTE(data.wVersion)!= 2) {
cout << "库版本错误" << endl;
WSACleanup();
return 1;
}
else {
cout << "加载库成功" << endl;
}
//2、创建套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
else {
cout << "创建套接字成功" << endl;
}
int nRecvNum = 0;
int nSendNum = 0;
char sendBuf[1024] = "";
char recvBuf[1024] = "";
sockaddr_in addServer;
addServer.sin_family = AF_INET;
addServer.sin_port = htons(12345);
addServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //本地环回地址
//直接广播:ip地址是直接的广播地址即可发送
//addServer.sin_addr.S_un.S_addr = inet_addr("192.168.134.255");
//有限广播:需要申请广播权限
//addServer.sin_addr.S_un.S_addr = inet_addr("255.255.255.255");
//
//bool b = true;
//setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char*)&b, sizeof(b));
while (1) {
//3、发送数据
gets_s(sendBuf);
nSendNum = sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&addServer, sizeof(addServer));
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}
//4、接收数据
nRecvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, nullptr, nullptr);
if (nRecvNum > 0) {
//打印接收到的数据内容
cout << "server:" << recvBuf << endl;
}
else if (nRecvNum == 0) {
cout << "连接断开" << endl;
break;
}
else
{
cout << "接收数据错误:" << WSAGetLastError() << endl;
break;
}
}
//5、关闭套接字、卸载库
closesocket(sock);
WSACleanup();
return 0;
}
三、数据传输
3.1 数据包
3.2 单播、组播、广播
广播:路由器范围内的所有设备
组播:关心你是谁,不关心你在哪
3.3 ARP
TCP/IP协议是一个协议族
缓存有老化机制,因为存储空间是有限的。每条数据后都有一个时间戳,如果某数据的时间戳一直没有更新,就会把这个长时间未使用的数据删除
阻塞(blocking)、非阻塞(non-blocking):可以简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了(进程或线程就阻塞在那了,不能做其它事情,等待的事情发生会第一时间收到),否则就可以理解为非阻塞(在等待的过程中可以做其它事情,需要轮询是否完成,事情发生的时候不一定立刻知道)。
socket默认是阻塞的,所以接收和发送都是阻塞的
//把socket设置为非阻塞的
u_long iMode = 1;
ioctlsocket(sock, FIONBIO, &iMode);
缓存
//缓冲区
//获取发送缓冲区和接收缓冲区大小
int recvSize = 0;
int sendSize = 0;
int size = sizeof(recvSize);
getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recvSize, &size);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sendSize, &size);
cout << "SO_RCVBUF=" << recvSize << ",SO_SNDBUF=" << sendSize << endl; //65536
发送函数的阻塞和非阻塞
发送阻塞:当发送缓冲区空间不够大的时候,等到发送缓冲区空间足够大的时候再往里拷贝数据
发送非阻塞:当发送缓冲区空间不够大的时候,有多少空间拷贝多少数据,剩下的数据自己处理
四、子网划分
本地(有限)广播: 地址就是255.255.255.255,它不经路由转发,发送到本网络下的所有主机,只能在局域网内转发,数据包中不含自己的ip地址。作用:通常在计算机启动时,希望从网络IP地址服务器DHCP处获得一个IP地址(目的ip地址为全1,源ip地址为0)。
直接广播: 计算方法通过主机的掩码与网络地址计算出来。掩码最后为0的位为主机位。掩码与网络地址相与,然后主机位全变为1,就是直接广播地址。这样该网络地址下的所有主机都能接收到广播,数据包包含自己的ip地址。作用:当主机需要寻找网络中的邻居时,发出直接广播包,(目的ip主机地址为全1,源ip地址为自身ip)。
//直接广播:ip地址是直接的广播地址即可发送
//addServer.sin_addr.S_un.S_addr = inet_addr("192.168.134.255");
//有限广播:需要申请广播权限
addServer.sin_addr.S_un.S_addr = inet_addr("255.255.255.255");
bool b = true;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char*)&b, sizeof(b));
非默认只会比默认的大,用来划分子网
答案:
1、 210. 33. 5.01000100
255.255.255.10000000
按位与&得网络地址
210.33.5.0
2、子网掩码为11,A类网络默认为8
2^3 = 8个子网
步长 = 256 - 224 = 32
所以,子网号为20.0.0.0、20.32.0.0、20.64.0.0 ~ 20.224.0.0
3、2^n >= 5 所以 n = 3
24 + 3 = 27位网络号,子网掩码:255.255.255.224
每个子网可容纳2^5 - 2 = 30台主机
4、ip地址为10.100.23.238,所以为A类网络,子网掩码默认为255.0.0.0
该网络子网掩码为:255.255.11111000.0
所以,可以划分2^(8+5) = 2^13 = 8192个子网
每个子网都有2^(8+3) - 2 = 2046个主机
步长=256-248=8
所以子网为:
(1)10.0.0.0~10.0.7.255
(2)10.0.8.0~10.0.15.255
(3)10.0.16.0~10.0.23.255
......
(8190)10.255.232.0~10.255.237.255
(8191)10.255.240.0~10.255.247.255
(8192)10.255.248.0~10.255.255.255
五、TCP
5.1 TCP服务端
5.1.1 监听
//4、监听
err = listen(sock, 10);
if (err == SOCKET_ERROR) {
cout << "监听失败:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else {
cout << "监听成功" << endl;
}
5.1.2 接收连接
int nRecvNum = 0;
int nSendNum = 0;
char recvBuf[1024] = "";
char sendBuf[1024] = "";
sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
while (1) {
//5、接受连接
//每个连接成功的客户端,都会产生一个新的socket,用于和当前客户端进行数据收发
SOCKET sockTalk = accept(sock, (sockaddr*)&addrClient, &addrClientSize);
if (sockTalk==INVALID_SOCKET) {
cout << "连接失败:" << WSAGetLastError() << endl;
break;
}
else {
cout << "["<< inet_ntoa(addrClient.sin_addr) <<"]" << "连接成功" << endl;
}
while (1) { //开始通信
}
//回收通信使用的套接字
closesocket(sockTalk);
}
5.1.3 数据接收、数据发送
while (1) { //开始通信
//6、接收数据
nRecvNum = recv(sockTalk, recvBuf, sizeof(recvBuf), 0);
if (nRecvNum > 0) {
//接收数据成功
cout <<"client:" << recvBuf << endl;
}
else if (nRecvNum == 0) {
//连接断开
cout << "连接断开" << endl;
}
else {
//接收数据失败
cout << "数据接收失败:" << WSAGetLastError() << endl;
}
//7、发送数据
gets_s(sendBuf);
nSendNum = send(sockTalk, sendBuf, sizeof(sendBuf), 0);
if (nSendNum == SOCKET_ERROR) {
cout << "发送失败:" << WSAGetLastError() << endl;
break;
}
}
//回收通信使用的套接字
closesocket(sockTalk);
5.1.4 TCP服务端完整代码
//TCP服务器端完整代码
#include <iostream>
#include <Winsock2.h>
#pragma comment (lib,"Ws2_32.lib")
using namespace std;
int main() {
//1、加载库
WORD versions = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(versions, &data);
if (err != 0) {
cout << "加载库失败" << endl;
return 1;
}
//判断库版本
if (HIBYTE(data.wVersion) != 2 || LOBYTE(data.wVersion) != 2) {
cout << "版本号错误" << endl;
WSACleanup();
return 1;
}
else {
cout << "加载库成功" << endl;
}
//2、创建套接字
SOCKET sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
else {
cout << "创建套接字成功" << endl;
}
//3、绑定ip和端口号
sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(67890);
addrServer.sin_addr.S_un.S_addr = INADDR_ANY;
err = bind(sock, (sockaddr*)&addrServer, sizeof(addrServer));
if (err == SOCKET_ERROR) {
cout << "绑定失败:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else {
cout << "绑定成功" << endl;
}
//4、监听
err = listen(sock, 10);
if (err == SOCKET_ERROR) {
cout << "监听失败:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else {
cout << "监听成功" << endl;
}
int nRecvNum = 0;
int nSendNum = 0;
char recvBuf[1024] = "";
char sendBuf[1024] = "";
sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
while (1) {
//5、接受连接
//每个连接成功的客户端,都会产生一个新的socket,用于和当前客户端进行数据收发
SOCKET sockTalk = accept(sock, (sockaddr*)&addrClient, &addrClientSize);
if (sockTalk==INVALID_SOCKET) {
cout << "连接失败:" << WSAGetLastError() << endl;
break;
}
else {
cout << "["<< inet_ntoa(addrClient.sin_addr) <<"]" << "连接成功" << endl;
}
while (1) { //开始通信
//6、接收数据
nRecvNum = recv(sockTalk, recvBuf, sizeof(recvBuf), 0);
if (nRecvNum > 0) {
//接收数据成功
cout <<"client:" << recvBuf << endl;
}
else if (nRecvNum == 0) {
//连接断开
cout << "连接断开" << endl;
}
else {
//接收数据失败
cout << "数据接收失败:" << WSAGetLastError() << endl;
}
//7、发送数据
gets_s(sendBuf);
nSendNum = send(sockTalk, sendBuf, sizeof(sendBuf), 0);
if (nSendNum == SOCKET_ERROR) {
cout << "发送失败:" << WSAGetLastError() << endl;
break;
}
}
//回收通信使用的套接字
closesocket(sockTalk);
}
//9、关闭套接字、卸载库
closesocket(sock);
WSACleanup();
}
5.2 TCP客户端
5.2.1 连接
//3、连接
sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(67890);
addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.1.18");
err = connect(sock, (sockaddr*)&addrServer, sizeof(addrServer));
if (err == SOCKET_ERROR) {
cout << "连接错误:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else {
cout << "连接成功" << endl;
}
5.2.2 TCP客户端完整代码
//TCP客户端完整代码
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
int main() {
//1、加载库
WORD versions = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(versions, &data);
if (err != 0) {
cout << "加载失败" << endl;
return 1;
}
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "库版本错误" << endl;
WSACleanup();
return 1;
}
else {
cout << "加载库成功" << endl;
}
//2、创建套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
else {
cout << "创建套接字成功" << endl;
}
//3、连接
sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(67890);
addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.1.18");
err = connect(sock, (sockaddr*)&addrServer, sizeof(addrServer));
if (err == SOCKET_ERROR) {
cout << "连接错误:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else {
cout << "连接成功" << endl;
}
int nRecvNum = 0;
int nSendNum = 0;
char recvBuf[1024] = "";
char sendBuf[1024] = "";
while (1) {
//4、发送数据
gets_s(sendBuf);
nSendNum = send(sock, sendBuf, sizeof(sendBuf), 0);
if (nSendNum == SOCKET_ERROR) {
cout << "发送失败:" << WSAGetLastError() << endl;
break;
}
//5、接收数据
nRecvNum = recv(sock, recvBuf, sizeof(recvBuf), 0);
if (nRecvNum > 0) {
//接收成功
cout << "server:" << recvBuf << endl;
}
else if (nRecvNum == 0) {
cout << "连接断开" << endl;
}
else {
cout << "数据接收失败:" << WSAGetLastError() << endl;
}
}
//6、关闭套接字、卸载库
closesocket(sock);
WSACleanup();
}
5.3 TCP协议
5.3.1 TCP头
5.3.2 ACK确认应答
5.3.3 超时重传
5.3.3 三次握手、四次挥手
5.3.3.1 三次握手
为什么不需要4次?
正常情况下,收到请求包需要回复一个ACK,同时想要和对方建立连接需要发一个SYN,ACK和SYN能合在一个包里发送。
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。
为什么两次不行?
1.TCP为应用层提供全双工服务,所以要确认接收和发送都能成功
2.防止已失效的连接请求又传回服务端
5.3.3.2 四次挥手
收到FIN不立刻回FIN先回ACK:防止超时重传
ACK和FIN不合一起发:在收到FIN后还可能给主动方发送有效数据(ack不算有效数据),要等待处理结果发完后再回FIN
客户半关闭
2MSL目的:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIMEWAIT状态停留的时间为2倍的MSL。这样可以确保对方收到ACK了,若没有收到还可以让TCP再次发送最后的ACK以防这个ACK丢失。
5.3.4 RTT
【第21章TCP的超时与重传】
最初的TCP规范使TCP使用低通过滤器来更新一个被平滑的RTT估计器(记为O),用M表示所测量到的RTT。
R← αR+ ( 1-α )M
5.3.5 RTO
Karn算法
RFC 793推荐的重传超时时间RTO的值应该设置为RTO = 2R
在往返时间变化起伏很大时,基于均值和方差来计算RTO,将比作为均值的常数倍数来计算RTO能提供更好的响应。
均值偏差是对标准偏差的一种好的逼近,但却更容易进行计算。这就引出了下面用于每个RTT测量M的公式。
Err = M-A
A←A + gErr=A+g(M-A)=(1-g)A+gM
D←D + h( | E rr |-D) =D+h|Err|-hD =(1-h)D+h|Err|
RTO = A + 4D
5.3.6 累积应答
5.3.7 窗口
5.3.8 流量控制
5.3.9 粘包问题
1、数据易与标识符重复;一个数据包的时候,起始标志位难以判断何时结束
2、容易造成空间浪费
4、频繁连接开销大,性能低下
5.3.10 心跳机制
5.3.11 Nagle
Nagle算法的主要目的是为了预防小分组的产生,因为在广域网中,小分组会造成 网络拥塞 。 当网络中存在大量小分组时,网络拥塞出现的可能性会增加,因为每个小分组都需要占据 网络带宽 和 路由器缓存空间 。
5.3.12 拥塞控制
5.3.12.1 慢启动与拥塞避免
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh。这样得到的算法的工作过程如下:
1) 对一个给定的连接,初始化cwnd为1个报文段, ssthresh为65535个字节。
2) TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
3) 当拥塞发生时,ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
4) 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。
慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止,然后转为执行拥塞避免。
慢启动算法初始设置cwnd为1个报文段,此后每收到一个确认就加1。正如20.6节描述的那样,这会使窗口按指数方式增长:发送1个报文段,然后是2个,接着是4个⋯⋯。
拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加比起来,这是一种加性增长。我们希望在一个往返时间内最多为cwnd增加1个报文段。
在该图中,假定当cwnd为32个报文段时就会发生拥塞。于是设置ssthresh为16个报文段,而cwnd为1个报文段。在时刻0发送了一个报文段,并假定在时刻1接收到它的ACK,此时cwnd增加为2。接着发送了2个报文段,并假定在时刻2接收到它们的ACK,于是cwnd增加为4。这种指数增加算法一直进行到在时刻3和4之间收到8个ACK后cwnd等于ssthresh时才停止,从该时刻起, cwnd以线性方式增加,在每个往返时间内最多增加1个报文段。
5.3.12.2 快速重传
如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。
这个算法通常按如下过程进行实现:
1) 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
2) 每次收到另一个重复的ACK时, cwnd增加1个报文段大小并发送1个分组。
3) 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
图21-9
当SYN的超时发生时, ssthresh被置为其最小取值(512字节,在本例中表示2个报文段)。
为进入慢启动阶段, cwnd被置为1个报文段(256字节,与当前值一致)。当收到SYN和ACK时,没有对这两个变量做任何修改,因为新的数据还没有被确认。当ACK 257到达时,因为cwnd小于等于ssthresh,因此仍然处于慢启动阶段,于是将cwnd增加256字节。当收到ACK 513时,进行同样的处理。
当ACK 769到达时,我们不再处于慢启动状态,而是进入了拥塞避免状态。新的cwnd值按以下方法计算:
考虑到cwnd实际上以字节而非以报文段来维护,因此这就是我们前面提到的增加1/cwnd。
在这个例子中我们计算
为885字节。
当下一个ACK 1025到达时,我们计算:
为991字节。
5.3.12.3 快速恢复
图21-10
本图中cwnd的前6个值就是我们为图21-9所计算的数值。在这个图中,要想直观分辨出在慢启动过程中的指数增加和在拥塞避免过程中的线性增加之间的区别是不可能的,因为慢启动的过程太快。
我们需要解释在重传的3个点上所发生的情况。回想起每个重传都是因为收到3个重复的ACK,表明1个分组丢失了。这就是21.7节的快速重传算法。ssthresh立即设置为当重传发生时正在起作用的窗口大小的一半,但是在接收到重复ACK的过程中cwnd允许保持增加,这是因为每个重复的ACK表示1个报文段已离开了网络。这就是快速恢复算法。
cwnd的值一直持续增加,从图21-9中对应于报文段12的最终取值(1089)到图21-1中对应于报文段58的第一个取值(2426),而ssthresh的值则保持不变(512),这是因为在此过程中没有出现过重传。
当最初的2个重复的ACK(报文段60和61)到达时它们被计数,而cwnd保持不变。然而,当第3个重复的ACK到达时,ssthresh被置为cwnd的一半,而cwnd被置为ssthresh加上所收到的重复的ACK数乘以报文段大小,然后发送重传数据。
又有5个重复的ACK到达(报文段64~66, 68和70),每次cwnd增加1个报文段长度。最后一个新的ACK(报文段72段)到达时,cwnd被置为ssthresh(1024)并进入正常的拥塞避免过程。由于cwnd小于等于ssthresh,因此报文段的大小增加到cwnd,取值为1280。
当下一个新的ACK到达时,cwnd大于ssthresh,取值为1363。
在快速重传和快速恢复阶段,我们收到报文段66、68和70中的重复的ACK后才发送新的数据,而不是在接收到报文段64和65中重复的ACK之后就发送。这是cwnd的取值与未被确认的数据大小比较的结果。当报文段65到达时, cwnd为2048,但未被确认的数据有2304字节,因此不能发送任何数据。当报文段65到达后,cwnd被置为2304,此时我们仍不能进行发送。但是当报文段66到达时, cwnd为2560,所以我们可以发送1个新的数据报文段。类似地,当报文段68到达时,cwnd等于2816,该数值大于未被确认的2560字节的数据大小,因此我们可以发送另1个新的数据报文段。报文段70到达时也进行了类似的处理。
5.4 总结
使用UDP协议,保证传输速度快的同时还要保证稳定可靠?
传输层采用UDP,应用层手动实现seq和ACK机制。发送方创建发送队列,用于存储发送数据,便于重发,同时对其标号。发送数据后启动定时器,接收方创建接收队列,回复seq和ACK;若超时没有接收到接收方ACK,则重发数据。
同理,可以在应用层实现滑动窗口、累计应答等机制以提高效率。
TCP协议包含多种机制,若进行简单通信只想使用部分机制,可以在底层用UDP协议,在上层手动实现部分机制。