UDP
UDP作为传输层的协议,它规定了数据什么时候发送,发送多少的问题。 交付上层(实际上是同层),是由目的端口号来完成的。 有效载荷和数据进行分离的工作是由固定报文首部长度来实现的。
数据自上而下传输的过程,首先一开始通常是我们的客户端主动连接服务器,
- 那么我们的客户端肯定是知道源端口号的
- 那么目的端口号我们从上面内容可以得知我们访问的服务器通常都是端口号是
总所周知 的, - 然后UDP的16位长度是整个报文(头部+数据),该字段的最小值为8(即只有首部);
- 16位UDP校验和是验证整个报文是否有问题,在这里处理计算报文,还加上了12字节的一个伪头部进行计算。伪头部的格式如下图。
这里的传输层的校验更多的是验证是否是发给我这个协议的。IP的校验偏重于是否有出错。
过程解析: 所以当对端UDP层获取到这个报文时,先识别8字节,然后就能够拿到这个报文的长度究竟是多少,测试报文是否有出错,然后通过16位目的端口号,得知要标识交付给上层的哪一个协议的。
UDP的一些特点:
- 报文通过目的端口号进行报文的分用,通过定长找到对应的报文长度字段进行获取有效载荷。
- UDP协议没有任何的一个填充字段或选项,代表他的内容就那么简单。
- UDP如何得知有效载荷最大为16位udp长度,即最大65536字节,也就是64KB,若一个报文超过则需要应用层进行手动分包。
- UDP没有真正意义上的发送缓冲区。但是有接受缓冲区。这代表UDP只要有数据就会无脑发送,但是能接受到的都会进行处理。
- UDP协议发送的数据不保证有序,这是因为发送不同报文可能IP路由走的路线不相同,所以可能后发送的报文走的比前面快。
- UDP支持一对一,一对多,多对一和多对多的交互通信。如果一定要涉及到全双工的话,大概理解为不仅提供全双工,甚至提供全多工服务,只是UDP是不可靠的服务而已。
- 当传输的报文超过64KB需要应用层手动分包。
- 当接收端没有空间,则会则会将报文进行丢弃。
UDP最重要的四个特点:
- 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
- 面向数据报: 不能够灵活的控制读写数据的次数和数量;
- UDP传输在对端接受是乱序还是有序的是不确认的。
UDP代码编写
实验目的:测试UDP最多能发送多少数据
结果就是可以发送65508,也就是2^16-20字节的数据。然后就能看到errno为EMSGSIZE。当然可以使用SO_SNDBUF,在setsockopt函数当中进行更改。
进行底层缓冲区大小更改的样例。
int buf_len;
socklen_t len=sizeof(buf_len);
getsockopt(listenfd,SOL_SOCKET,SO_SNDBUF,(void *)&buf_len,&len);
printf("snd 初始 :buf:%d\n",buf_len);
int setbuf=150000;
socklen_t len1=sizeof(setbuf);
setsockopt(listenfd,SOL_SOCKET,SO_SNDBUF,(void *)&setbuf,len1);
udpserver.cc
#include<iostream>
using std::cout;
using std::endl;
using std::cerr;
#include<unistd.h>
#include<string>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<cstdio>
#include<unistd.h>
#include<string.h>
#include<error.h>
#include<arpa/inet.h>
#include<cstdlib>
#define NUM 65508
int main()
{
int sock = socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr << "bind errror" << endl;
exit(1);
}
char ch = 'a';
char str[NUM];
for(int i = 0;i < NUM;++i)
{
str[i] = ch;
}
for(;;)
{
char recvbuffer[10240];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(sock,recvbuffer,sizeof(recvbuffer)-1,0,(struct sockaddr*) &peer,&len);
if(s > 0)
{
recvbuffer[s] = 0;
cout << "client send message :" << recvbuffer << endl;
}
else if(s == 0)
{
cout << "clinet quit .... " <<endl;
continue;
}
else{
cout << " recvfrom error ..." << endl;
continue;
}
s = sendto(sock,str,sizeof(str)-1,0,(struct sockaddr*)&peer,sizeof(peer));
if(s > 0)
{
str[s] = 0;
cout << str << endl;
}
else if(s == 0)
{
cout << "client quit ..." << endl;
exit(0);
}
else{
if(errno == EMSGSIZE)
{
cout << "EMSGSIZE" <<endl;
}
if(errno == ENOBUFS)
{
cout << "ENOBUFS" <<endl;
}
cout << s << " :" << strerror(s) << endl;
exit(1);
}
}
return 0;
}
udpclient.cc
#include<iostream>
using namespace std;
#include<unistd.h>
#include<string>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<error.h>
#include<arpa/inet.h>
#include<cstdlib>
int main()
{
int sock = socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(8080);
for(;;)
{
char recvbuffer[10240];
ssize_t s = read(0,recvbuffer,sizeof(recvbuffer)-1);
if(s > 0)
{
recvbuffer[s] = 0;
}
s = sendto(sock,recvbuffer,sizeof(recvbuffer)-1,0,(struct sockaddr*)&dest_addr,sizeof(dest_addr));
if(s > 0)
{
recvbuffer[s] = 0;
cout << recvbuffer << endl;
}
else if(s == 0)
{
cout << "client quit ..." << endl;
exit(0);
}
else{
cout << s << " :" << strerror(s) << endl;
exit(1);
}
cout << " client send success clinet recv start ..." << endl;
char buffer[65536];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
s = recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s > 0)
{
buffer[s] = 0;
cout << "server send message " << buffer << endl;
}
else if(s == 0)
{
cout << "server close sock..." << endl;
}
else {
cout << " error " << endl;
}
}
return 0;
}
如何理解报文结构:
- udp/tcp是在内核里面,tcp/ip协议栈,本质也是Linux内核的一部分。
- 填充报头即是位段类型对应的变量进行赋值。
- 所以报文结构本质也是用C语言的结构体来的,如下图:
struct udp_header
{
uint32_t src_port:16;
uint32_t dst_port:16;
uint32_t dup_length:16;
uint32_t dup_check:16;
}
udp的报文若超过MTU的大小,IP层会分片处理,此时由于应用层看来报文若无法拆分,就只能接受分片处理。
基于UDP的应用层协议: 理解sendto(拷贝函数): sendto直接将数据报扔给下一层协议,并不是把数据发送到网络,而是把用户数据拷贝到udp/tcp发送缓冲区。
如何理解sendto和recvfrom:
- sendto函数,由于没有发送缓冲区,所以数据不会在内核停留,立马就发送到网络当中。
- recvfrom函数,每次接受到的一定是一个个完整的报文,并且会保存在文件描述符当中的缓冲区当中。
传输层负责什么: 数据的发送的时间取决于系统的传输层协议(什么时候发,发多少,丢包了怎么办),传输层知道网络的状态,对端的接受空间等等信息。
无论是哪一层都满足下列: 上一层协议做决策,下一层决策做执行!!
注意: 父子进程可以使用两个管道,将读写设置为非阻塞实现全双工。
面向数据报的理解: 若100字节数据,发送端选择发送10个报文发送,那么对端也必须10此recvfrom来接受。
路由器的功能:
udp结束了,来看看tcp吧!!
为什么要有TCP/IP协议
在操作系统的学习阶段,我们时常需要进行IO操作,硬件的数据到另一个硬件需要协议吗?需要的!! 早在冯诺依曼体系当中,也是涉及一个硬件和硬件之间也是互相独立的,有完整的硬件单元。那么这些硬件之间需要通信是否需要协议? 需要的,本质是因为有“线”。
一个单处理器系统中的总线,大致分为三类:
(1)CPU 内部连接各寄存器及运算部件之间的总线,称为内部总线。
(2)CPU 同计算机系统的其他高速功能部件,如存储器、通道等互相连接的总线,称为系统总线
(3)中、低速 I/O 设备之间互相连接的总线,称为 I/O 总线。
结论来了:
- 在网络中的各种设备,各种电脑之间也是直接或者间接使用各种“线”进行连接的。
- 唯一的区别就是:
线更长了 ,传播的距离更远了。 - 而传输过程长就需要一些协议来保证数据能够可靠的到达。
传输过程中的可靠性问题(TCP) 链接的设备多,定位设备的问题(IP)
计算机内部也存在协议: 几乎各种设备都是有寄存器的,数据寄存器和状态寄存器,寄存器数据可以指导硬件完成写入。也就是磁盘直连有SCSI,HBA等协议。PCI都是与主板和外设的协议。所以即使是计算机内部,也存在协议。
TCP报头能够显示的特性
- TCP的长度是4位标识,其中每一位的基本单位是4字节,这就表示报头的大小最大时60字节,而最小的时候就是20字节,即0101。标准TCP报头长度20字节,最大的报头长度60字节。也就是0101~1111,这里的基本长度是以4字节衡量的。
- 即可以携带的选项最大是40字节。
TCP
TCP实现有效载荷和首部进行分离的工作,是由4位首部长度(自描述字段)实现的。 TCP实现交付给上层的工作是由16位目的端口号。
假设首部长度值为6,则说明选项有4字节,这样子就可以做到完整的读取完报头数据,然后数据的读取可以根据数据的协议做处理。
- 如UDP一般:TCP通信也是全双工的。
- http双方关系不对等,tcp双方关系是对等的
对于报文字段的理解
确认应答
当主机一往主机二发起一条消息,主机一是没法知道主机二是否收到消息了的。但是如果主机二若回应一条消息,则主机一就能知道主机二收到了消息。即只发送一条信息没有回应的时候无法得知信息有被成功接收。
但是这样子实际上最后一次发出的一个消息是没有办法确定对端是否有收到这条消息的。
结论一:什么叫做可靠性? 1.最重要的:确认应答机制。(每次都是对历史数据可靠性的保证。)
主机一是可以给主机二发起多条信息,并不是一定是发一条接受一条。 并且并不是每条报文都一定要有回应,只要对最新的报文有回应,就说明先前的报文接受到了,只不过回应丢失,但此时不影响。(不一定如下图)
TCP对比起UDP最大的特点就是保证了可靠性,那保证可靠性TCP做了什么?
- 其次重要的特性:保证数据能够按序到达
假设host1发送多个报文给host2,host2收到的报文一定是有序的吗?不一定!跟UDP一样,那TCP如何保证有效性呢? 如同下单和快递实际到货并不一定是一一对应的,这与路由的路径有关。 收到的报文若是不连续,实际上也是一种不可靠的现象
解决方案: TCP报头中具有32位序号,序号的价值可以对报文进行顺序重排。 能够保证对端TCP能够将乱序的TCP进行排序。
报文排序
如何理解确认序号? 我们知道了序号的作用,确认序号实际上会对已经接受到的序号+1,表示让对端机器下次从序号+1的位置开始发送。即假设确认序号位16,则16之前的都全部收到,下次从16后往后发。
为什么同时需要序号和确认序号? 因为通信是双方进行的,host1可以往host2发送数据,此时有32位序号,host2也会往host1发送数据,此时也需要32位序号,并且需要填写确认序号。即保证全双工需要序号和确认序号。
所以说所谓的数据发送,站在OS的角度,即从发送缓冲区通过网络拷贝到对端的接受缓冲区。
16位窗口大小的作用: 窗口大小填写的是自己的 接受缓冲区的大小,提供给对端是让对端根据我的窗口大小,考虑网络状态,然后决定发送多少数据给我,不会因为发的过多导致丢包严重等等问题。
recv,send究竟做了什么:实质是拷贝函数 TCP当中有接受缓冲区和发送缓冲区的,recv,send也就是往从数据拷贝到tcp缓冲区,以及从tcp缓冲区数据拷贝到用户层。
通过互相通报自己的接受能力给对方,填写到TCP报文中的16位窗口大小,以达到两个方向上传输速度的控制,叫做流量控制 。 流量控制
网络编程与系统编程无边界: 一个进程将数据放入缓冲区,一个进程将数据从缓冲区当中取出,这就是生产者消费者模型。
- 当缓冲区满了,可以发送报文填写窗口大小为0,不让对方生产了,
- 如果缓冲区空了,让消费者停止消费。实际上完成了同步的过程;
而生产者让消费者快点取数据可以填充PSH字段。 握手期间双方就互相告知了彼此的窗口大小。
六个字段
如何理解PSH字段: PSH字段的出现首先是因为对端的接受缓冲区在一段时间内没有被上层取走,此时有可能是数据没有到达低水位线导致的,PSH字段标识让对端不管套接字的数据有多少,都让对端的数据交付给上层。
SYN:同步标志位 SYN连接建立请求。标识该报文是一个连接建立请求报文。 SYN+ACK:表示连接建立请求报文,ACK标识该报文也有对上一个报文的确认成分。
为什么要有SYN? 即每次到达的连接的需求是不一样的,有这个标志位就知道对应是来干嘛的,才能提供对应的服务;而SYN标志位表示建立连接的请求,下图的蓝,紫,红分别代表建立连接,断开连接,链接上来了数据。所以SYN能够区分其中是否是建立连接的这一种情况。这个SYN是给内核看的,内核可以根据这个字段知道哪些是建立连接的。 listen_sock上有事件,其他套接字上的读事件和写事件。 FIN:标识断开连接标志位 双方需要都发送FIN才彻底断开连接。因为TCP是全双工的。
PSH: 发送方得知对面的接收缓冲区一直为0,发送PSH让对面的上层快点把数据接受上去。 假设对端上层处于休眠状态。 发送方可以发送报头询问是否有跟新窗口大小。 或者对方将数据取走给发送方发送窗口更新。
假如对端长时间不更新,不发送窗口更新报头,我作为发送方,就会填充一个PSH标记位,让对方的上层尽快将数据取走。
URG标记位:
- URG比较少使用,但是通常使用会搭配紧急指针使用。TCP提供了优先处理数据的能力,就是设置TCP的URG标志位(紧急数据标记位),搭配16位的紧急指针使用。
这个数据也称之为带外数据。 - URG是按序到达的,所以当如果后序的数据优先被读取并且处理,是不可能的。
比如有控制信息等等,但是有些场景需要特定数据插队,比如需要做特殊处理,所以需要用URG,它可以联合紧急指针,紧急指针指向数据的一个字节,这个字节的数据需要优先处理。 - 在send当中flags标记有一个MSG_OOB,即发送一个紧急数据出去。(带外数据)
recv当中的flags也要设置MSG_OOB才能读取。 - 读取时recv也需要携带这个字段。
RST标记(重要):
RST标志是双向的,服务器可以发送RST给客户端让客户端重新建立链接,客户端也可以发送RST给服务器表示客户端需要重新建立连接。
首先,建立连接是有成本的,需要创建对应的数据结构,建立连接并不是100%成功的。倘若三次握手当中的最后一次ACK丢失,导致host2并没有建立好连接。当服务端启动的时候发现已经有连接到来,此时操作系统不认为链接建立成功,而发送
由于三次握手的最后一次的ACK是可能接受不到,所以说可能出现host1建立了连接,但是host2并没有建立了连接。由于双方认为链接建立好是由时间差的。host1认为链接建立好就可能直接发数据,但是host2可能没有收到ACK;此时可以设置复位标志位。 此时host1发数据则host2看到连连接都没建立好就发数据,就知道上一个ACK没收到,就会给host1发送一个字段。
所以没有四次握手,偶数次握手就会等导致host2先建立连接。服务器的资源是有限的,先让服务器建立连接而双方没有连接建立成功的成本是很大的。
所以说奇数次握手(除1次外)都是可行的。 三次握手就是最小成本保证双方建立连接,同时对网络的负担最小。
序列号的理解: 序列号可以理解为内核维护一个循环队列,用数组模拟,所以每个字节都有对应的下标,下标与序列号存在某种映射关系,从接受的报文的确认序列号,我们内核就可以对下标进行加加等操作。当需要发送数据,操作系统有能力知道从哪个位置发,发多少,将数据添加报头后发送。
超时重传
发送方一段时间没收到对方的ACK,认为数据丢失,进行重传。 此时丢失有两种情况: 1.报文丢失 2.ACK丢失
两种场景不同,但是发送端都认为数据丢失了。但是后一种情况对端会收到两份相同的数据,唯一的区别就是对端可以进行去重 。
如何做到数据去重: 若对端发现数据已经接受过了,说明自己的ACK丢失了。TCP具有去重能力,报文是有序号的。这是序号提供的功能。
序号能够保证按序到达!
如何保证TCP有超时重传的机制:
- 需要超时重传实际上也就说明发送方数据发送出去也不能马上将数据删除或者覆盖。
- 需要收到对方的ACK才能将数据进行移除
如何确定超时时间? 这个与带宽有关,所以这个超时时间是动态的。
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
- TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时
- 时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
- TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间:
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍. 如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传. 如果仍然得不到应答, 等待 4500ms 进行重传. 依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制
TCP是面向连接的,那么它就有对应的连接管理机制。
Connect发起三次握手 客户端SYN_SEND的时候服务器已经调用了listen,在监听套接字的信息。 服务器就可以同发送SYN+ACK,此时进入SYN_RCVD状态。 客户端若收到立马ESTABLISHED表示站在客户端的角度,建立已经成功。connect 也就返回了。但此时服务端不一定建立完成。 而服务器收到ACK才会ESTABLISHED表示服务器也认为连接建立成功,并且accept 返回,表示服务器获取一个连接上来。
此时就可以调用send,recv,write,read来通信了。但是我们通信并不一定像上面的一样发起一条接受一条,而是可以发起多条,等待ACK即可。
所谓write,本质是把数据拷贝到发送缓冲区,由TCP决定什么时候发送。
并且双方通信结束后需要调用close关闭文件描述符。这里有一个细节文件描述符的声明周期是随进程的!!! 什么意思呢?今天客户端建立好连接后死机了,进程退出,此时文件描述符关掉了吗?关掉了!!
三次握手
客户端发送SYN报文,进入SYN_SEND状态; 服务器发送SYN+ACK,进入SYN_RCVD; 客户端发送ACK,进入ESTABLEISHED,在客户端认为连接已经建立好; 服务器接收到ACK,认为连接已经建立好了。
四次挥手
文件的生命周期随进程,当进程异常退出,文件描述符也会随之关闭。 假设客户端关闭连接,则发送FIN报文进入FIN_WAIT,服务器此时收到报文会对报文进行应答,发送进行ACK表示同意客户端断开连接,并进入CLOSE_WAIT状态。此时从客户端->服务器的连接就断开了 。但是服务器此时还是可以给客户端发送消息的。只有当服务器也发送FIN报文给客户端此时服务器发送FIN会进入LAST_ACK状态,表示收到最后一个ACK就会断开连接。 ,客户端进行ACK后进入TIME_WAIT状态(主动断开连接的一方都会进入的状态)等待一段时间自动CLOSED。 而服务器若收到了ACK则关闭连接,否则会重复发送FIN报文最后退出。
验证CLOSE_WAIT
前提:服务器编写套接字进行获取连接但不关闭连接。不close文件描述符 即当服务器关闭进程,或者主动关闭,都会使连接处于LAST_ACK的状态。 netstat -ntp 查看tcp的链接,带上pid,转化成数字的方式查看。
实验: 服务器accept,此时客户端主动断开连接,但是服务器不关闭文件描述符,观察双方的文件状态。
服务器: 客户端: 特点: 服务器上的CLOSE_WAIT状态需要等待很长的时间服务器才会对文件描述符进行关闭。 但是客户端上面的FIN_WAIT2状态很快就能消散。并且客户端不会进入TIME_WAIT状态了!
当客户端使用telnet连接上就关闭连接,此时由于我的服务器并没有关闭文件描述符,所以会进入CLOSE_WAIT状态。 原因: 服务器虽然没有关闭文件描述符,但是对于客户端发来的关闭连接的请求会发送ACK给对端,此时客户端由于一段时间没有收到服务器发来的FIN,就认为服务器异常了,自己就关闭连接了。而服务器上的CLOSE_WAIT会持续一段时间。
客户端使用telnet即可,服务器用下面的代码。
TCP验证CLOSE_WAIT状态代码
server.cc
#include "tcp_server.hpp"
using namespace ns_tcpserver;
int main()
{
TcpServer svr(8080);
svr.InitTcpServer();
svr.Loop();
return 0;
}
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_tcpserver{
const int backlog = 1;
class TcpServer{
private:
uint16_t port;
int listen_sock;
public:
TcpServer(int _port):port(_port), listen_sock(-1)
{}
void InitTcpServer()
{
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0){
std::cout << "socket error" << std::endl;
exit(2);
}
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
exit(3);
}
if(listen(listen_sock, backlog) < 0){
std::cerr << "listen error" << std::endl;
exit(4);
}
}
void Loop()
{
while(true){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0){
std::cout << "warning: accept error" << std::endl;
continue;
}
std::cout << "get a new sock: " << sock << std::endl;
}
}
~TcpServer()
{
if(listen_sock >= 0) close(listen_sock);
}
};
}
而客户端由于关闭了链接,会进入FIN_WAIT2,表示收到了服务器发送的ACK,虽然服务器并没有调用close,但是对于客户端发送的FIN报文是会处理的(即发送ACK)
如果此时服务器不停止,一旦有连接时close_wait状态,可能会维持一个比较长的时间,若服务器存在大量close_wait状态的套接字,此时一定要注意是否服务器有bug!!
此时若直接将服务器关闭,可以通过netstat -ntp观察到服务器进入LAST_ACK状态。 但是如果对端在TIME_WAIT时间过后了,连接关闭了,此时服务器也会经过一段时间才会将链接进行关闭。
只有在CLOSED状态才是彻底关闭了文件描述符,不然如果客户端关闭连接就没法给服务器发送ACK进行回应了!
可以发现,倘若服务器若处于LAST_ACK而客户端已经关闭了,会进行较长时间的等待!!所以侧面证明了TIME_WAIT的重要性。
time_wait状态由服务器主动关闭测试的原因:
服务器端主动杀掉进程来关闭连接的情况: 这个时候由于服务器会主动进入TIME_WAIT状态,客户端能够正常退出,但是由于TIME_WAIT时间过长导致服务器的端口依旧处于被占用状态,导致一时间程序没法重新运行!!!
TIME_WAIT时间是由配置文件和系统共同决定的。
解决服务挂掉无法立即bind
setsockopt的重要性: 在socket的后面添加即可。 监听套接字,层级,功能 opt是用来绑定的一个值。 int opt = 1;setsockopt(lsock,SOL_SOCKET,SO_REUSERADDR,&opt,sizeof(opt));
基于短链接模式下的就是服务器主动断开连接,此时实际并无大碍,因为不影响申请新的连接。
TIME_WAIT = 2*MSL MSL表示从A->B的时间。
TIME_WAIT存在的必要性: 1.较大概率对端能够尽快断开连接。因为最后一个ACK可能丢失。让链接尽量是四次挥手成功的。 2.此时可能历史上还有数据没有到达(数据并不是按序到达的),所以这是为了历史数据在网络中尽快消散。
超时重传时500ms(指数增长),而TIME_WAIT在Linux下时60s,这个时间可以收到大量的FIN消息,此时可以认为这个ACK已经丢失!!!
可靠性是什么? 在tcp这里,数据的可靠性强调的是用户数据传送时候的可靠性,而建立连接,释放连接只是对于用户数据的可靠性做的工作。用户数据指的是通过send,recv接受的数据。
发送方在发送数据的时候,把数据抛到网络当中,此时数据不一定完成了发送过程!那么在我没有收到ACK的时候,刚刚发出去的数据要不要被暂时保存起来呢?不能! – 可靠性 2.发送方一次可以发送多个数据,保证一个发送,一个应答,保证每一个报文都有ACK即可(有特殊情况) 。 缓冲区可以分成三个部分,已经发送并且收到ACK, 和已经发送但是没有收到ACK或没有发送,和尚未发送部分。
中间部分就是滑动窗口:无需等待确认应答而可以继续发送的数据最大量。
滑动窗口大小收到对方的接受能力和网络拥塞的状况。
滑动窗口本质是发送缓冲区的一部分。
滑动窗口是效率上的一种体现,因为单次能发送的数据由窗口已经标识了。
滑动窗口的大小: 滑动窗口的大小也取决于对面接受缓冲区的大小,若对方能接受的数据为0,那么我们的滑动窗口也就是0。不过还要跟网络拥塞窗口取min值进行发送。 而对方的接受能力即对面发过来报文当中滑动窗口的大小。
当滑动窗口的数据发送时,若没有收到ACK,窗口不移动,而收到ACK后,ACK报文会提供对端的窗口大小,若窗口大小为0,则窗口的右侧不动,左侧像右侧对齐。 表示滑动窗口大小为0,并且窗口原先数据归类到收到ACK的左侧模块。
深入理解滑动窗口: 1.窗口只能整体向右移动吗?能缩小吗,能扩大吗? 不是,能,能 缩小的情况可能对面的窗口大小变小了,上层没有把数据取走,此时由于接受方的窗口大小变小,我们的滑动窗口也必须缩小。 扩大的情况就是对端的窗口大小突然增大,我们的窗口大小也能随之增大,但是不能超过对面接受缓冲区大小。
滑动窗口能够配合流量控制:支持了发送大量数据,并且尽可能保证数据传输过去不会由于对端接受不了被丢弃。
流量控制是可靠性的体现,让数据不会超过对端的可接受的范围;为滑动窗口打下了基础。
2.如何理解滑动窗口,如果设计一个滑动窗口?
3.滑动窗口一直向右移动,缓冲区大小有限,有没有可能移动出发送缓冲区。
TCP是面向字节流的,可以认为发送缓冲区是一个char send_buffer[SIZE],每个空格是一个字节。
小总结: 对方发送ACK的同时,将会变更我的滑动窗口的左侧。而对方窗口更新将会更改我的滑动窗口的右侧。
并且滑动窗口具有保存未收到ACK部分的数据的能力。
对于服务器也如此,因为套接字本身是全双工的,每个套接字都有发送和接受缓冲区!
在刚开始还没有和对方建立连接,我们能否知道对方的接受能力? 不能的!因为在三次握手时双方才会开始协商起窗口大小,协商MSS大小。
超时重传机制
情况一: 数据包已经抵达, ACK被丢了.
这种情况的部分ACK丢失没有 影响,此时后续的报文可以说明前面的报文已经获取。(即不怕中间报文没收到,即收到多少,窗口往右边移动即可) 情况二: 数据包就直接丢了. 主机B若发现1001~2000长时间没收到,由于后续的报文都会重复发送下次要发送1001,所以主机A收到重复的请求就会认为1001~2000 丢失,此时只需要重新传这个区间即可,就不会过多的重复数据!!! TCP提供对1001~2000 进行快重传机制 ,让对端的数据能够更快完整的收上来。
快重传 vs 超时重传 ACK的语义非常重要,因为决定了即使收到了后面的数据,前面的数据没有收到,也会返回先前没有收到数据的下一个字节序!
快重传需要收到三个以上的重复请求报文才会触发,所以超时重传和快重传并不会冲突,如三次握手的时候是串行的,此时用不了快重传。 但后续报文接受发送过程中快重传是提高效率的方法。
- 重新回忆超时重传,是以500ms为基本单位指数增长。
- 快重传是在连续收到三个以上同样的报文则将该报文发送。
窗口的跟新可以是双方的,即我主动向你询问你的窗口大小,或者你在你的窗口大小更新的时候也发送一个报文告知我你的窗口大小发生变化。
拥塞控制
滑动窗口由内核进行维护,报头的滑动窗口标识了对方的接受能力,但是发送时我们除了对方的接受能力需要考虑,还要考虑网络状况是否允许我们发送大量的报文。
滑动窗口 = min(拥塞窗口,对端的窗口大小) 为什么这么说呢,对端的窗口大小决定了对端能接受的大小,我们发送的时候就可以有一个尺度,但是有这样一种情况,我们今天网络很差,即使对端的窗口大小很大,但是网络情况差到发送一点点数据都不行,这个时候我们应该减少发送量。
此时如果利用超时重传机制,导致网络会更加拥塞,此时我们应该需要使用拥塞窗口来控制。
当我们今天发送数据,数据发送批量,发现一小部分没有发送成功,我们觉得很正常,此时利用TCP的重传机制解决即可。 但是如果大部分数据都没发送成功,此时我们应该考虑网络环境的问题。此时我们应该减少数据发送,让网络中的数据变少,减少网络压力。
- 此处引入一个概念程为拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;这样子实际上我们第一次发送1个数据,收到回应后,拥塞窗口就变成2,在对端接受缓冲区允许的情况,我们可以发送2倍的数据!!
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
刚开始的指数增长是尽快让传递批量数据,而到达慢启动的阈值我们就线性增加即可,此时不能用指数增长。此时将慢启动的阈值设置为发送网络拥塞的一半,然后重1开始按指数级发送数据,到达阈值再线性增长。
如果此时大批量数据重传只会导致网络中的数据更加拥塞!!并且数据也不会被对方大量收到。
主机如果一次性发送的数据量大于拥塞窗口,就可能发生网络拥塞。
由于网络的状态主机并不知道,所以也是以尝试的方式进行的。
在一个局域网当中,当发生网络拥塞时,这个网络当中的大部分主机都会进行拥塞控制!所以网络的状况会变好,因为使用TCP协议的机子都会采用拥塞控制,所以同一时间可能大量的机子都会不向网络发送数据,让网络的状况变好。
延迟应答
提高效率的一种策略,即网络传输的时间可能比较长,那么我这次发送东西给你的时候可以尽量一次准备多一点再发送,因为我准备东西的时间远远小于在网络传输的时间,所以总体能够提高效率。
延迟应答的时间比超时重传的时间短,保证能处理数据尽量多的同时返回一个较大的窗口。
捎带应答
例如报文报头部分携带ACK字段,同时正文当中也可以附送一些数据,这样就可以少单独发送数据了。
双向通信的时候即做确认,又做应答。
TCP面向字节流
从本主机可以一个个字节拷贝,发送给对端可以一个个字节发送,对端也可以选择一个个接受,或一次接受完。即不需要关心数据的格式。 发送的时候由TCP自由决定,接受的时候由TCP自由决定。
粘包问题
需要通过自定义协议,规定报头+数据如何区分,以及数据和报头分别多长。
就是由于接受方可以一个个接受字节数据,但是由于字符数据有对应的含义,如http协议当中的每个字段都有对应的道理。
所以需要应用层的协议来确定读上来是一个完整的http请求,而不会读到下一个数据报的数据。
如http协议在读取数据的时候可以先读取到换行符然后一行解决,直到读到换行,然后在读到的数据的Content-Length当中获取正文的大小即可,再读取正文的大小即可。
明确报文和报文之间的边界问题: 特殊字符(#$@) 固定大小 (协议规定) 自描述字段(如Content-Length)
TCP并不需要解决粘包问题,TCP所看到的都是一个个的字节数据。真正TCP的数据解释是应用层的协议解决的。
注意: UDP不存在粘包问题,UDP的报文是一个个交付给上层,上层只有收到和没收到两种情况,这也就是面向数据报。
TCP连接异常
连接崩溃的情况
- 机器重启:会释放文件描述符,发送FIN,和正常关闭一样。
- 进程终止:和机器重启一样。
- 网线断开/机器掉电:接收端的连接还在,当接收端对我进行写入,会收到一个reset,然后TCP内置一个保活定时器,询问对方是否还在,不在就释放连接。
但最好不要把保活定时器作为一个应用层的工具,因为不同的应用层对于连接是否需要关闭是不一样的,比如QQ,在断线后,也会定期尝试重新连接。
UDP实现可靠性
如需要聊天: 应用层加上按序到达,丢包重传等机制。
listen的第二个参数
首先说明一点,服务器不调用accept也会将连接建立好,只不过上层不会对这个数据进行接受而已。
结论:
-
accept不参与三次握手,只是负责将已经建立好的连接拿上来。 -
backlog为1,可以接受两个连接。 这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1 -
存在两个队列,全连接队列是已经建立好连接的;半连接队列是用来保存处于SYN_SENT和SYN_RECV状态的请求 -
半连接队列的长度不宜过长,因为过长的时候后续被服务的等待时间也就过长。 -
SYN_RECV同时来了,是看谁先完成三次握手,三次握手区间不需要保证按序到达,否则第一个SYN_RECV先来但不建立连接,就会阻塞后面的连接了。
制作一个网络版的命令行解释器
预备知识 为了解析能够成功,我们读取上来需要将buffer[s-2]的位置置0,否则会出现问题。
将远端发送过来的命令行进行处理,并将数据处理结果做出响应。
#include<iostream>
using namespace std;
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string>
#include<typeinfo>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
class Server
{
public:
Server(int port):_port(port)
{}
void InitServer()
{
_lsock = socket(AF_INET,SOCK_STREAM,0);
if(_lsock < 0)
{
cerr << "socket";
exit(1);
}
int opt = 1;
setsockopt(_lsock,SOL_SOCKET,SO_REUSEPORT,&opt,sizeof(opt));
struct sockaddr_in addr;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
if(bind(_lsock,(struct sockaddr*)&addr,sizeof(addr)) < 0)
{
cerr << "bind" ;
exit(2);
}
if(listen(_lsock,5) < 0)
{
cerr << "listen";
exit(3);
}
}
void start()
{
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_lsock,(struct sockaddr*)&peer,&len);
std::string peer_ip = inet_ntoa(peer.sin_addr);
int peer_port = peer.sin_port;
cout << "peer_ip: " << peer_ip << " peer_port: "<< peer_port<<endl;
char buffer[1024];
size_t s = recv(fd,buffer,sizeof(buffer),0);
if(s > 0)
{
buffer[s-2] = 0;
cout << "clinet# " << buffer <<endl;
}
char *command[64] = {0};
command[0] = strtok(buffer, " ");
int i = 1;
while(command[i] = strtok(NULL, " ")){
i++;
}
if(fork() == 0)
{
dup2(fd,1);
execvp(command[0],command);
}
else
waitpid(-1,nullptr,0);
close(fd);
}
}
private:
int _lsock;
int _port;
};
采用popen接口:popen实际上就做了我们上面做的切割字符串,然后fork创建子进程完成任务的工作。 注意popen打开要用pclose关闭,不然就手动wait或者忽略SIGCHILD信号。
注意,若是编写udp,只能用下面这种方法,因为dup2只能允许流式的,而不允许数据报式的。
#include<iostream>
using namespace std;
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string>
#include<typeinfo>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
class Server
{
public:
Server(int port):_port(port)
{}
void InitServer()
{
_lsock = socket(AF_INET,SOCK_STREAM,0);
if(_lsock < 0)
{
cerr << "socket";
exit(1);
}
int opt = 1;
setsockopt(_lsock,SOL_SOCKET,SO_REUSEPORT,&opt,sizeof(opt));
struct sockaddr_in addr;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
if(bind(_lsock,(struct sockaddr*)&addr,sizeof(addr)) < 0)
{
cerr << "bind" ;
exit(2);
}
if(listen(_lsock,5) < 0)
{
cerr << "listen";
exit(3);
}
}
void start()
{
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_lsock,(struct sockaddr*)&peer,&len);
std::string peer_ip = inet_ntoa(peer.sin_addr);
int peer_port = peer.sin_port;
cout << "peer_ip: " << peer_ip << " peer_port: "<< peer_port<<endl;
char buffer[1024];
size_t s = recv(fd,buffer,sizeof(buffer),0);
if(s > 0)
{
buffer[s-2] = 0;
cout << "clinet# " << buffer <<endl;
}
FILE* pf= popen(buffer,"r");
std::string str;
char ch;
while(fread(&ch,1,1,pf))
{
str+=ch;
}
send(fd,str.c_str(),str.size(),0);
pclose(pf);
close(fd);
}
}
private:
int _lsock;
int _port;
};
服务端: 客户端:
校验和计算
看这篇博客吧。写的挺好的。https://blog.csdn.net/zwl1584671413/article/details/107252462 IP层的校验和偏重于有没有出错。 传输层校验和报文是否是正确的,是否是给我这个协议的。
SYN-Cookie
半连接队列满了怎么办?当有大量请求或者服务器被攻击,此时我们应当如何处理? 此时可以设置SYN-Cookie,在服务器端不记录,但是发送SYN-ACK + Cookies给用户端,直到客户端发送ACK以及先前的服务器发来的Cookies,服务器重新生成请求。
缺点:
- Cookie技术只计算了报头,并且没有真正在服务端放入连接队列,所以很多功能没有了,如超时重传;
- 不高效,计算Cookie需要一定的运算量。
优点: 对于SYN泛洪攻击能够起到一定的作用。
DOS攻击
常见的攻击,从TCP/IP四层协议。
- http可以用来攻击服务器的内部资源,消耗服务器内部的运算器能力。
- TCP由于需要建立连接,所以通常攻击时采用发送SYN,但是不对服务器的SYN+ACK回应,这样就能塞满服务器的半连接队列表,并且由于有超时重传机制,这个连接会在服务器存在一段时间,消耗服务器的资源。
- UDP则可以通过伪造ip源/目的地址,比如源ip地址填写服务器,然后目的ip地址发送给各大其他应用商的服务器,然后借刀杀人。
- UDP也可以伪造不同的源IP,目的IP填服务器的,这样子有不同的资源发送给服务器,实际上会消耗服务器的带宽。
- 也可以使用基于UDP协议的DNS服务器,当源IP填服务器的,然后发送要访问的地址给DNS,DNS会发送若干倍的资源给服务器。相当于信息被扩大了很多倍。
- IP层可以通过发送ping命令,也是消耗服务器的带宽。
解决方案: 购买IP清洗服务器,让服务器先对这些访问的IP进行清理,并且这种服务器凭借多年经验,有自己的黑白名单。以及对这类访问做了极大的优化,可以接受更多的请求。
|