posix API与网络协议栈的实现原理
网络一共有8类技术文章:
本文是这系列文章最后一篇
下一专题写池式结构:
写完池式结构就写底层组件是怎么做的,比如libevent
做网络编程的时候,所接触到所有的网络编程,往底层走,往底层去看一看的时候,会发现到头来走的全是这些API
以客户端和服务器分开来讲,服务端有哪些?
客户端这边也同样如此
可以看到这就是所有的API另外再加一个对API的管理,就是epoll
网络编程底层的时候就这11个API
几个不是很常用的
fcntl()就是设置fd阻塞和非阻塞的,这个东西其实更多意义来讲的话,它是跟文件系统有关系的,它跟文件有关系
那以TCP为例,TCP总共过程分三个阶段,不讲具体的代码,这些API怎么实现的跟协议栈上的一些关联
其实TCP它本身来说他是个非常固定的东西
这是非常常见以及常用的问题。
先从TCP和udp这个概念上来讲,就是TCP和udp
哪些场景?既然 TCP这么常用,那为什么还会有一些场景会用到udp,udp主要用在哪里?
有这么两种应用场景
大量数据传输,比如我们传输文件,你打开迅雷会员的时候,你就应该可以感受到,打开迅雷整个局域网就你一个人下,其他人上网速特别慢,那为什么出现这些现象?
就是因为udp它中间不带这个拥塞控制,不带这种流量控制所以它在下载的时候,它的传输效率会比tcp要快,这是第一个场景在下载的时候。
第二个是说的这种实时性很强的,比如在游戏领域做竞技类游戏对应的协议,在这种弱网的环境下面传输的时候,
还有一个半个,就是在已有的一些RFC文件里面,比如像DNS协议传输分两部分里面既有TCP也有udp,udp用在哪呢?
比如说我们打开浏览器去请求一个域名的IP的时候,从我们浏览器先获取到这个 IP地址的时候,这一步它是采用的udp协议,那对于TCP呢就是域名服务器节点和节点之间的数据同步,它采用是tcp
对于这个地方为什么用UDP很难说清楚,可能就是因为他不需要去建立这个连接,你只需要发送请求反馈数据就可以了,根本就不需要建立这个连接,所以这这种是依赖于以前已经有的东西
udp相比较TCP而言,
它有哪些缺点以及有哪些优点,它的缺点有哪些?
UDP和TCP是属于传输层的,然后arp,ip,acmp协议是属于网络层的。
第一个udp的缺点,就是第一个不稳定,那为什么udp不稳定?
你的不稳定是从哪里来的?
不可靠不稳定,为什么还要进行文件传输?
udp不稳定,应该从这么几个维度来讲,
udp的不稳定是相比较TCP而言,那不稳定,
TCP总共一起三个过程,
UDP的不稳定,从这三个维度来说,
第一个有建立连接上面稳定性是否,第二个传输数据上面,第三个在断开连接上面
TCP既然是这三个过程,那 udp应该对比来说它也有这三个过程,那么不稳定,应该从这三个方面来说。
第一个在建立连接的这个过程,
建立连接的这个过程上面udp和tcp的区别在哪里?
udp是什么样的,udp压根就没有连接,就是我们udp在发送数据的时候,在准备一个socket的时候,就是我们调用sendto这个函数,
比如一个客户端和一个服务器,在客户端这边我们建立一个socket,紧接着我们调用send to就ok了,这个函数很有意思,第一个fd第二个buf第三个buf长度,第四个目的的IP地址。
相当于是什么意思,相当于你只要调用sendto,你根本中间就不要在乎这个数据它有没有连接,就是说我们只管调用sendto,对端就能收到,这个能不能收到我没法确定,只管sendto
大家有没有见过在udp这个基础上面调用connect函数的?
udp的这个connect它没有发送具体的数据,仅此就是尝试一下这条链路就是通的,对方有没有存在,对方的IP的端口有没有存在,如果把这个成功的话,那后面sendto失败的可能性会要低;
那 Tcp是怎么样的?
就是在TCP建立连接发送数据之前,我们一定要调用connect,服务器这端它是需要建立一个特定的状态的,我们才能够给对方发,比如说它需要进入一个listen的状态,
这里面有一个问题,这也是之前
三次握手是发生在哪一个函数里面?
如果你把这当做是哪个函数里面,那对TCP知识的理解不透彻,
就是在我们调用这些API这都是在应用程序里面调用的,那三次握手是发生在哪?是发生在协议栈和协议栈之间,就是在TCP 协议和TCP协议之间,这是协议栈帮忙实现的,跟我们的应用程序它是没有关系的。
3次握手,4次挥手,都是在协议栈里面实现的。
但是这个过程要有一个发起者,就是客户那边当我们调connect的时候此时准备三次握手,
再讲三次握手之前还有一个问题要解释清楚,叫TCP头。
就是每一帧数据,每一帧的TCP的包前面这一段都带这个东西,就是把这三次握手中间每一次都会带有这样一个数据包
然后再给大家讲一下三次握手。
在调用connect这个函数客户端先发起,在应用程序上面,我们调用这个函数,就把它IP地址和端口copy到协议栈里面,协议栈自己会准备一个包,也就是我们所说的syn包,那syn的包什么意思?就是在TCP头里面,那8个状态里面有一个叫做syn的状态位,把那一位置一,然后把这个数据发给对方。**至于这个发送是什么时候发的?**就是在connect的时候,对应的IP地址和端口发到协议栈上面,协议栈上组一个包,这时候把它发出来。
然后服务器收到这个包之后,第一次返回一个ack的包和一个syn的包,ack等于刚刚那个syn加一
协议栈里面具体在发送的时候那个代码的具体实现,有个叫commit_skb,
去万达广场里面吃饭的时候,那餐馆第一个你去到餐馆里,在这服务员的地方领一个号,然后你自己走开一点,然后等这个号开始服务员叫你,然后你再过来,过来之后你把号提交给服务员,服务员就拿这个号跟之前对比是不是自己发的,如果是就允许进去这个问题,
站在服务端而言有两次数据是需要存储起来的,
第一个tcp建立连接的时候,这时候在服务端需要保存客户端的信息,**那为什么需要保存客户端信息呢?**服务端只有一个,客户端可以多个同时建立三次握手。
服务端而言,它会存在有比如现在有三个在这个队列里面,
但是这个节点请注意,这个节点里面每一个框代表了一个客户端信息,也就是一个节点,这个节点它还不能用,它是一个半成品的状态,三次握手还没有完成,三次握手还只进行了第一次,这是一个半完成的状态,
就是一个半成品,等最后这一次过来之后,拿到之前的这里面首先拿到客户端的信息,
来对比这个半状态的结果里面有还是没有,如果有再把对应的数据对应的节点拿到下面。
这个节点,不是拷贝,是移动,拿了之前的这个节点的信息改变状态信息再加入进来。
前面这个半成品队列叫做半链接队列,下面这个队列叫做全连接队列。
这个半链接队列,我们也叫做syn队列
下面这个叫做accept队列。
怎么进入到全连接队列里面,要进入这个全连接队列的这个状态机里面,
只能通过三次握手的最后一次,这是一个前提条件,三次握手的最后一次过来,这是一个前提条件并且到半连接里面有,这两个条件同时满足,才进入全链接队列
**会提到一个问题,就是两个队列是共享的吗?**两个队列不是共享的,它的节点是共享的
节点是一样的
进入这个状态之后,accept函数才得以处理。
他总共一起做两个事情,第一个从全连接队列里面取出一个节点出来,第二个分配fd
如果这个全连接这里面一个节点没有,我们调用accept这个函数怎么处理?
这时候会有一个条件等待,就是条件等待阻塞的过程,等待的这个全连接队列里面有一个节点
如果我们这个 fd设置为非阻塞的那也就是判断完这个条件队里面为空,那就直接返回了,返回一个-1
还是那句话,三次握手发生在哪一个函数里面?
如果这个半连接队列存在的话,这个东西它已经进入到一个listen的状态。
三次握手是发生在哪个函数里?
广义上三次握手,就发生在客户端connect里面,而服务端注意,服务端既不在listen,也不是accept,
是进入listen之后,三次握手完成之后,在调用accept函数
3次握手是说的协议栈和协议栈之间的操作,协议栈被动去实现它
有两个小疑问。
从这个半连接队列里面三次握手,最后这一次过来的时候,从这个半连接队列里面怎么找到这个客户端特定的节点,这是第一个问题?
第二个小疑问,就是这里的这个节点,他在什么时候释放,他的生命周期有多长?
通过什么找到对应客户端,就是这个节点就等同于跟客户端它是一对一的,也就等于客户端
这个问题应当从包头里面解析出来,没错就是5元组
我们的端口只有6535个,那为什么我们那个连接能做到100万?那怎么做的?
也是跟那个5元组也有关系,就是 fd它对应的就是5元组,那 fd怎么对应这个五元组呢?
就在accept()这个过程中间,它返回了一个fd,是accept()从这个节点里面拿出来一个节点,是通过这个五元组来找到它
源端口不同,这是第一个问题,组合到一起情况就很多
每一个TCP的包协议头里面都有,原端口号和目的端口号,IP地址里面有原IP和目的ID,所以就可以解析出来这个5元组。5元组至关重要,网络连接每一个包每一个连接操作系统怎么找到对应的fd,就是通过5元组
seq它是验证双方是否合法,不是验证那个节点是否合法,如果现在双方不合法,那这个节点根本就不把它丢进来
好,第二个问题就这个节点的生命周期有多长?
第一,就是到底有没有听过TCP的那11个状态,那个状态存在哪里?
存到这个节点里面,这里的这个节点它是伴随着整个一个连接的生命周期,
它不是fd的生命周期,fd的生命周期是在accept()里面创建的,
但是这个节点是先于fd的,这个节点已经有了,fd还没有,fd是在accept之后分配的,但是这个节点早就已有了
这个节点,也有另外一个高大上的名字叫做TCP控制块,英文名叫做tcb。
listen这个函数就是把对应这个 fd置为一个listen的状态,可以进行三次握手
udp的并发怎么做?
很有可能一帧数据夹杂的其他的数据
你在发送的这个过程中间肯定有一些分包的问题,
第一种策略,我们通过udp上面设计一些协议,设定一些传输的控制性,我们来做这个事情。
就是在sendto的buffer里面我们加入一些协议能不能解决问题?
是不可以的。
如果只通过send to发送在同一个端口上面,刚刚说到这个问题,是没办法去做一个包把它区分开来的。
也就是a的包里面发b的包,接收这个数据的时候,你很难区分清楚,
从这么多个客户端接收数据进来,它很有可能出现数据的乱序
那我们下一节课再给大家解释清楚这个 Udp的并发是怎么做的。
发送数据都会在这一个recvbuffer里面,所以就出现一种脏数据现象,通过我们buffer加协议头这个问题他是解不了的,因为你多个数据只在这一个recvbuffer里面解出来
可以采用TCP的方式,用udp去模拟TCP的方式。
只做第一次,也就是说在客户端跟服务器建立连接的时候,不叫建立连接,发送数据包的第一次的时候这个也要配合的应用协议,发送第一帧数据的时候,
这个 fd–>recvbuffer接收第一帧数据的时候,接收到一个新的客户端来了,然后对应应用程序上面分配一个新的fd,以及一个对应的端口号,再send出来,也就出现了一种现象,一个fd会对应一个客户端
第一步
就是recvfrom接收到一个客户端信息,然后拿出它的IP地址和端口。
第二步
新建一个fd,然后调用sendto,
相比较而言,此时可能从一个微观的角度来说,从这一次session来说,你就可以在这个过程中把这一次当做一个客户端,那这个过程中间就出现了一个fd对应一个客户端这样来解,这是在底层上面一定要做到这样才能够去解决这个 udp处理并发的问题
重新走一个端口也可以走我们之前的端口发出去也是ok的,但是我们要走另外一个fd
kcp只是在TCP这个框架上面,做了一层应用协议
就在传输数据包是怎么样,传输数据包是怎么样这个过程。 kcp是纯算法的
有两个问题,
一个是客户端fd=connect()成功,但是服务端一直不accept(),调不调用accept()这条道路都是通的,客户端是可以写,只是服务端不能收到而已,
第二就是这个半链接的节点对象,除了保存11个状态以外,还保留send/recv缓存
第二个就是数据传输
tcp的传输过程
第一个问题。
TCP它是如何保证顺序?
TCP是一个流式套接字,什么叫流呢?就是先发的先到,后发的后到就是保证顺序的
那这个顺序的过程中间, TCP它又是如何保证顺序?
在讲这个之前先举一个例子,
比如说
现在有一个蓝框,有一个球场有一个框,这个框里面呢放了100个球,标号从0~100,然后这100个球呢把它放到另外一个100米远的地方,
把它放过去,然后这么把它发过去,那怎么保证它顺序?
这里有这么几个问题,
第一个问题,
在发送的过程中间,这是一个最简单最简单的方法,
第一个方法就是把1号包发完之后,对方确认接收了,发送第二个,第二号包发送完之后,对方确认,发送第三个,这样他肯定保证是顺序的,
比如说我们现在先为每个包编一个号,就是比如说这100个球,我们先把第一个球送给对方对方接收到了,然后返回一个确认值,然后这边才发送二号包。
就是第二个球接收到了再发第三个球这样一个过程,这样一个过程接收的肯定是正确的,
这种方法它保证了顺序,但是它的效率极低,它的效率是不高的,那怎么办?
就会出现一种现象,就第一个包发出去之后,第二个包
这是第一号发出去,
可能没有返回确认。
第二个
也发了,第三个也发了,
第四个包也在路上,
然后再来去确认,应该是这样一个状态,这才是一个高效的,这才是一个有那么一点点智能的,它才能够保证这个数据的这个高效性,
这里面就有一个问题,问题在哪?
既然多个包同时在网络上传输的时候,这时候我们可能在网络上在公网上传输我们是很难保证,
先发的先到后发的后到,
刚刚说的TCP里面先发的先到后发的后到,这是在我们应用程序已经接收到数据的时候,它是先receive的是先发,后receive的后到,但我没接收之前在网络上面传输的时候,网络上面是一个极其复杂的情况,那这个过程我们是很难保证,先发的数据先到后发的数据后到。
没办法确定哪个包先到达对方这个机器上面的。
这个顺序是没法确定的,
这就是一个重排的过程。
比如这边是1234。
这边1号2号4号,3号没有收到,
这时候有一个机制叫做超时重传,
每接收到一个包,这时候协议栈会启动一个定时器,启动一个200毫秒的定时器,
在接收一号包的时候重置这个定时器,再次启动200毫秒的定时器,在他接收4号的时候再次重置,一旦超时一旦超时就开始检测这些包的顺序到底哪个没到,
第一个词超时重传就是从小到大依次验证,
找到比如说这个3号包没有的话,那就重新从3号包以后全部重发,4号包也进行重发,就这么一个情况。
这就是tcp能保证,最小的接收的是顺序的,至少1和2是顺序的3,4重发的时候再次保证顺序
那这个过程中间大家可以看到TCP的一些缺点,
第一个确认时间周期长,
第二个重发的时候,比如说3号没有收到4号包也会进行重发,即使收到也会进行重发,那其实这个过程当中应该理解重发的比例,已经收到的就是重发的这个次数有点多。
为什么4号包也要重发?
TCP给已经接收到的这个对端,
就是这个头
这个确认消息ack它只有一个数字,
这个 Ack的意思可以确认多少号包以前的全部收到了,后面的包没有收到,是这个意思,这个 ack这个值
它只是一个数字,在这个设计协议本身的时候就已经决定了,那是没办法,
比如说我们再多几个包,
我们很难表示3号和5号收到,4号和6号没有收到,协议头我们是表示不出来。
这就是由于TCP协议本身的一些缺陷,
那也就是udp在这个基础上面,所以大家所理解的kcp也是在这个基础上面解决这些问题,就比如说它的确认时间,那我们可以做到收到一个包就确认一次,这样的话它的实时性会更强。
第二个就是关于这个重发的次数,我们可以
比如说在一次发送中我们告诉对方我们有哪些包中间可以没有收到,这也可以做,那也是可以从一定的线路上面去优化TCP。
TCP在传输的过程中间只要理解这一个点就是tcp如何保证顺序的,解理解这一点就ok,
然后还有一个过程,
这一个包一个包的发送,我们怎么能够进行
我们一次发送,我们可以传出4个或传出更多的怎么做,它是一种什么样的规律,那我们怎么能够做到?
刚刚发一个包,我们刚刚那个状态怎么就可以变到在网上发多个包以及这多个包是怎么计算的?
这里要跟大家讲到的一个概念就是所说到的慢启动,
比如第一次从一开始的时候我们先发一个包,
第二次
我们在我们第一个包发送出去,我们能够准确的在规定的时间里面收到了ACK,
那我们第二次就发2个包,第三次就发4个包,
第四次8个包
这个慢启动并不慢,这个慢启动是它的初始值比较小,然后需要从一点点一点点的加起来,
并不是一开始设立一个很大的数没有,然后在这个过程中间到底涨到多少合适
线性增长这个过程中间,一旦超出要把它降一半,
前面那段叫做慢启动,后面这一段叫做拥塞控制
这个函数send
只做到一点,他跟我们数据成不成功一点关系都没有,send成功了压根就不能够去决定我们数据有没有成功
他只能去决定这个应用程序的数据把它放在协议栈里面,
如何保证顺序的?数据包怎么发的?
send这个函数跟write这个函数一样的,他唯一还做的一个事情就是copy_from_user
从用户空间里面把这个数据copy到协议栈里面,放到sendbuffer里面
第三个讲的是断开连接
断开连接的过程对于应用程序而言,两个接口可以做
一个是close
另外一个是shutdown
在这一堆函数里面,
站在一个网络传输的角度接和收,
要么接要么收
我们调用哪一些接口,协议栈它会发送数据出去。
相当于一个广义上的send
就这4个它都有1个发送的过程。
调用close时候,应用程序上调用close看似只是一个fd,
他没有发送,不需要copy任何数据,也就是在这sendbuffer里面,我们准备一帧里面带有fin结束标志的这个包把它发送出去,
那这4次挥手的过程
因为他是个比较复杂东西,
是TCP里面就是状态比较多的地方
这是我们调用close,会发送一帧fin,接收完之后这里有ACK,
然后在这个阶段里面我们再调用cloes,
两边是对称的,TCP是双向通信的,
这个fd即可接又可收,
调用close这个过程,就是关闭了它的发送,就是调用close之后它这边是不能发送
但是在广义上来说,另外这边它不能收了,没有数据可收了。
所以这里面有两个东西epollhup,epollrdhup
这两个状态里面有这个问题
为什么没有一个 epollwhup为什么没有写?
写这个东西在这个逻辑里面它是多余的
**为什么没有epollwrhup这个东西。**而是有epollhup和epollrdhup
这边是读关闭了,站在这一端而言,接收到了fin,
也就是对应如果两端都关闭了,也就是对应来说自己也发送出去close,并且接收到这个 ACK这一步完了之后,这个 fd如果还在里面,它就会epollhup这个状态
第一个就是大量的time_wait,大量的close_wait
第二个就是大量的fin_wait1,fin_wait2
这也是经常在面试中经常会问到的,第一个出现大量time_wait怎么办?
主动方调用close才有time_wait,
是最先调用close的时候他才有time_wait
其实这个服务端而言出现了大量time_wait,其实这是一个非常不正常的现象,
也就是说你的服务器逻辑代码里面应该是出现了主动调用close的现象。
第一个原因,自己代码里面主动调用了close
首先第一查自己逻辑,自己调用close地方是不是正常的,
如果是正常的,
可以通过setsockopt()把它设置为一个reuse,就把它设置为重用。
第二个如果出现大量close_wait
0=recv()再去close
其实在这个现象是因为调用close延迟
可能在这个过程中,就是知道对方已经关闭了,知道一个客户端主动断开连接之后,需要去释放服务,这个客户端对于一些业务一些逻辑信息。
所以在这个逻辑处理的时间上面可能有点长
所以就会把这个close延迟之后
就是看你调用close的地方,是不是在正确的时机
我怎么知道是不是正确时机
就是recv返回0之后
这时候
应该是立即调用close
对客户业务代码的解析以及释放可以抛给另外一个线程,这里边另外一个线程去释放业务代码,不应该在这个流程处理异步处理
这个fin_wait2有没有方法终止,
就是现在进入了一个fin_wait2的状态,
能不能够终止?
等到对方,对方一直都不关闭,所以这个中间有没有方法去中断?
没有
你能做的事情都已经做完了,剩下的只有一个事情,
你能强行禁止,只能通过kill
从TCP的状态迁移图上
压根就没有进入到这个close的状态,也就是这个过程中间,
你就不会再次重新去分配,
那要我重新再去分配它,而是拿着之前用的东西拿来用。
提供出来重用,能够从一大一定限度上面减少这个time_wait,