【系统软件工程师面试】2. 网络部分

网络部分

一、tcp/udp区别

二、tcp 三次握手/ connect/ accept 关系, read返回0

三、select/ epoll

ET/LThtml

在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
从字面上看, 意思是:EAGAIN: 再试一次,EWOULDBLOCK: 若是这是一个阻塞socket, 操做将被block,perror输出: Resource temporarily unavailablejava

总结:
这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种状况,若是是阻塞socket,read/write就要阻塞掉。而若是是非阻塞socket,read/write当即返回-1, 同时errno设置为EAGAIN。
因此,对于阻塞socket,read/write返回-1表明网络出错了。但对于非阻塞socket,read/write返回-1不必定网络真的出错了。多是Resource temporarily unavailable。这时你应该再试,直到Resource available。linux

综上,对于non-blocking的socket,正确的读写操做为:
读:忽略掉errno = EAGAIN的错误,下次继续读
写:忽略掉errno = EAGAIN的错误,下次继续写程序员

对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。web

 

epoll的两种模式LT和ET
两者的差别在于level-trigger模式下只要某个socket处于readable/writable状态,不管何时进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。面试

因此,在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN算法

正确的读apache

= 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
}
if (nread == -1 && errno != EAGAIN) {
    perror("read error");
}

 

正确的写编程

int nwrite, data_size = strlen(buf);
= data_size;
while (> 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
        if (nwrite == -1 && errno != EAGAIN) {
            perror("write error");
        }
        break;
    }
    n -= nwrite;
}

 

正确的accept,accept 要考虑 2 个问题
(1) 阻塞模式 accept 存在的问题
考虑这种状况:TCP链接被客户端夭折,即在服务器调用accept以前,客户端主动发送RST终止链接,致使刚刚创建的链接从就绪队列中移出,若是套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其余某个客户创建一个新的链接为止。可是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其余描述符都得不处处理。缓存

解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept以前停止某个链接时,accept调用能够当即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其余实现把errno设置为ECONNABORTED或者EPROTO错误,咱们应该忽略这两个错误。

(2)ET模式下accept存在的问题
考虑这种状况:多个链接同时到达,服务器的TCP就绪队列瞬间积累多个就绪链接,因为是边缘触发模式,epoll只会通知一次,accept只处理一个链接,致使TCP就绪队列中剩下的链接都得不处处理。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的全部链接后再退出循环。如何知道是否处理完就绪队列中的全部链接呢?accept返回-1而且errno设置为EAGAIN就表示全部链接都处理完。

综合以上两种状况,服务器应该使用非阻塞地accept,accept在ET模式下的正确使用方式为:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
}
if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
    perror("accept");
}

 

一道腾讯后台开发的面试题
使用Linuxepoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?

第一种最广泛的方式:
须要向socket写数据的时候才把socket加入epoll,等待可写事件。
接受到可写事件后,调用write或者send发送数据。
当全部数据都写完后,把socket移出epoll。

这种方式的缺点是,即便发送不多的数据,也要把socket加入epoll,写完后在移出epoll,有必定操做代价。

一种改进的方式:
开始不把socket加入epoll,须要向socket写数据的时候,直接调用write或者send发送数据。若是返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,所有数据发送完毕后,再移出epoll。

这种方式的优势是:数据很少的时候能够避免epoll的事件处理,提升效率。

四、timeout wait过多, 2MSL

  1. netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'    

它会显示例以下面的信息:

TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1

经常使用的三个状态是:ESTABLISHED 表示正在通讯,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。

若是服务器出了异常,百分之八九十都是下面两种状况:

1.服务器保持了大量TIME_WAIT状态

2.服务器保持了大量CLOSE_WAIT状态

由于linux分配给一个用户的文件句柄是有限的(能够参考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT两种状态若是一直被保持,那么意味着对应数目的通道就一直被占着,并且是“占着茅坑不使劲”,一旦达到句柄数上限,新的请求就没法被处理了,接着就是大量Too Many Open Files异常,

 

1.服务器保持了大量TIME_WAIT状态

这种状况比较常见,一些爬虫服务器或者WEB服务器(若是网管在安装的时候没有作内核参数优化的话)上常常会遇到这个问题,这个问题是怎么产生的呢?

从 上面的示意图能够看得出来,TIME_WAIT是主动关闭链接的一方保持的状态,对于爬虫服务器来讲他自己就是“客户端”,在完成一个爬取任务以后,他就 会发起主动关闭链接,从而进入TIME_WAIT的状态,而后在保持这个状态2MSL(max segment lifetime)时间以后,完全关闭回收资源。为何要这么作?明明就已经主动关闭链接了为啥还要保持资源一段时间呢?这个是TCP/IP的设计者规定 的,主要出于如下两个方面的考虑:

1.防止上一次链接中的包,迷路后从新出现,影响新链接(通过2MSL,上一次链接中全部的重复包都会消失)
2. 可靠的关闭TCP链接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会从新发fin, 若是这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。因此主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短期内接受大量请求或者受到攻击。

关于MSL引用下面一段话:

[plain]  view plain copy print ?
  1. MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你能够想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,所以我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其余連線使用。  

再引用网络资源的一段话:

[plain]  view plain copy print ?
  1. 值 得一说的是,对于基于TCP的HTTP协议,关闭TCP链接的是Server端,这样,Server端会进入TIME_WAIT状态,可 想而知,对于访 问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压 240*1000=240,000个 TIME_WAIT的记录,维护这些状态给Server带来负担。固然现代操做系统都会用快速的查找算法来管理这些 TIME_WAIT,因此对于新的 TCP链接请求,判断是否hit中一个TIME_WAIT不会太费时间,可是有这么多状态要维护老是很差。  
  2. HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP链接传输多个 request/response,一个主要缘由就是发现了这个问题。  

也就是说HTTP的交互跟上面画的那个图是不同的,关闭链接的不是客户端,而是服务器,因此web服务器也是会出现大量的TIME_WAIT的状况的。
 
如今来讲如何来解决这个问题。
 
解决思路很简单,就是让服务器可以快速回收和重用那些TIME_WAIT的资源。
 
下面来看一下咱们网管对/etc/sysctl.conf文件的修改:
[plain]  view plain copy print ?
  1. #对于一个新建链接,内核要发送多少个 SYN 链接请求才决定放弃,不该该大于255,默认值是5,对应于180秒左右时间   
  2. net.ipv4.tcp_syn_retries=2  
  3. #net.ipv4.tcp_synack_retries=2  
  4. #表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改成300秒  
  5. net.ipv4.tcp_keepalive_time=1200  
  6. net.ipv4.tcp_orphan_retries=3  
  7. #表示若是套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间  
  8. net.ipv4.tcp_fin_timeout=30    
  9. #表示SYN队列的长度,默认为1024,加大队列长度为8192,能够容纳更多等待链接的网络链接数。  
  10. net.ipv4.tcp_max_syn_backlog = 4096  
  11. #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少许SYN攻击,默认为0,表示关闭  
  12. net.ipv4.tcp_syncookies = 1  
  13.   
  14. #表示开启重用。容许将TIME-WAIT sockets从新用于新的TCP链接,默认为0,表示关闭  
  15. net.ipv4.tcp_tw_reuse = 1  
  16. #表示开启TCP链接中TIME-WAIT sockets的快速回收,默认为0,表示关闭  
  17. net.ipv4.tcp_tw_recycle = 1  
  18.   
  19. ##减小超时前的探测次数   
  20. net.ipv4.tcp_keepalive_probes=5   
  21. ##优化网络设备接收队列   
  22. net.core.netdev_max_backlog=3000   
[plain]  view plain copy print ?
  1.   
修改完以后执行/sbin/sysctl -p让参数生效。
 
这里头主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle 
net.ipv4.tcp_fin_timeout 
net.ipv4.tcp_keepalive_*
这几个参数。
 
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。
net.ipv4.tcp_fin_timeout这个时间能够减小在异常状况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。
net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测链接存活的相关配置。
 
2.服务器保持了大量CLOSE_WAIT状态
休息一下,喘口气,一开始只是打算说说TIME_WAIT和CLOSE_WAIT的区别,没想到越挖越深,这也是写博客总结的好处,总能够有意外的收获。
 
TIME_WAIT状态能够经过优化服务器参数获得解决,由于发生TIME_WAIT的状况是服务器本身可控的,要么就是对方链接的异常,要么就是本身没有迅速回收资源,总之不是因为本身程序错误致使的。
但 是CLOSE_WAIT就不同了,从上面的图能够看出来,若是一直保持在CLOSE_WAIT状态,那么只有一种状况,就是在对方关闭链接以后服务器程 序本身没有进一步发出ack信号。换句话说,就是在对方链接关闭以后,程序里没有检测到,或者程序压根就忘记了这个时候须要关闭链接,因而这个资源就一直 被程序占着。我的以为这种状况,经过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
 
若是你使用的是HttpClient而且你遇到了大量CLOSE_WAIT的状况,那么这篇日志也许对你有用: http://blog.csdn.net/shootyou/article/details/6615051
在那边日志里头我举了个场景,来讲明CLOSE_WAIT和TIME_WAIT的区别,这里从新描述一下:
服 务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常状况下,若是请求成功,那么在抓取完 资源后,服务器A会主动发出关闭链接的请求,这个时候就是主动关闭链接,服务器A的链接状态咱们能够看到是TIME_WAIT。若是一旦发生异常呢?假设 请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭链接的请求,服务器A就是被动的关闭了链接,若是服务器A被动关闭链接以后程序员忘了 让HttpClient释放链接,那就会形成CLOSE_WAIT的状态了。
 
因此若是将大量CLOSE_WAIT的解决办法总结为一句话那就是:查代码。由于问题出在服务器程序里头啊。

五、RST出现缘由

TCP异常终止的常见情形

咱们在实际的工做环境中,致使某一方发送reset报文的情形主要有如下几种:

1,客户端尝试与服务器未对外提供服务的端口创建TCP链接,服务器将会直接向客户端发送reset报文。

 

 

2,客户端和服务器的某一方在交互的过程当中发生异常(如程序崩溃等),该方系统将向对端发送TCP reset报文,告之对方释放相关的TCP链接,以下图所示:

 

 

3,接收端收到TCP报文,可是发现该TCP的报文,并不在其已创建的TCP链接列表内(好比server机器直接宕机),则其直接向对端发送reset报文,以下图所示:

 

TCP_NODelay

做者:Pengcheng Zeng
连接:https://www.zhihu.com/question/42308970/answer/123620051
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

参考 tcp(7): TCP protocol
TCP_NODELAY
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor utilization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.
TCP/IP协议中针对TCP默认开启了 Nagle算法。Nagle算法经过减小须要传输的数据包,来优化网络。关于Nagle算法,@ 郭无意 同窗的答案已经说了很多了。在内核实现中,数据包的发送和接受会先作缓存,分别对应于写缓存和读缓存。
那么针对题主的问题,咱们来分析一下。
启动TCP_NODELAY,就意味着禁用了Nagle算法,容许小包的发送。对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择。好比,对于SSH会话,用户在远程敲击键盘发出指令的速度相对于网络带宽能力来讲,绝对不是在一个量级上的,因此数据传输很是少;而又要求用户的输入可以及时得到返回,有较低的延时。若是开启了Nagle算法,就极可能出现频繁的延时,致使用户体验极差。固然,你也能够选择在应用层进行buffer,好比使用java中的buffered stream,尽量地将大包写入到内核的写缓存进行发送;vectored I/O(writev接口)也是个不错的选择。
对于关闭TCP_NODELAY,则是应用了Nagle算法。数据只有在写缓存中累积到必定量以后,才会被发送出去,这样明显提升了网络利用率(实际传输数据payload与协议头的比例大大提升)。可是这由不可避免地增长了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。固然这个问题只有在连续进行两次写操做的时候,才会暴露出来。
咱们看一下摘自Wikipedia的Nagle算法的伪码实现:
if there is new data to send
  if the window size >= MSS and available data is >= MSS send complete MSS segment now else if there is unconfirmed data still in the pipe enqueue data in the buffer until an acknowledge is received else send data immediately end if end if end if
经过这段伪码,很容易发现连续两次写操做出现问题的缘由。而对于读-写-读-写这种模式下的操做,关闭TCP_NODELAY并不会有太大问题。
The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.

连续进行屡次对小数据包的写操做,而后进行读操做,自己就不是一个好的网络编程模式;在应用层就应该进行优化。对于既要求低延时,又有大量小数据传输,还同时想提升网络利用率的应用,大概只能用UDP本身在应用层来实现可靠性保证了。好像企鹅家就是这么干的。

相关文章
相关标签/搜索