高性能网络编程(一)----accept创建链接

最近在部门内作了个高性能网络编程的培训,近日整理了下PPT,欲写成一系列文章从应用角度谈谈它。linux

编 写服务器时,许多程序员习惯于使用高层次的组件、中间件(例如OO(面向对象)层层封装过的开源组件),相比于服务器的运行效率而言,他们更关注程序开发 的效率,追求更快的完成项目功能点、但愿应用代码彻底不关心通信细节。他们更喜欢在OO世界里,去实现某个接口、实现这个组件预约义的各类模式、设置组件 参数来达到目的。学习复杂的通信框架、底层细节,在习惯于使用OO语言的程序员眼里是绝对事倍功半的。以上作法无可厚非,但有必定的局限性,本文讲述的网 络编程头前冠以“高性能”,它是指程序员设计编写的服务器须要处理很大的吞吐量,这与简单网络应用就有了质的不一样。由于:一、高吞吐量下,容易触发到一些 设计上的边界条件;二、偶然性的小几率事件,会在高吞吐量下变成必然性事件。三、IO是慢速的,高吞吐量一般意味着高并发,如同一时刻存在数以万计、十万 计、百万计的TCP活动链接。因此,作高性能网络编程不能仅仅知足于学会开源组件、中间件是如何帮我实现指望功能的,对于企业级产品来讲,须要了解更多的 知识。程序员


掌握高性能网络编程,涉及到对网络、操做系统协议栈、进程与线程、常见的网络组件等知识点,须要有丰富的项目开发经验,可以权衡服务器运行效率与项目开发效率。如下图来谈谈我我的对高性能网络编程的理解。编程


上面这张图中,由上至下有如下特色:tomcat

•关注点,逐渐由特定业务向通用技术转移服务器

•使用场景上,由专业领域向通用领域转移网络

•灵活性上要求愈来愈高多线程

•性能要求愈来愈高架构

•对细节、原理的掌握,要求愈来愈高并发

•对各类异常状况的处理,要求愈来愈高框架

•稳定性愈来愈高,bug率愈来愈少

在作应用层的网络编程时,若服务器吞吐量大,则应该适度了解以上各层的关注点。


如上图红色文字所示,我认为编写高性能服务器的关注点有3个:

一、 若是基于通用组件编程,关注点可能是在组件如何封装套接字编程细节。为了使应用程序不感知套接字层,这些组件每每是经过各类回调机制来向应用层代码提供网络 服务,一般,出于为应用层提供更高的开发效率,组件都大量使用了线程(Nginx等是个例外),固然,使用了线程后每每能够下降代码复杂度。但多线程引入 的并发解决机制仍是须要重点关注的,特别是锁的使用。另外,使用多线程意味着把应用层的代码复杂度扔给了操做系统,大吞吐量时,须要关注多线程给操做系统 内核带来的性能损耗。

基于通用组件编程,为了程序的高性能运行,须要清楚的了解组件的如下特性:怎么使用IO多路复用或者异步IO的?怎么实现并发性的?怎么组织线程模型的?怎么处理高吞吐量引起的异常状况的?


二、 通用组件只是在封装套接字,操做系统是经过提供套接字来为进程提供网络通信能力的。因此,不了解套接字编程,每每对组件的性能就没有原理上的认识。学习套 接字层的编程是有必要的,或许不多会本身从头去写,但操做系统的API提供方式经久不变,一经学会,受用终身,同时在项目的架构设计时,选用何种网络组件 就很是准确了。

学 习套接字编程,关注点主要在:套接字的编程方法有哪些?阻塞套接字的各方法是如何阻塞住当前代码段的?非阻塞套接字上的方法如何不阻塞当前代码段的?IO 多路复用机制是怎样与套接字结合的?异步IO是如何实现的?网络协议的各类异常状况、操做系统的各类异常状况是怎么经过套接字传递给应用性程序的?


三、网络的复杂性会影响到服务器的吞吐量,并且,高吞吐量场景下,多种临界条件会致使应用程序的不正常,特别是组件中有bug或考虑不周或没有配置正确时。了解网络分组能够定位出这些问题,能够正确的配置系统、组件,能够正确的理解系统的瓶颈。

这里的关注点主要在:TCP、UDP、IP协议的特色?linux等操做系统如何处理这些协议的?使用tcpdump等抓包工具分析各网络分组。


通常掌握以上3点,就能够挥洒自如的实现高性能网络服务器了。


下面具体谈谈如何作到高性能网络编程。

众所周知,IO是计算机上最慢的部分,先不看磁盘IO,针对网络编程,天然是针对网络IO。网络协议对网络IO影响很大,当下,TCP/IP协议是毫无疑问的主流协议,本文就主要以TCP协议为例来讲明网络IO。

网络IO中应用服务器每每聚焦于如下几个由网络IO组成的功能中:A)与客户端创建起TCP链接。B)读取客户端的请求流。C)向客户端发送响应流。D)关闭TCP链接。E)向其余服务器发起TCP链接。

要掌握住这5个功能,不只仅须要熟悉一些API的使用,更要理解底层网络如何与上层API之间互相发生影响。同时,还须要对不一样的场景下,如何权衡开发效率、进程、线程与这些API的组合使用。下面依次来讲说这些网络IO。



一、与客户端创建起TCP链接

谈这个功能前,先来看看网络、协议、应用服务器间的关系


上图中可知:

为简化不一样场景下的编程,TCP/IP协议族划分了应用层、TCP传输层、IP网络层、链路层等,每一层只专一于少许功能。

例如,IP层只专一于每个网络分组如何到达目的主机,而无论目的主机如何处理。

传输层最基 本的功能是专一于端到端,也就是一台主机上的进程发出的包,如何到达目的主机上的某个进程。固然,TCP层为了可靠性,还额外须要解决3个大问题:丢包 (网络分组在传输中存在的丢失)、重复(协议层异常引起的多个相同网络分组)、延迟(好久后网络分组才到达目的地)。

链路层则只关心以太网或其余二层网络内网络包的传输。


回到应用层,每每只须要调用相似于accept的API就能够创建TCP链接。创建链接的流程你们都了解--三次握手,它如何与accept交互呢?下面以一个不太精确却通俗易懂的图来讲明之:


研究过 backlog含义的朋友都很容易理解上图。这两个队列是内核实现的,当服务器绑定、监听了某个端口后,这个端口的SYN队列和ACCEPT队列就创建好 了。客户端使用connect向服务器发起TCP链接,当图中1.1步骤客户端的SYN包到达了服务器后,内核会把这一信息放到SYN队列(即未完成握手 队列)中,同时回一个SYN+ACK包给客户端。一段时间后,在较中2.1步骤中客户端再次发来了针对服务器SYN包的ACK网络分组时,内核会把链接从 SYN队列中取出,再把这个链接放到ACCEPT队列(即已完成握手队列)中。而服务器在第3步调用accept时,其实就是直接从ACCEPT队列中取 出已经创建成功的链接套接字而已。


现有咱们能够来讨论应用层组件:为什么有的应用服务器进程中,会单独使用1个线程,只调用accept方法来创建链接,例如tomcat;有的应用服务器进程中,却用1个线程作全部的事,包括accept获取新链接。


缘由在于: 首先,SYN队列和ACCEPT队列都不是无限长度的,它们的长度限制与调用listen监听某个地址端口时传递的backlog参数有关。既然队列长度 是一个值,那么,队列会满吗?固然会,若是上图中第1步执行的速度大于第2步执行的速度,SYN队列就会不断增大直到队列满;若是第2步执行的速度远大于 第3步执行的速度,ACCEPT队列一样会达到上限。第一、2步不是应用程序可控的,但第3步倒是应用程序的行为,假设进程中调用accept获取新链接 的代码段长期得不到执行,例如获取不到锁、IO阻塞等。


那么,这两个队列满了后,新的请求到达了又将发生什么?

若SYN队 列满,则会直接丢弃请求,即新的SYN网络分组会被丢弃;若是ACCEPT队列满,则不会致使放弃链接,也不会把链接从SYN列队中移出,这会加重SYN 队列的增加。因此,对应用服务器来讲,若是ACCEPT队列中有已经创建好的TCP链接,却没有及时的把它取出来,这样,一旦致使两个队列满了后,就会使 客户端不能再创建新链接,引起严重问题。

因此,如TOMCAT等服务器会使用独立的线程,只作accept获取链接这一件事,以防止不能及时的去accept获取链接。


那么,为何如Nginx等一些服务器,在一个线程内作accept的同时,还会作其余IO等操做呢?

这里就带出阻塞和非阻塞的概念。应用程序能够把listen时设置的套接字设为非阻塞模式(默认为阻塞模式),这两种模式会致使accept方法有不一样的行为。对阻塞套接字,accept行为以下图:


这幅图中能够看到,阻塞套接字上使用accept,第一个阶段是等待ACCEPT队列不为空的阶段,它耗时不定,由客户端是否向本身发起了TCP请求而定,可能会耗时很长。

对非阻塞套接字,accept会有两种返回,以下图:


非阻塞套接字上的accept,不存在等待ACCEPT队列不为空的阶段,它要么返回成功并拿到创建好的链接,要么返回失败。


因此,企业级的服务器进程中,若某一线程既使用accept获取新链接,又继续在这个链接上读、写字符流,那么,这个链接对应的套接字一般要设为非阻塞。缘由如上图,调用accept时不会长期占用所属线程的CPU时间片,使得线程可以及时的作其余工做。

相关文章
相关标签/搜索