linux网络udp和tcp

端口号

端口号可以识别将数据传输给哪一个应用程序。
在这里插入图片描述
在tcp/ip协议中,用源ip,源端口号,目的ip,目的端口号,协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看);
在这里插入图片描述

端口号划分

0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的. 你是不能绑定的。
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.(16字节最大能表示的数)

常见端口号

ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
http服务器, 使用80端口
https服务器, 使用443

SSH

SSH是一个可以安全远程登陆的服务器(telnet也可与远程登陆,但是不安全。)

SSH既然可以远程登陆,那么我们是否可以在一个装有ssh的任意系统的主机上登陆云服务器呢?

答案是可以的。

ssh 账号名@ip地址

我就可以在windows上的cmd登陆进来了。
在这里插入图片描述
手机上也可以用ssh登陆。

两个问题

一个进程是否可以有两个端口号?
可以。不过不常见。ftp里面有这种情况。(其实只要能通过端口号找到对应pcb就可以)

一个端口号是否可以被两个进程绑定?
不可以,这样就无法通过这个端口号找到对应的进程了。

netstat

netstat是一个用来查看网络状态的重要工具.
语法:netstat [选项]
功能:查看网络状态
常用选项:

  • n 拒绝显示别名,能显示数字的全部转化成数字(比如ssh带上-n就会变成22)
  • l 仅列出有在 Listen (监听) 的服务状态
  • p 显示建立相关链接的程序名
  • t (tcp)仅显示tcp相关选项
  • u (udp)仅显示udp相关选项
  • a (all)显示所有选项

带n选项的local address是22

在这里插入图片描述
不带n的local address22是ssh
在这里插入图片描述

pidof

可以直接查看进程的pid
在这里插入图片描述
怎么用pidof和kill结合来终止进程?

xargs

这是不对的,因为管道相当于把pid写入到匿名管道里面,kill进程从里面读取信息。
最后就相当于 kill pid -9,格式不对。
在这里插入图片描述

要把pid作为参数传给kill进程,要用xargs来传

pidof httpServer | xargs kill -9

长连接和短连接

这是tcp通信的过程,先connect,然后发送request,然后接收respond
最后disconnect
在这里插入图片描述
在这里插入图片描述

UDP协议

UDP不保证可靠性

UDP header

在这里插入图片描述
第一个是源端口号
第二个是目的端口号
第三个是整个udp数据报的大小,包含了报头和data
第四个是udp的校验和

有几个问题说一下:

  1. 为什么之前用udp通信的时候,从client端recvfrom的时候可以获取到远端的套接字呢?
    原因就是UDP数据报自己带有源端口号,因此传入一个输出型的结构体就可以获取远端的套接字。
  2. 读取udp数据报的时候如何把报头和数据分离开?
    报头就是8个字节,读完8个字节后面就是数据。但是数据读多少字节呢?有一个UDP length,这个长度记录了整一个数据报的大小,用长度减去报头的大小(8个字节)就是要读取的数据大小
  3. 如果检验错误,整一个数据报就被丢弃了。

udp的header其实就是一个结构体。
在这里插入图片描述

UDP的特点

UDP传输的过程类似于寄信.

  • 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
  • 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
  • 面向数据报: 不能够灵活的控制读写数据的次数和数量
  • udp可以读写同时,是全双工的
  • udp没有缓冲区,它拿到data之后加上header就直接交给下层网络层了。

应用层发给udp的数据不能拆分也不能合并(面向数据报)
举个例子:如果上层发了一个100字节的数据,udp不能调用10次recvfrom,每次读10个字节。必须读一次,一次读100字节。

udp最大长度

udp length是16字节,因此udp length最大是65536字节,也就是64kb。

基于udp协议的应用层协议

DNS:域名解析协议

tcp

tcp可以解决可靠性和效率两大方面
为什么会有可靠性问题?
网络传输,线路更长,容易出现问题。

不可靠问题都有哪些?
丢包(ACK+确认序号解决)
乱序
(序号解决)
错误(错误了就丢掉了,也是丢包的一种)
接收缓冲区满了,来不及接收了(接收不了了就丢掉了,也是丢包)

tcp的特点

  • 面向连接
  • 按序到达(序号)
  • 确认应答(确认序号)
  • 超时重传

tcp header

在这里插入图片描述

4位首部长度

4位首部长度表示该TCP头部有多少个32位bit(有多少个4字节);

tcp的header标准长度是20,但是有一个选项,选项的长度取决于该tcp的data offset(4位header长度)

什么意思呢?
当data offset为5的时候,header长度为20,因此选项长度为0
当data offset为15(4个bit最大能表示的数),header长度为15 * 4 = 60,因此选项长度为60 - 20 = 40

注:data offset不能为0.因为首部长度最少也要20字节。

tcp的可靠性(确认应答机制)

如何保证发送信息被对方接收到了?
在这里插入图片描述
答案:通过对方是否给自己的回应来判断对方是否收到了自己的消息。


注:ACK (Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。(ACK可以附带数据一起发送,后面讲)
在这里插入图片描述

这种机制叫做确认应答机制
下面这个图很形象的说明了确认应答机制。发一句我ACK一次
在这里插入图片描述

这里讲一下http的send和tcp的发送数据的关系:
send和tcp中的发送数据并没有直接关系。

  1. http的send是先将数据从用户层拷贝到内核的发送缓冲区
  2. tcp是通信细节,send只是把整一个发送的过程笼统的概括成发送,它并不关心底层干了什么,有可能一次send,tcp底层发送了n次数据,接收了n次ack。

超时重传机制

如果server的ACK发送失败了,怎么办?server要不要继续ACK?

答:不用。有一种机制叫重传机制,client端没有接收到server的ACK,他就认为是自己发送数据失败了,重新发送一遍。

其实client是不知道这个数据丢了的,这种行为是人为规定的。

促发超时重传机制有两种可能性:
1.request丢了
2.request成功发送过去了,但是服务端的ACK丢了

在这里插入图片描述
在第二种情况:server端是收到了两次同样的数据,server会把重复的数据丢掉。
server怎么知道重复了呢?
序号相同,因此重复了。


超时的时间怎么定呢?
超时的时间是浮动的,根据网络情况而定。网络差时间就久一点,网络好时间就短一点。

linux中超时是以500ms为单位进行操控。
第一次等500ms重发
第二次等2500ms重发
第三次等4
500ms重发
第四次等8*500ms重发

直到超过一个阈值,就会自动断开连接。

32位序号(解决乱序问题 + 去重)

我们发数据的时候,有可能会乱序。比如说hello world,发送的时候可能先发送world,再发送hello。因为发送距离太远了,由于路径选择问题,信息可能并不会按顺序到达。

32位序号就是防止乱序而出现的问题的。他会给数据编号。

它会给hello world给编个号,给hello编号1,再给world编号2。即使接收的时候是乱序的,最后排一下序即可。

具体编号方式:
tcp为每一个数据的字节都编上了序列号
在这里插入图片描述
注:没有数据的tcp header的序号是不会变的,(可以理解成没有)。序号只会给数据编号。

32位确认序号(解决丢包问题)

现在有这么一个情况,client发了三次request,server应该回应三次ACK来确定收到了这三个信息。

那我们怎么区分这三个ACK是回应哪一条消息的呢?

因此我们要给ACK编上号。这个确认的应答的编号应该是和对应的request相对应的。(不是一样的)

比如说现在我要发hello, world, !!!,编号分别是6,7,8.
那么我们的ACK的确认序号就是7,8,9。
会在原始header序号基础上+1


举几个例子再解释一下:

  1. client端收到的ACK序号为8是什么意思?
    server端收到了编号7之前的所有内容。现在client应该从原始header序号为8开始发送信息。

(下面这个很重要,也是tcp允许丢包的现象)

  1. client端都到了ACK序号为8,但是ACK序号7的回应都丢了,client会怎么样?
    client会以为原始header序号为6,7的内容都被收到了,现在就发送序号为8的内容。

总结一下:
确认序号是server给client发的ack的编号,值是原始序号+1.代表的含义是原始序号为确认序号-1的所有数据都被收到了。

简单来说是表示历史上的都收到了,这个是协议规定的。是为了配合滑动窗口才这么设置的。


为什么一个header要同时有序号和确认序号(重点)

既然client给server发送request的时候server只关心client的序号,server给client发送ACK的时候client只关心server的确认序号,那为什么一个header里面要同时包含两个序号?

不可以client给server发送的时候只填序号,server给clientACK的时候只填确认序号吗?

答:tcp是全双工的。读写可以同时进行,这也是后面要讲的ACK带数据的情况。ACK带数据的时候,client端既需要ACK的确认序号,又需要数据的序号。


tcp缓冲区

之前看过sock的代码,里面有两个接收和写队列,它就是tcp的接收缓冲区和发送缓冲区
在这里插入图片描述
send函数和recv函数本质上是把在用户区的数据拷贝到了内核区的receive_queue和write_queue里面了,其实是一个拷贝接口。

tcp的缓冲区本质上就是一个生产者消费者模型

16位窗口大小

指的是发送方的接收缓冲区剩余空间的大小

是发送端的接收缓冲区的剩余空间还是接收端的接收缓冲区的剩余空间呢?

发送端的!!!为什么呢?

因为别人在发送数据返回给你的时候,大小要合理,不能直接乱发。

可以通过窗口大小来进行流量控制。

总结一下:
我要发送数据给接收方的时候,就必须把我自己的窗口大小,即自己的接收缓冲区剩余空间大小发送给接收方,实现流量控制。

标志位

ACK (Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。

SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。在客户机和服务器之间建立正常的TCP网络连接时,客户机首先发出一个SYN消息,服务器使用SYN+ACK应答表示接收到了这个消息,最后客户机再以ACK消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。

FIN:finish,终止

PSH:push,提示接收端应用程序立刻从TCP缓冲区把数据读走

RST:reset。重新建立连接。
下图是发生rst的情况:
在这里插入图片描述
URG:紧急指针是否有效,1代表有效,0代表无效。紧急指针指向的数据叫带外数据(out of band)说人话就是加急的数据,需要优先读取。紧急指针只能指向一个字节的数据。在内核里面紧急指针不是指针,只是一个整数,0代表没有,1代表一个字节。

内核tcpheader源代码

在这里插入图片描述

tcp连接管理机制

在这里插入图片描述

三次握手

1.链接是什么?
链接本身是有成本的,因为链接是需要创建数据结构来管理的。
tcp3次握手成功之后,client和server都要维护链接。

2.为什么是三次握手?
如果用1&2次握手,容易遭到syn洪水攻击。
什么意思呢?
如果用1次握手,即client发一次syn就建立连接,那client疯狂的发syn就会导致创建很多连接,创建连接是需要成本的,会导致server顶不住了。

如果用2次握手也是同理,client发一次syn,server立刻回一个ack。server在发送完ack之后就建立连接了,client可以最后把ack丢掉,不让client端建立连接,就可以让client没有负担的疯狂的发送syn,server创建很多连接,也会挂掉。(因此偶数次的握手是绝对不可取的,因为是服务器先承担了建立连接的责任。)

三次握手可以挡住基本的洪水攻击,至少在建立链接的时候client端也同样地要维护链接。即使client端发动洪水攻击,它自己的主机上也要先建立很多垃圾连接。

为什么三次握手可以?
1.用最小成本验证双工。(读写同时进行)解释一下:验证全双工的方式就是我既能收到你的信息也可以发送信息给你。如果是1次握手,server只收,client只发,无法验证另外的功能。如果是2次握手,server接收syn后发送ack,但是无法知道client是否收到了自己的ack,因此也无法验证。

2.让服务器不要出现连接建立的误判情况。解释一下:当三次握手最后一次client发送ack的同时,client就认为连接已经建立好了,但是最后一次ack是有可能会丢失的,如果丢失了服务器是不会建立连接的。这样服务器就没有资源损失,损失让client承担了。我们要选择让客户端去承担这个责任,因为server是一对多的,多次遭受误判会导致建立很多垃圾连接,导致资源浪费。

四次挥手

为什么要四次挥手?
client和server是双工通信的,因此断开连接要关闭client的通道和server的通道。关闭之后又要ack,因此是四次。

在这里插入图片描述

1.client发送FIN,server发送ACK。
这里有一个问题:client发送断开发送连接,底层的数据结构有被释放吗?以后还会发数据给server吗?
答:第一次发送完FIN之后,底层数据结构并不会被释放。它所说的断开发送连接指的是应用层不再发送数据了,即用户无法在从用户区把数据拷贝到内核里的发送队列了。它之后在server端发送FIN之后还会回应一个ack的。那个时候才会释放底层的数据结构。

2.client发送FIN,在应用层干了什么?
答:client端close(sock)关闭连接。

TIME_WAIT

TIME_WAIT要求主动断开连接的一方(server和client都可以进入TIME_WAIT),要进行等待。为什么?
假设主动断开的一方是客户端。

客户端在最后发送ack的时候有可能会丢失,如果没有TIME_WAIT,客户端发送完ack直接就关闭连接了,万一此时ack丢了,server端收不到ack,他就超时重传FIN,客户端还是收不到,server继续重传,这会浪费资源。

如果有TIME_WAIT,client的ack还是可能丢,server会重传。client收到重传之后继续ack,这样server的连接肯定可以关闭。
如果ack没有丢,server的连接关闭了,TIME_WAIT等了一段时间发现没有FIN重传过来,他就认为连接已经关闭了,它也从TIME_WAIT变成CLOSE了。

总结一下:
1.保证ack被对方收到了。
2.等待历史数据在网络上进行消散。有可能断开连接的请求是先于数据发送的请求的,因此要等待所有数据都发送完才可以断开连接

为了做到上面几点,TIME_WAIT设置成2MSL。 MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长的最长时间,超过这个时间报文将被丢弃。

ESTABLISHED

这个状态代表对象认为自己的连接已经建立好了。比如说三次握手之后,client发出ACK之后立马就进入了established,因为它认为它已经建立好连接了。server收到了ack之后也进入了established。

总体过程文字描述

服务端状态转化:

  1. [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
  2. [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.
  3. [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
  4. [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
  5. [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
  6. [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.

客户端状态转化:

  1. [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
  2. [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
  3. [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
  4. [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
  5. [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
  6. [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.(原因在上面)

抓包看tcp细节

注意:由于用的是云服务器,因此观察很麻烦。
telnet的时候千万不要在云服务器上面telnet,在windows的cmd上telnet,看的会舒服很多。

抓包命令
-i表示监听接口
any是所有接口
-nn是把能显示成数字的都显示成数字
tcp是抓tcp的包
port8080是抓端口号为8080的包

sudo tcpdump -i any -nn tcp port 8080
三次握手

client的ip是220.115.159.7,端口号是49481
server的ip是172.21.0.2 端口号是8080

client先发了一个syn给server
server再发了一个syn+ack给client
client再发一个ack给server

连接建立完成
在这里插入图片描述

netstat -npt看一下,现在连接已经创建起来了,已经是established状态了。(由于主机是服务器,远端是客户端,因此local address是服务器)
在这里插入图片描述

CLOSE_WAIT状态

要浮现出CLOSE_WAIT的状态,就让server端不要close(sock)就好了。
由于我让服务器不close(sock),因此服务器永远不会进入LAST_ACK状态给client发FIN。

下图我们发现client给server发了一个FIN+ACK,server回了一个ACK
之后就没了。
在这里插入图片描述
netstat查看:server确实在CLOSE_WAIT状态

在这里插入图片描述

总结:
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.

TIME_WAIT状态与bind error

要复现TIME_WAIT状态就让主动断开连接的那一方不要close(sock)即可。由于本地是服务器,因此我们这次先退出服务器。

可以发现出现了TIME_WAIT

在这里插入图片描述
这也是为什么我们有时候退出了服务器,短时间再绑定同一个端口号的时候会出现bind error的原因了,因为有连接还没有断开,服务器先关闭导致一直在TIME_WAIT状态出不来了。

那为什么等一段时间就又可以了,之前讲过TIME_WAIT等待的时间是2MSL,过了这个时间就好了。

setsockopt(重要)

一个大型服务器如果上面连了几千万个连接,然后忽然服务器挂掉了。服务器成了先退出的一方,服务器现在要重启,重启的时候由于每一个链接都处于TIME_WAIT状态,重启之后服务器也无法bind成功。那服务器就只能等了,怎么解决这个问题呢?

用setsockopt

使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
在这里插入图片描述

这样就不会出现由于进入了TIME_WAIT而导致的bind error问题了。

它的写法和其他socket函数都差不多,基本是固定的

int opt = 1;
setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

setsockopt写的位置应该在刚创建出来的socket后面
顺序如下:

socket()
setsockopt()
bind()
listen()
accept()
...

滑动窗口

刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.

这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候

既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).

如图:

在这里插入图片描述
滑动窗口是发送缓冲区的一部分。
在这里插入图片描述

窗口内的数据代表要发送的数据,当发送之后收到ack,窗口就会往右移动。比如说:上图收到ack的确认序号为36,则证明32-35的数据都收到了,窗口就移动到36了。

**超时重传机制和滑动窗口也有关系,**如果32没有收到ack,窗口就不动了,等待一定时间后再继续发送。

重传机制和滑动窗口的关系

情况1
在这里插入图片描述
这些ack丢了没什么关系,因为它们不是那一批当中的最后一个ack。当4001的ack被收到之后,滑动窗口认为之前那一批1 - 4000的所有数据都被收到了,他就开始移动了。

情况2
在这里插入图片描述

这种情况是数据丢了,1001-2000的数据丢了,没有给ack,因此滑动窗口不会动的。即使收到了除了1001-2000的数据以外的所有数据都收到了,ack回应的依然是1001,它要求发送方从1001开始重新发数据。

协议规定,如果收到连续三个同样的ack,发送方就会立刻重传。这种重传方式叫做快重传。

快重传和超时重传是互相补充的。

  1. 当窗口大小比较小的时候,比如为2的时候,收到的ack最多也就两个,无法触发快重传,这时候还是使用超时重传。
  2. 快重传是用于大量数据传输的时候用的

流量控制(接收窗口大小)

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.

因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
  • 窗口大小字段越大, 说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端

这个图解释的很好。
在这里插入图片描述

一旦接收方的接收队列满了。有两个机制可以让发送方知道它什么时候才可以继续发数据。

  • 一个是窗口探测,等一段时间它就发送一个不带数据的请求,询问窗口大小。
  • 一个是窗口更新通知,接收方可以继续接收数据的时候会发送更新通知,告知发送方。

接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;

那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;

拥塞控制

当网络很拥塞的时候,如果发送方发送10000个数据,9999个都丢了,那么发送方不会重传,而会等待。因为本来网络就已经很拥塞了,此时继续重传,会加重网络的压力。
这叫拥塞控制。

但不能一直等吧,拥塞控制采用的算法叫慢启动。

TCP引入了慢启动的机制,先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据

在这里插入图片描述

  • 此处引入一个概念为拥塞窗口
  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到一个ACK应答, 拥塞窗口加1;
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口

像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快

为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍. 此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

但拥塞窗口也不是一直会变大的,即使网络不拥塞,拥塞窗口的大小还和对方的窗口大小有关系。如果对方的窗口大小很小,拥塞窗口不会一直增大,因为受流量控制的缘故。
在这里插入图片描述

总结一下窗口:有滑动窗口拥塞窗口,这两个窗口是发送方的窗口,还有窗口大小,是接收方的窗口。

延迟应答

如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.

  • 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
  • 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
  • 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;

一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

总结一下就是:延时应答就是收到数据后不立刻ack,等上层得用户层先取走缓冲区得一部分数据之后再ack。

这个策略就是为了提高tcp的传输效率。

那么所有的包都可以延迟应答么? 肯定也不是;每个系统都有自己的规定。
我们知道隔了几个包之后就必须应答一次,或者隔了一段时间就应答一次即可。

捎带应答

应答的时候带数据就是捎带应答。
比如ack带数据就是捎带应答。

在这里插入图片描述

捎带应答有可能会让最后的四次挥手变成三次挥手。server收到FIN之后,捎带了上一次对FIN的ack和下一次的FIN进来,本来两次的通信变成一次。

tcp总结:

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:

  1. 校验和
  2. 序列号(按序到达和去重)
  3. 确认应答
  4. 超时重发
  5. 连接管理(三次握手四次挥手)
  6. 流量控制(防止丢包)
  7. 拥塞控制(防止网络原因导致丢包)

提高性能:
8. 滑动窗口
9. 快速重传
10. 延迟应答
11. 捎带应答

其他:
定时器,用于为超时重传,TIME_WAIT,这些要计时的东西计时。

关于tcp的几个重要问题

解释面向字节流

创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;(之前看过的那个sock结构体里面的两个queue)

  1. 调用write时, 数据会先写入发送缓冲区中;
    如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
    如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  2. 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据;
  3. 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做双工
  4. 由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
    写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
    (这一段话就是面向字节流的最终解释)

和udp对比一下:
面向数据报就不可以这样,给了100个字节只能一次读100个字节,不能拆开。原因就是udp没有缓冲区。而tcp有缓冲区。

之前讲过管道也是面向字节流的:
管道也是面向字节流的。因为管道本身就是一段缓冲区。有100个字节的数据,你可以一次读100,也可以读10次10字节。

粘包问题

之前说过,面向字节流容易出一个问题就是不知道一个数据包的边界在哪里,很容易读出界。这种问题叫粘包问题。

为了解决粘包问题,我们就要分开每一个数据包的边界。这个工作并不是tcp做的,而是应用层做的。比如http每次向下交付数据的时候,会加上自己的报头。http的报头上有空行,可以分开报头和数据。读到空行的时候再读content-length个字节就可以把第一个数据包的正文读完。然后后面的就是第二个数据包了。

总结一下就是:tcp并不关心数据包的边界问题,这个问题是由应用层自己加上边界解决的。

udp有粘包问题吗?
没有,因为udp自己的报头就做好了数据包的边界,直接读就好了。也可以理解成udp是面向数据报的,因此不会有粘包问题。

TCP异常情况

进程终止

我们知道发数据和收数据都是进程在做,如果此时我正在发消息,进程崩了,会发生什么?我们之间的链接会怎么样?

我们知道套接字是file*指向的,因此套接字本质也是一个文件。文件的生命周期是随进程的(在进程间通信讲过,管道的生命周期也是随进程的)

因此我们可以得到一个结论:进程终止之后,这个链接也没有了。

但是这个链接是怎么没了的呢?是否要告诉对方我们要断开连接了?

答案是要的,进程退出的时候会直接触发四次挥手,和我们应用层调用close没有区别。

机器重启的情况

我们关机的时候,有可能会提示xxx正在运行,正在关闭xxx。

所以关机之前要把所有进程终止的,因此机器重启的情况和进程终止的情况是一样的,都是直接触发四次挥手。

直接拔电源或者直接拔网线

接收端认为连接还在,一旦接收端有写入操作,接收端就可以发现连接已经不在了。(因为直接拔网线没有机会四次挥手,对方不会认为连接断开了。)

即使没有写入操作,tcp也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,就会把连接释放。

listen的第二个参数backlog

链接队列的长度。这个队列是存放等待链接的。

accept本质是从连接的等待队列中把链接读取上来。相当于从缓冲区上读数据

如果accept不读取,相当于这个链接就一直在传输层,无法到应用层。

linux里面有两种链接队列。

  • 一个是半链接队列,一个是全链接队列。

先讲全链接队列。全链接队列是用来存放established状态的链接的。它最多能存放backlog + 1个established的链接。

半链接队列是用来存放sys_recv的半链接(准确来说不是链接,因为三次握手没有完成)的,当全链接队列放满的时候,就无法再建立established状态的链接了。此时如果客户端还继续连接,发送syn,server只会回一个syn+ack,至于client再次回复的ackserver端就不管了,直接丢弃。

至于半链接队列能放多少个sys_recv状态的半链接,取决于操作系统。

注意:listen的backlog里面的都是创建好的连接。accept只是读取到应用层而已。读不读取和这个连接是否被创建没有关系,只要listen了就是创建了。

为什么实际存放的链接数会比backlog大1?后面讲内核代码的时候讲

内核理解链接

在inet_connection_sock里面有一个成员,它是用来管理链接队列的.accept就是从这里拿走request_sock_queue里面的链接的。
在这里插入图片描述
因此它的类型也是request_sock_queue

request_sock_queue里面就有两种链接队列。一个用来放request_sock,一个用来放listen_sock。到这里我们有一个感觉了,链接其实就是request_sock.
在这里插入图片描述
链接的成员
在这里插入图片描述

内核解释listen第二个参数

在sock结构体里面,有这两个成员
在这里插入图片描述
很明显,第一个是记录当前request_queue里面的链接数量的,第二个是记录request_queue最大能放多少链接数的。内核是如何判断这个request_queue是满的呢?很特别,这也是为什么+1的原因。

它比较是否满用的是大于符号,不是大于等于。有点奇怪,但事实如此。

在这里插入图片描述

sk_buffer

内核所有报文都是这个结构体,我们可以看到tcp的缓冲区write_queue和recv_queue里面放的类型全是sk_buff

在这里插入图片描述

socket内核整套理解图

在这里插入图片描述

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值