阻塞:烧水,直到水烧完了,再继续接下来的事情
非阻塞:烧水,在烧水的同时,去做别的事情,每隔一段时间查看水是否烧完了
我们在前面的博客中分别写了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;
}
当发送缓冲区空间不足够的时候发送函数才会有阻塞和非阻塞
阻塞:当发送缓冲区空间不足够的时候,等待空间足够大再往发送缓冲区中拷贝数据
非阻塞:当发送缓冲区空间不足够的时候,有多少空间拷贝多少数据进去,剩余没拷贝的数据由应用程序自己决定(重发一次完整的数据或者发那部分没发送的数据)
(1)非阻塞会一直占着CPU,所以正常情况下我们都是使用阻塞模式并配合使用线程(线程是由系统自动调度的,不会一直占着CPU)来实现数据的互相传输(这里的传输是可以一方一直输出数据的,不用等对方接收之后才能再次输出)
(2)阻塞对数据反应快,非阻塞对数据反应慢
应用程序不运行的时候存在磁盘里,当运行的时候就会被调用到内存空间里,每个程序会被分配给4个G的虚拟内存(虚拟的意思:实际分配的不是4个g,假设如果最开始是500M的话,不够了会继续给你分配内存,但最多只能分配4个G)
1> 0~2G(内核空间)
大家一起共同使用的
2> 2~4G(用户空间)
每个应用程序自己的空间
当我们在应用程序里创建一个socket的时候,操作系统会在内核空间里分配两个缓冲区,一个缓冲区叫发送缓冲区,一个缓冲区叫接收缓冲区
当其他设备通过网络给这个应用发送数据,操作系统收到之后会写到接收缓冲区里,当我们调用recvfrom()这个函数时,应用程序就会把接收缓冲区里的数据拷到当前进程的变量中去
当应用程序发送数据的时候,应用程序会先把要发送的数据写到当前进程的变量中去,当我们调用sendto()这个函数时,就会把数据从当前进程的变量发送缓冲区中去,然后操作系统会通过网卡继续发送
这里我们还可以知道阻塞的好处就是反应快,如果是非阻塞无法立刻与数据做出反应
使用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)
所以当我们发送一个大文件的时候,需要分开发
当我们接收一个大文件的时候,需要分开收,全部收到之后再组合到一起
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
图中的一行表示32位,4个字节
1> 版本:用的IPV4还是IPV6,用的哪个版本的IP地址
2> 首部长度:记录IP头的首部总长度,用来取出IP头
3> 总长度:IP数据包的总长度
当数据的大小超过了MTU的最大长度1500个字节那么要进行分片(4,5,6是在分片的基础上使用的)
4> 标识:给每个数据报加一个标识,使发送的数据按照应有顺序进行处理
5> 标志:判断可不可以分片,是否是当前最后一片
6> 片偏移:用来组合分片的数据报
7> 生存时间:实际上就是一个计时器,经过一定时间(经过几个路由器)后还没有到达目的地,那么就要丢掉这个包
8> 协议:传输层使用的哪个协议
9> 首部检验和:通过一种算法把首部所有的数据算出一个数,这个就是检验和,保证整个首部在传输中数据没有被修改,如果收到了发现算出来的数和首部检验和的存的那个数不一致,那么当前包就是无效包了,丢弃
1> 可选字段:可选字段数据的添加:如果要往里可选字段里添加数据,必须是4个字节的倍数数据
可选字段中数据的传输规则:采用TVL(Type Value Length)格式的)
注意:当对端设备没有应用层时,如果要给它传数据,就需要在网络层传所以就用到了ip头中的可选字段
1.加载库(库为ws2_32.lib)
2.创建套接字(所用到的函数为socket())
3.绑定IP地址和端口号
4.监听(所用到的函数为listen())
5.接受连接(所用到的函数为accept()),注意这里的函数成功后会产生一个新的socket,我们之后要进行关闭
6.接收数据(所用到的函数为recv())
7.发送数据(所用到的函数为send())
8.关闭连接产生的套接字((所用到的函数为closesocket()))
9.关闭套接字(所用到的函数为closesocket())
10.卸载库(所用到的函数为WSACleanup())
1.加载库(库为ws2_32.lib)
2.创建套接字(所用到的函数为socket())
3.连接
4.发送数据
5.接收数据
6.关闭套接字(所用到的函数为closesocket())
7.卸载库(所用到的函数为WSACleanup())
#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;
}
看下面图进行了解
功能:保持接收方接受数据有序
功能:告诉发送方收到了这个数据(假设收到了编号为10号的数据包,那ack就会返回11,告诉发送方该发送第11号数据包了)
功能:记录TCP头总共有多长,方便在数据包中把这个TCP头截取出来,然后把用户数据传给应用层
功能:一般在协议头中都是用来做标志位的(每一个标志位后面都对应着一个功能,标志位 置1了就代表要使用这个功能,置0了就代表不使用这个功能)
注意:如果要同时使用几个功能,那么将这些功能的标志位异或起来
FIN标志位代表断开连接的功能
SYN标志位代表建立连接的功能
RST标志位代表重新建立连接的功能
PSH标志位代表快速处理此数据包的功能
ACK标志位代表使用确认号的功能(如果当前包是一个确认包就要使用该功能)
URG标志位代表的使用紧急指针的功能
功能:确保当前协议头里面的数据在传输的过程中没有被篡改
保证发送接收数据准确可靠
看下图进行理解
这里是先从客户端向服务端发送数据包seq(序号)是1~100,
然后服务端把TCP头中的ACK置为1,ack赋值为101,然后发送到客户端,标识101编号之前的数据包都已经收到了
之后客户端收到之后继续发送数据包seq(序号)是101~200,
最后服务端把TCP头中的ACK置为1,ack赋值为201,然后发送到客户端,标识201编号之前的数据包都已经收到了
看下图进行理解
这里是先从客户端向服务端发送数据包seq(序号)是1~100的
发送完之后会有一个定时器,开始倒计时,如果在一定的时间内客户端没有收到ack,那么就代表数据包丢了,
那么客户端就再重新发一次数据包seq(序号)是1~100的到服务端
这里是先从客户端向服务端发送数据包seq(序号)是1~100的,
发送完之后会有一个定时器,开始倒计时,如果在一定的时间内客户端没有收到ack,那么就代表数据包丢了,
那么客户端就再重新发一次数据包seq(序号)是1~100的到服务端,服务端如果已经有了,那么就丢弃这个包(重复的包进行丢弃),然后服务端回一个对应序号的ack
看下图进行理解
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,所以不是四次
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状态,至此主动方也完成连接的关闭
是为了保证最后一个ACK被动方能够收到
因为如果ACK丢失了,那么主动方就会在2MSL中再次接收到FIN报文
如果ACK没有丢失,那么主动方就不会在2MSL中再次接收到FIN报文
1> 立刻回一个ACK是为了防止超时重传
2> 不立刻回复一个FIN是因为收到FIN报文之后之前的数据可能还没有处理完,在处理的过程中还要发消息,所以我们要等之前数据处理完之后再发FIN报文
在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。
RTT由三个部分决定:即链路的传播时间、末端系统的处理时间以及路由器的缓存中的排队和处理时间。
注意:路由器的缓存中的排队和处理时间对RTT的影响最大
TCP每发送一个报文段,就对此报文段设置一个超时重传计时器。此计时器设置的超时重传时间RTO应当略大于TCP报文段的平均往返时延RTT,一般可取RTO=2RTT
但是,也可以根据具体情况认为调整RTO的值,例如可以设置此超时重传时间RTO=90秒。当超过了规定的超时重传时间还未收到对此报文段的预期确认消息,则必须重新传输此TCP报文段