c++后台开发面试常见知识点总结(二)网络编程
(1)TCP和UDP有什么区别?
TCP是传输控制协议,提供的是面向连接的,可靠地字节流服务。使用三次握手建立连接,四次挥手释放连接。UDP是用户数据报协议,传输的是UDP数据报,是无连接的,而且没有超时重发机制。 TCP保证数据按序到达,提供流量控制和拥塞控制,在网络拥堵的时候会减慢发送字节数,而UDP不管网络是否拥堵。 TCP是连接的,所以服务是一对一服务,而UDP可以1对1,也可以1对多(多播),也可以多对多。
TCP可靠传输的保证:
停止等待协议:每发送完一个分组就停止发送,等待对方确认。收到确认后再发送下一个分组。无差错情况下收到确认再发送。如果发送方超过一段时间仍没有收到确认就会重新发送(超时重传)。因此发送完必须保留分组副本,分组和确认分组都必须编号,超时计时器应该比平均往返时间长一点。
连续ARQ协议:发送方每收到一个按序到达的确认,就把发送窗口向前移动一个分组的位置。
滑动窗口机制:窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
UDP如何实现可靠传输:
在应用层模仿传输层TCP的可靠性传输。不考虑拥塞处理的简单设计。1、添加seq/ack机制,确保数据发送到对端。2、添加发送和接收缓冲区,主要是用户超时重传。3、添加超时重传机制。
注:1、发送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x+1的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。2、时间到后,定时任务检查是否需要重传数据。
(2)建立和释放TCP/IP过程。
三次握手过程:TCP提供的可靠数据传输服务,是依靠接收端TCP软件按序号对收到的数据分组进行逐一确认实现的。三次握手协议指的是在发送数据的准备阶段,服务器端和客户端之间需要进行三次交互:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的syn(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,服务器收到此包,客户端和服务器进入ESTABLISHED状态,完成三次握手。
三次握手的原因:第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
四次挥手: 数据传输结束后,通信双方都可以释放连接。现在客户端和服务器都处于ESTABLISHED状态,客户端应用进程向其TCP发出连接释放报文段,主动关闭TCP连接。客户端进入FIN_WAIT1(终止等待1)状态。然后服务器立刻确认,服务器进入CLOSE_WAIT(关闭等待)状态。此时TCP处于半关闭状态,客户端已经没有数据要发送了,如果服务器仍要发送数据,客户端仍然接收。客户端收到服务器的确认后,就进入FIN_WAIT2(终止等待2)状态,等待服务器发出连接释放报文。 如果服务器已经没有向客户端发送的数据,则服务器发送请求释放报文,服务器进入LAST_ACK(最后确认)阶段,等待客户端的最后确认。客户端在收到服务器的请求后,要发出确认,然后进入TIME_WAIT(时间等待)状态。此时,连接还未释放,必须等待2MSL后,客户端才进入CLOSED状态。服务器收到客户端最后的确认,进入CLOSED状态,连接释放。
四次挥手的原因:客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
TIME_WAIT:客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
1. 确保最后一个确认报文段能够到达。如果 服务端 没收到 客户端 发送来的确认报文段,那么就会重新发送连接释放请求报文段,客户端 等待一段时间就是为了处理这种情况的发生。
2. 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段。
(3)抓包分析发现服务器保持了大量CLOSE_ WAIT 和TIME_WAIT套接字
在服务器的日常维护过程中,会经常用到下面的netstat命令查看TCP套接字的状态:
TIME_WAIT 814 CLOSE_WAIT 1 FIN_WAIT1 1 ESTABLISHED 634
SYN_RECV 2 LAST_ACK 1
常用的三个状态是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。
因为linux分配给一个用户的文件句柄是有限的,而TIME_WAIT和CLOSE_WAIT两种状态如果一直被保持,那么意味着对应数目的通道就一直被占着,一旦达到句柄数上限,新的请求就无法被处理。
服务器保持了大量TIME_WAIT状态的套接字:TIME_WAIT是主动关闭连接的一方保持的状态。 对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可 想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,维护这些状态给Server带来负担。
原因:大量出现这个状态往往说明系统的并发比较高,大量的连接建立和释放,
处理:优化系统内核参数让服务器能够快速回收和重用那些TIME_WAIT的资源。
服务器保持了大量CLOSE_WAIT状态的套接字:
原因:Server 程序处于CLOSE_WAIT状态,而没有跳转到LAST_ACK状态,说明Server还没有发FIN给Client,那么可能是在关闭连接之前还有许多数据要发送或者其他事要做,导致没有发这个FIN package。通常一个CLOSE_WAIT会维持至少2个小时的时间。如果有个恶意程序,给服务器造成一堆的CLOSE_WAIT,通常是等不到释放那一刻,系统就已经崩溃了。
解决:修改TCP/IP的参数,来缩短2个小时这个时间:修改tcp_keepalive_*系列参数有助于解决这个问题。
5.IO模型:
阻塞IO,非阻塞IO,IO复用,信号驱动式IO,异步IO
同步IO和异步IO,
阻塞IO和非阻塞IO。
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中(),然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:1. 等待数据准备 2.将数据从内核拷贝到进程中
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
5.1. 阻塞IO的特点就是在IO执行的两个阶段都被block了。
5.2.非阻塞 IO的特点是用户进程需要不断的主动询问kernel数据好了没有。如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call(read),那么它(read)马上就将数据拷贝到了用户内存,然后返回。
5.3. I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。阻塞IO第一阶段是阻塞在recvfrom, . I/O 多路复用第一阶段是阻塞在select.
5.4. 异步 I/O不阻塞,用户进程发起read操作之后,立即返回,立刻就可以开始去做其它的事。不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
5.5. 信号驱动式IO
同步I/O和异步I/O: 同步I/O 需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
6. select,poll,epoll的实现和区别
select:用户进程调用select函数后会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可)函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
优点:select目前几乎在所有的平台上都支持,有良好的跨平台支持。
缺点:1. 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。2. 轮寻排查当文件描述符个数很多时,效率很低。poll通过一个可变长度的数组解决了select文件描述符受限的问题,但轮寻排查的问题未解决。epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
Poll: poll通过一个可变长度的数组解决了select文件描述符受限的问题,但轮寻排查的问题未解决。
epoll: 没有描述符限制。采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。
LT模式是默认模式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
Epoll的优势:1. 监视的描述符数量不受限制。2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过为每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。所以epoll只返回状态发生变化的文件描述符。每个描述符就绪时自己调用回调函数上报给epoll。
epoll实现源码:(红黑树+就绪队列),为什么用红黑树实现,有哪些好处 ?
红黑树存储epoll所监听的套接字。当添加,删除或者修改(查找)一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
List链表用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
EPOLLONESHOT:
背景:多线程环境下,一个SOCKET事件到来,数据开始解析,这时候这个SOCKET又来了同样一个这样的事件,而你的数据解析尚未完成,那么程序会自动调度另外一个线程或者进程来处理新的事件,这造成一个很严重的问题,不同的线程或者进程在处理同一个SOCKET的事件,这会使程序的健壮性大降低而编程的复杂度大大增加!!即使在ET模式下也有可能出现这种情况。如果对描述符socket注册了EPOLLONESHOT事件,那么操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次。。想要下次再触发则必须使用epoll_ctl重置该描述符上注册的事件,包括EPOLLONESHOT 事件。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 。
7.流量控制和拥塞控制
流量控制是为了控制发送方发送速率,保证接收方来得及接收。拥塞控制是为了控制发送方发送速率降低整个网络的拥塞程度。拥塞控制4种算法:1.慢开始与拥塞避免 2.快重传与快恢复。
8. TCP协议是如何保证可靠传输的?如何用udp封装实现tcp ?
TCP通过序列号、检验和、确认应答信号、重发控制、连接管理、窗口控制、流量控制、拥塞控制实现可靠性。
具体表现:针对发送端发出的数据包序列号的确认应答信号ACK;针对数据包丢失或者出现定时器超时的重发机制;针对数据包到达接收端主机顺序乱掉的顺序控制;针对避免网络拥堵时候的流量控制;针对刚开始启动的时候避免一下子发送大量数据包而导致网络瘫痪的拥塞控制。
此外,TCP作为一种面向有连接的控制传输协议,只有在确认对端主机存在时才会发送数据,从而可以控制通信流量的浪费。
udp封装实现tcp:
UDP不提供复杂的控制机制,不提供可靠传输机制,是尽最大能力交付的。在传输过程中如果出现丢包,UDP也不负责重发,甚至当数据包的到达顺序乱掉之后也没有纠正顺序的功能。但具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
如果需要这些细节控制的话,就需要在采用UDP协议的应用层去作出处理。在应用层模仿传输层TCP的可靠性传输。目前有开源程序利用udp实现了可靠的数据传输。如UDT(基于UDP的数据传输协议(UDP-basedData Transfer Protocol,简称UDT))
9.浏览器访问网站的一次完整过程?浏览器中输入一个URL发生什么,用到哪些协议?
1.DHCP 配置主机信息:
如果主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。DHCP是基于UDP的,DHCP服务器是连接在交换机上的,不跨路由;在返回DHCP应答报文的时候经过交换机,用到了交换机的自学习能力。DHCP 配置了主机的IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。
2.DNS 解析域名
浏览器中输入URL,首先浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。
DNS服务器是基于UDP的。DNS协议是跨路由的(DNS服务器可能会在路由外)。域名解析过程会需要网关路由器的 MAC 地址,而DHCP过程中只知道网关路由器的IP地址,所以会用到ARP协议,解析IP地址得网关路由器的MAC地址(广播的方式)
3. HTTP 请求页面
有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。
TCP的数据包会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要知道目的IP地址的物理地址(MAC地址),有需要ARP协议。
10.DDOS,怎么解决,如何让Server端收到ACK后再分配资源,不改变Client,不封装IP数据包?
DDOS :SYN Flood是经典的DDoS攻击方式,SYN Flood攻击利用了TCP三次握手的缺陷,能够以较小代价使目标服务器无法响应。
TCP协议为了实现可靠传输,在三次握手的过程中设置了一些异常处理机制。第三步中如果服务器没有收到客户端的最终ACK确认报文,会一直处于SYN_RECV状态,将客户端IP加入等待列表,并重发第二步的SYN+ACK报文。重发一般进行3-5次,大约间隔30秒左右轮询一次等待列表重试所有客户端。另一方面,服务器在自己发出了SYN+ACK报文后,会预分配资源为即将建立的TCP连接储存信息做准备,这个资源在等待重试期间一直保留。更为重要的是,服务器资源有限,可以维护的SYN_RECV状态超过极限后就不再接受新的SYN报文,也就是拒绝新的TCP连接建立。
SYN Flood正是利用了上文中TCP协议的设定,达到攻击的目的。攻击者伪装大量的IP地址给服务器发送SYN报文,由于伪造的IP地址几乎不可能存在,也就几乎没有设备会给服务器返回任何应答了。因此,服务器将会维持一个庞大的等待列表,不停地重试发送SYN+ACK报文,同时占用着大量的资源无法释放。更为关键的是,被攻击服务器的SYN_RECV队列被恶意的数据包占满,不再接受新的SYN请求,合法用户无法完成三次握手建立起TCP连接。也就是说,这个服务器被SYN Flood拒绝服务了。
解决:修改内核参数:启用SYN Cookie;设置SYN最大队列长度;设置SYN+ACK最大重试次数;
启用SYN Cookie:SYN Cookie的作用是缓解服务器资源压力。启用之前,服务器在接到SYN数据包后,立即分配存储空间,并随机化一个数字作为SYN号发送SYN+ACK数据包。然后保存连接的状态信息等待客户端确认。启用SYN Cookie之后,服务器不再分配存储空间,而且通过基于时间种子的随机数算法设置一个SYN号,替代完全随机的SYN号。发送完SYN+ACK确认报文之后,清空资源不保存任何状态信息。直到服务器接到客户端的最终ACK包,通过Cookie检验算法鉴定是否与发出去的SYN+ACK报文序列号匹配,匹配则通过完成握手,失败则丢弃。
设置SYN最大队列长度是使用服务器的内存资源,换取更大的等待队列长度,让攻击数据包不至于占满所有连接而导致正常用户无法完成握手。
设置SYN+ACK最大重试次数是降低服务器SYN+ACK报文重试次数,尽快释放等待资源。
这三种措施与攻击的三种危害一一对应,完完全全地对症下药。但这些措施也是双刃剑,可能消耗服务器更多的内存资源,甚至影响正常用户建立TCP连接,需要评估服务器硬件资源和攻击大小谨慎设置。
11.blocking(默认)和nonblock模式下read/write行为的区别:
将socket fd设置为nonblock(非阻塞)是在服务器编程中常见的做法,采用blocking IO并为每一个client创建一个线程的模式开销巨大且可扩展性不佳(带来大量的切换开销),更为通用的做法是采用线程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。
结论:
1. read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)
2. blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)
3. 对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败。这是read/write对连接异常的一种反馈行为。
read/write对连接异常的反馈行为:
对应用程序来说,与另一进程的TCP通信其实是完全异步的过程:1. 我并不知道对面什么时候、能否收到我的数据也不知道什么时候能够收到对面的数据。2. 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)
对于1采用write() -> read() -> write() -> read() ->...的序列,通过blocking read或者nonblock read+轮询的方式,应用程序可以保证正确的处理流程。
对于2,kernel将这些事件的“通知”通过read/write的结果返回给应用层。
12.socket网络编程有哪些系统调用?
TCP客户:socket() ->connect()->write()->read()->close()
TCP服务器:socket()->bind()->listen()->accept()->fork()->read()/write()->close()
socket():
int socket(AF_INEF,SOCK_STREAM,0);//创建一个IPv4/ IPv6的TCP套接字。
connect():客户用connect()建立与TCP服务器的连接。
int connect(int sockfd,const struct sockaddr *seraddr,socklen_t addrlen)
在创建套接字,用对端服务器的IP地址和端口号(通过main命令行参数指定)填充struct sockaddr之后。客户用connect()建立与TCP服务器的连接。
TCP连接的过程是由内核完成,connect()的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接. 连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)客户端的 connect() 函数默认会一直阻塞(在连接期间),直到三次握手成功或超时失败才返回。
Connect()失败原因:1. TCP客户在多次连接请求分节后一直接受不到服务器对SYN分节的响应,则返回ETIMEOUT错误。
2.若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(服务器也许还没有运行)。这是一种硬错误
3.若客户的SYN在中间的某个路由器上引发一个路由不可达ICMP错误,客户主机内保存该消息,并重发SYN分节。
Connect()失败后套接字不可再用,必须关闭,不能再对这样的套接字调用Connect()重新连接请求。
bind()
int bind(int sockfd,const struct sockaddr *seraddr,socklen_t addrlen)
bind()把填入到套接字地址结构中的本地协议地址(比如IP地址+TCP端口)赋予一个套接字.
如果一个TCP客户或服务器未曾用bind()捆绑一个端口,当调用Connect()或Listen()时内核就为相应的套接字选择一个临时端口。客户端通常都是由内核就为相应的套接字选择一个临时端口。服务器端通常需要用bind()捆绑一个众所周知端口。
进程可以把一个特定的IP地址绑定到它的套接字上,但是这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户,就为该套接字上发送的数据报指派了源IP地址。对于TCP服务器,这就限制了该套接字只接受那些目的地址为指定IP地址的客户连接。TCP客户通常不把IP地址捆绑到它的套接字上。当连接套接字时内核将根据所用外出网络接口来选择源IP地址,所用外出接口取决于到达服务器所需的路径。如果TCP服务器没有吧IP地址捆绑到它的套接字上,内核就把客户发送的SYN的目的IP地址作为服务器的源IP地址。
Listen():
int Listen(int sockfd,int backlog)
listen函数仅由服务器调用,它做两件事情:1.当socket函数创建一个套接字时,它被假设成一个主动套接字,是一个将调用connect发起连接的客户套接字。Listen函数把一个未连接的套接字转换成一个被动等待连接的监听套接字,指示内核应接受指向该套接字的连接请求。调用listen导致套接字从CLOSED状态转化为LISTEN转态。
2.本函数第二个参数backlog规定了内核应该为相应套接字队列的最大连接数目。
内核为一个监听套接字维护两个队列;
未完成连接队列:客户发送SYN请求分节,服务器接受后这些套接字处于SYN_RCVD状态,等待三次握手的完成。Listen函数不会阻塞。
已完成连接队列:每个已完成连接的客户对应其中的一项。套接字处于ESTABLISHED状态。两队列之和不超过backlog。
在三次握手建立连接的过程中,服务器收到客户端的连接请求分节SYN就在未完成连接队列中创建一项,当连接完成后,未完成连接的对应条目转移到已连接队列中。accept能够返回。
Ddos攻击原理和listen(): .listen()有一个队列,处理连接请求。在收到匿名IP发过来的SYN之后,会在listen队列中存放一个记录,但是队列容量是有限的,当这样的恶意请求过多的时候,listen队列里就塞满了这些无效的连接请求,然后装不下更多的连接记录了,所以就拒绝其他请求了。
accept ()
int accept (int sockfd, struct sockaddr *cliaddr,socklen_t * addrlen);
当进程调用accept时,已完成连接队列中的对头项将返回给进程,或者如果该项为空,那么进程将被投入睡眠,直到TCP在已连接队列中放入一项才唤醒它。
调用accept内核将已连接的对端进程(客户)的协议地址装填进地址结构中,利用值结果返回参数返回客户端的协议地址。
如果acccept成功,那么其返回值是由内核自动生成的一个全新套装字描述符,套接字绑定到装填了对端协议地址的套接字地址结构上。在accept中,它的第一个参数是监听套接字描述符,它的返回值为已连接套接字。
监听套接字与已连接套接字:一个服务器通常只创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字。当服务器完成对某个给定的服务时,相应的已连接套接字就被关闭。
fork ()和exec()
pid_t fork (void);
fork()是进程创建的唯一方式。TCP中在父进程调用accept()获得一个已连接套接字,然后调用fork()创建一个子进程,用来处理每个连接。父进程用来继续监听其他的客户连接。
父进程中调用fork之前打开的所有的描述符在fork返回之后,复制到子进程中由子进程共享。子进程关闭复制过来的监听套接字(此时父进程中的监听套接字并没有关闭),接着读写这个已连接套接字。父进程关闭已复制给子进程的已连接套接字。子进程中调用相应的函数处理客户的连接请求。
fork()的两个典型用法:1.一个进程创建自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的操作。这是网络服务器的典型用法。2.一个进程想要执行另一个程序。既然创建新进程的唯一办法是调用fork(),该进程于是首先调用fork()创建一个自身的副本,然后其中一个副本(通常是子进程)调用exec
把自身替换成新的程序。这是诸如shell之类程序的典型用法。
exec():
存放在硬盘上的可执行文件能够被Unix执行的唯一方法是:由一个现有进程调用6个exec中的某一个。exec把当前进程映象替换成新的程序文件,而且该新程序通常是从main函数开始执行。进程ID并不改变。
close()和shutdown()
close: close一个套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read()和write()的第一个参数。然后TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
描述符引用计数:并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数减1,如果仍然引用计数大于0,这个close调用并不会引发TCP终止序列。如果确实想在某个连接上发送一个FIN,而不管引用计数,可以改用shutdown函数代替close函数。
父进程对每个由accept返回的已连接套接字必须调用close,否则父进程最终将耗尽可用描述符。更重要的是,没有一个客户连接会被终止,因为当子进程关闭已连接套接字,它的引用计数将由2减到1,且保持为1,因为父进程永不关闭任何已连接套接字了,这将妨碍TCP终止序列的发生,导致连接一直打开着。
Shutdown:
调用close终止TCP连接有两个限制:1。Close只是把套接字的引用计数减1,如果需要立刻关闭套接字,不管引用计数,可以改用shutdown函数代替close函数。
2.close一个套接字后,该套接字描述符不能再由调用进程使用,不能再作为read()和write()的第一个参数。即close同时终止读和写两个方向的数据传递。既然TCP连接是全双工的,那经常需要在不需要发送数据关闭发送这一端的时候仍然可以接收对方的数据。使用shutdown函数可以达到。
int shutdown(int sockfd, int howto)
shutdown函数的行为依赖于howto的值;
SHUT_WR 关闭连接的写这一半。对于TCP套接字这称为半关闭状态。当前留在套接字发送缓冲区中的数据将被发送,后跟TCP的正常终止序列。不管套接字的引用计数是否为0,这样的半关闭照样执行。进程不能再对这样的套接字调用任何函数。
SHUT_RD关闭连接的读这一半。套接字中不再有数据可以接收,而且套接字接收缓冲区现有的数据全部被丢弃。套接字接收的来自对端的任何数据都被确认,然后丢弃。进程不能载对这样的套接字调用任何函数。
getsockname()和getpeername()
返回与某个套接字关联的本地协议地址或者外地协议地址。
适用场景:
1.在一个没有调用bind的TCP客户上,connect成功返回后,getsockname()用于返回由内核赋予该连接的本地IP地址和本地端口号。
2.在以端口号0调用bind后(告知内核去选择本地端口号),getsockname()用于返回由内核赋予的本地端口号。
3.getsockname()用于获取某个连接的地址族。
4.在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立,(accept 成功返回),getsockname()可以用于返回由内核赋予本连接的本地IP地址。
在这样的调用中,套接字描述符必须是已连接套接字描述符而不能是监听套接字描述符。
5. 当一个服务器程序是由调用过accept的某个进程通过exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername()。Inned超级服务器fork并exec某个服务器程序时就是如此情形。
Inned 调用accept返回已连接套接字描述符和客户的IP地址端口号。 Inned 随后调用fork,创建一个子进程,子进程起源于父进程的内存映像的一个副本,父进程中的那个套接字地址结构和已连接描述符都复制到子进程中,然后当子进程exec执行某个服务器程序时,子进程的内存映像就被替换成服务器程序文件,包含之前的对端地址的那个套接字地址结构就此丢失。此时调用getpeername()可以获得客户的IP地址和端口号。
read()和write()
write成功返回只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。
write在什么情况下会阻塞?
对于每个socket,拥有自己的send buffer和receive buffer,当kernel的该socket的发送缓冲区已满时,write会阻塞。
已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。
一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞。
read调用从socket的receive buffer中拷贝数据到应用程序的buffer中。read调用阻塞,通常是发送端的数据没有到达。
fork、vfork以及clone的区别,exec的作用
fork、vfork、clone是Linux用来创建新的进程(线程)而设计的。exec()系列函数则是用指定的程序替换当前进程的所有内容。exec()系列函数经常在前三个函数使用之后调用,来创建一个全新的程序运行环境。Linux用init进程启动其他进程的过程一般都是通过exec()系列函数实现的。fork、vfork、clone分别调用了sys_fork、sys_vfork、sys_clone,最终都调用了do_fork函数,差别在于参数的传递和一些基本的准备工作不同。可见这三者最终达到的最本质的目的都是创建一个新的进程。Linux内核中没有独立的“线程”结构,Linux的线程就是轻量级进程,线程的基本控制结构和Linux的进程是一样的(都是通过struct task_struct管理)。
fork是最简单的调用,不需要任何参数,仅仅是在创建一个子进程并为其创建一个独立于父进程的空间。fork使用COW(写时拷贝)机制,并且COW了父进程的栈空间。
vfork是一个过时的应用,vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。
clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。clone和fork的调用方式也很不相同,clone调用需要传入一个函数,该函数在子进程中执行。此外,clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。
循环fork,问有多少个输出行的题
总打印出来了6个A,因为我们是先fork后打印。所以,在第1次循环,第1次打印A的时候,已经有一个增生的进程了,所以第1次打印共打印出2个A。在第2次循环的时候,最开始的进程与增生的进程各自又增生了一个进程,所以当前共有4个进程,打印出来了4个A。共有6个A。
网络通信出故障如何排查,讲讲fiddler,tcpdump
使用tcpdump抓包,并用Wireshark和Fiddler工具分析。排查问题的时候,进程要遇到抓包,如果是在windows环境,可以使用wireshark直接抓包,如果是在linux环境下,可以使用tcpdump命令进行抓包,然后取下来用wireshark或者Fiddler进行分析。tcpdump也可以用来抓udp的包。
https://blog.csdn.net/u014209205/article/details/81104908
紧急指针
TCP仅支持一个字节的带外(OOB)紧急数据。
TCP报文首部的紧急指针(urgent pointer)指向该带外紧急数据偏移的下一字节。
如发送方欲发送多字节的带外紧急数据,其结果是:紧急指针指向最后一个数据偏移的下一字节,而之前所有数据被当作普通数据处理。
即:发送端只把提供数据的最后一个字节当作带外紧急数据;接收端只会接收到一个字节的带外紧急数据
如何监听tcp丢包问题。(细节知识点)。分析丢包原因
用tcpdump抓包分析可以发现tcp丢包问题,如:
网络丢包原因分析:
1. 网络链路阻塞引起丢包。数据在网络传输的过程中会经过很多设备和网路链接。只要其中一个网路链接在数据传输过来之前已经满负载了,那么数据将会在这里阻塞一段时间,然后在经过网络链路传送(这也就是所谓的排队)。 如果说网络设备非常落后于这个网路链接的话,那么网路链接没有足够给新数据来等待的空间,只能将信息丢掉,发生丢包。
解决:增加阻塞链接的带宽;
2.网络设备(路由器,交换机)性能不够。当大量网络数据包传送到达网络设备,但是此时网络设备的CPU,或者内存满载了,并没有能力来处理其他的数据包。这导致设备不能处理的数据包都被丢弃。
解决:必须更换吞吐量更大,性能更好的网络硬件,或者构建集群来提高吞吐量。
tcp粘包问题,怎么处理?udp会粘包吗?为什么?
TCP粘包问题:TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。粘包可能由发送方造成,也可能由接收方造成。TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据,造成多个数据包的粘连。如果接收进程不及时接收数据,已收到的数据就放在系统接收缓冲区,用户进程读取数据时就可能同时读到多个数据包。因为系统传输的数据是带结构的数据,需要做分包处理。
TCP粘包处理-RingBuf方法:https://blog.csdn.net/hik_zxw/article/details/48398935
UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)。
在三次握手过程中第二条包丢了会怎么样?第三条丢了会怎样?有什么现象?
第二个ACK包丢了:客户端重发连接请求;
第三个ACK包丢了:客户端认为连接建立,写数据时,会触发RST。
服务器listen后不accept,客户端connect会返回吗。【可以,内核负责三次握手,维护一个已完成链接的队列,聊了一下已完成连接和未完成链接队列的问题】,