Unix/Linux编程:服务器软件设计

概念性的服务器算法

从概念上来说,各个服务器都遵循一种简单的算法:创建一个套接字,将它绑定到一个熟知的端口上,并期望在这个端口上接收请求,接着就进入无限循环,在该循环中,服务器接收来自客户的下一请求,处理这一请求,构造应答,然后将这个应答发回给客户。

但是,这个算法只适用于最简答的服务。如果像是文件传输这样的服务就不适用了,因为它在处理每一个请求时,要求有相当的时间,假设联系该服务器的第一个客户要求传输一个巨大的文件,而第二个客户要求传输一个小文件。如果服务器一直等到第一个文件传输完毕之后才传输第二个文件,那么,第二个文件就将为了一个小文件的传送而等待一段不合理的时间。

并发服务器和循环服务器

循环服务器:在一个时刻只处理一个请求的服务器实现
并发服务器:在一个时刻可以处理多个请求的服务器实现。

事实上,多数服务器并没有用于同时处理单个请求的冗余设施,而是提供一种表面上的并发性,方法是依靠多个执行线程,每个线程处理一个请求。淡然,用其他方法实现并发也是可行的—选择什么方法取决于应用程序。具体来讲,如果服务器相对它所执行的IO来说,只执行了一小点计算,那么,用一个单执行线程来实现是可能的,这个单线程使用异步IO,以便允许同时使用多个通信信道。

循环服务器便于理解和构造,但是性能很差,因为这样的服务器要使客户等待服务。并发服务器难以设计和构造,但是有比较好的性能。

传输协议的语义

TCP和UDP是TCP/IP协议族的两个主要传输协议,它们在很多方面是不同的。TCP提供面向连接的服务,UDP提供无连接的服务。另外,它们两者最大的不同来自它们提供给应用的语义

TCP语义

  • 点对点通信:TCP只提供面向连接的接口。TCP连接是点对点的,因为它只包括两个点------客户端在一端、服务器在另一端
  • 建立可靠连接:TCP要求客户应用程序在于服务器交换数据前,先要连接服务器,保证连接可靠建立。建立连接测试了网络的连通性,如果有故障发生,阻碍了分组到达远程系统,或者服务器不接受连接,那么,连接企图就会失败, 客户就会得到通知
  • 可靠交付:一旦建立连接,TCP保证数据会按照发送时的顺序交付,没有丢包、重复、乱序。如果因为故障不能可靠交付,发送方会得到通知
  • 双工传输:在任何时候,单个TCP连接都允许同时双向传输数据,而且不会相互影响。因此,客户可以向服务器发送请求,而服务器可以通过同一个连接发送回答
  • 流模式:TCP发送没有报文边界的字节流

UDP语义

  • 多对多通信:与TCP不同,UDP在可以进行通信的应用的数量上,具有更大的灵活性。多个应用可以向同一个接收方发送报文,一个发送方也可以向多个接收方数据。更重要的是,UDP能让应用使用底层网络的广播或者组播设施交付报文
  • 不可靠服务:UDP提供不可靠交付语义,即报文可能丢失、重复、乱服。它没有重传设施,如果发生故障,也不会通知发送方
  • 缺乏流控:UDP不提供流控------当数据报到达的速度比接收系统或应用的处理速度快时,只是将其丢弃而不会发出警告或者提示
  • 报文模式:UDP提供面向报文的接口,在需要传输数据时,发送方准确指明要发送的数据的字节数,UDP将这些数据放置在一个外发报文中。在接收方上,UDP一次交付一个传入报文。因此,当有数据交付时,接收到的数据拥有和发送方应用所指定的一样的报文边界

面向连接和无连接的访问

连接性问题是传输协议的中心,而客户使用这个传输协议访问某个服务器。TCP/IP协议族给应用提供了两种传输协议,TCP提供了一种面向连接的服务,而UDP提供了一种无连接的服务。因此,使用TCP的服务器是面向连接的服务,而UDP服务器是无连接服务器

尽管套接字接口允许应用程序把某个UDP套接字连接(用connect())到远程端口上,但这样做只会影响传入数据报的分用。因为UDP不是面向连接的协议,所以远程站点不会得到关于连接的通知,而且也不会由运输层的链接

尽管我们把这个术语用到了服务器上,但是如果我们将其限制在应用协议上则会更加准确些,因为,在无连接的实现和面向连接的实现之间进行选择依赖于应用协议。

面向连接的服务器

面向连接的方法的主要优点在于易于编程。

  • 因为传输协议自动处理分组丢失和交付失序问题,服务器就不需要对这些问题操心了
  • 面向连接的服务器只要管理和使用这些连接就行了。服务器接受来自某个客户的传入连接,然后,通过这个连接发送所有的通信数据。它从客户接受请求并发送应答。最后,在完成交付后关闭连接
  • 在连接状态打开时,TCP提供了所需要的可靠性。它重传丢失的数据,验证到达的数据没有传输差错,在必要时,对传入数据进行重新排序。当客户发送请求时,TCP要么将其可靠的交付,要么通知客户连接已经终端。与此类似,服务器可以依赖TCP交付响应或者通知它不能完成交付

缺点是面向连接的设计要求对每个连接都有一个单独的套接字,而无连接的设计则运行从一个套接字上与多个主机通信。在操作系统中,套接字的分配和最终的连接建立和终止连接的三次握手过程使TCP比UDP开销更大。另外,TCP在空闲的连接上根部不发生任何分组。假设客户与某个服务器建立了连接,并与之交换请求和响应,接着崩溃了。因为客户已经分配了,它就不会再发送任何请求了,然而,服务器到目前为止对它收到的所有请求都已经进行了响应,它就不会再发送任何请求了。在这种情况下,问题出在资源的使用上:服务器拥有分配给该连接的数据结构以及缓存空间,并且这些资源不能被重新分配。而服务器必须设计成始终在运行。如果不断有客户崩溃,服务器就会耗尽资源(比如套接字、缓存空间、TCP连接)从而终止运行

无连接的服务器

无连接服务器也有其优缺点。尽管无连接的服务器没有资源耗尽问题的困扰,但是它们不能依赖下层传输提供可靠的投递,通信一份或者双方都必须承担可靠性方面的责任。通常,如果没有响应到达,客户要承担重传的机制。如果服务器需要将响应分为多个数据分组,也需要实现重传机制。

总的来说,由于UDP不提供可靠分组,无连接传输要求应用提供可靠性,并在必要时,使用一种称为自适应重传的复杂技术。为现有的应用程序增加自适应重传需要程序员具有相当的专业知识。

在选择面向无连接还是面向连接的传输时,另一个需要考虑的是该服务是否需要广播或者组播通信。由于TCP只提供点到点通信,它不允许应用广播或者组播(使用UDP),因此,任何一个接受或者响应组播通信的服务器必然是无连接的。

四种类型的服务器

服务器可以是循环的或者并发的,可以是面向连接到或者无连接的,因此,可以分为四种:

在这里插入图片描述

  • 循环、无连接的服务器
    • 最常见的无连接服务器
    • 适用于要求对每个请求进行少量处理的服务
    • 循环服务器往往是无状态的
  • 循环、面向连接的服务器
    • 较常见
    • 适用于对每个请求进行少量处理,但是必须由可靠的传输
    • 因为与建立连接和终止连接相关的开销可能很高,平均响应时间可能并不短
  • 并发、无连接的服务器
    • 不常见
    • 服务器要为每个请求创建一个新进程或者线程。
    • 在很多系统中,创建线程或者进程所增加的开销决定了由并发性所获得的效率
    • 为证明并发性是可取的,要么创建一个新进程或线程所要求的时间小于计算响应的时间,要么并发请求必须能够同时使用多个IO设备
  • 并发、面向连接的服务器
    • 最一般的服务器类型。因为它提供了可靠的传输(即它可以跨越广域网)以及并发处理多个连接的能力
    • 实现方法:最常见的实现是使用并发进程或者进程线程来处理每个连接;很不常见的实现是依赖单线程和异步IO处理多个连接
    • 在并发进程的实现中:主服务器线程为每个连接创建一个从进程。使用多进程使得如下情况变得容易,即为每个连接执行一个单独编译的程序,而不是将所有代码放在一个单独的、巨大的服务器程序中
    • 在单线程实现中,一个执行线程管理多个连接,它通过使用异步IO来达到表面上的并发性。服务器反复的在它所打开的连接上等待IO,收到请求就进行处理。由于单个线程处理所有的连接,它就可以在多个连接之间共享数据。然而,因为服务器只有一个线程,即在一个具有多个处理器的计算机上,它处理请求的速度不会比循环服务器更快。应用程序必须共享数据或者堆每个请求的处理时间很短,只要在这种情况下这种服务器实现方案才是可取的

循环面向连接的服务器的算法

单个执行线程一次处理一个来自客户的连接,其一般性算法如下:

  1. 创建套接字并将其绑定到它所提供服务的熟知端口上
  2. 将该端口设置为被动模式,使其准备为服务器所用
  3. 从该套接字上接收来自下一个连接请求,获得该链接的新套接字
  4. 重复读取来自客户的请求,构造响应,按照应用协议向客户发回响应
  5. 当与某个特定客户完成交互时,关闭连接,并返回第三步等待接收新链接

循环无连接的服务器算法

循环服务器对那些具有小的请求处理时间的服务工作的最好。因为向TCP这样的面向连接的传输协议,要比向UDP这样的无连接的传输协议具有更高的额外开销,所以多数循环服务器使用无连接的传输,其一般算法如下:

  1. 创建套接字并将其绑定到它所提供服务的熟知端口上
  2. 重复读取来自客户的请求,构造响应,按照应用协议向客户发回响应

为循环、无连接的服务器创建套接字和面向连接的服务器是一样的。该服务器的套接字将保持无连接的,而且可以接受来自任何客户的传入数据报

并发无连接的服务器算法

并发的,无连接的服务器中,主服务器线程接受传入请求(数据报)并为处理每个传入请求而创建一个从线程

  • 主1:创建套接字并将其绑定到它所提供服务的熟知端口上,让该套接字保持无连接的
  • 主2:反复调用recvfrom接收来自客户的下一个请求,创建一个新的从线程来处理响应
  • 从1:从来自主线程的特定请求以及该套接字的访问开始
  • 从2:根据应用协议构造应答,并用sendto将该应答发回给客户
  • 从3:退出(即从线程在处理完一个请求后终止)

注意:尽管创建一个新进程或者线程的精确开销依赖于操作系统和下层的体系结构,但这个操作可能还是很昂贵的。因此只有很少的无连接服务器采用并发实现。

并发面向连接的服务器算法

某些连接的应用协议使用连接作为其通信的基本模式。它们允许客户同服务器建立连接,在这个连接上进行通信,之后就将此连接丢弃。在大多数场合下,客户和服务器之间的连接将处理不止一个请求:协议允许客户重复的发送请求和接收响应,而不必终止这个连接或者创建新的连接。因此:

面向连接的服务器在单个连接之间(而不是在各个请求之间)实现并发性

下面给出了并发服务器使用面向连接协议的步骤

  • 主1:创建套接字并将其绑定到它所提供服务的熟知端口上,让该套接字保持无连接的
  • 主2:将该端口设置为被动模式,使其准备为服务器所用
  • 主3:反复调用accept以便接收来自客户的下一个连接请求,并创建新的从线程或者进程来处理响应
  • 从1:从主线程传递来的连接请求(即针对连接的套接字)开始
  • 从2:用该连接与客户进行交互:读取请求并发回响应
  • 从3:关闭连接并退出。在处理完客户的所有请求后,从线程退出

如无连接时的情况,主线程从来不与客户直接进行通信。只要新连接一到达,主线程就创建一个从线程来处理这个连接。在从线程同这个连接进行交互式,主线程等待其他的连接

并发服务器

为什么需要并发

将并发引入服务器的主要原因是需要给多个客户提供快速的相应时间。并发性会缩短时间,如果:

  • 构造要求有相当的IO时间响应
  • 各个请求所要求的处理时间的变化很大
  • 服务器运行在具有多个处理器的计算机上

对于第一种情况: 运行服务器并发意味着即使机器只有一个CPU,他可以部分重叠的使用处理器和外设。当处理忙于计算响应时,IO设计可以将数据传送到存储器中,而这可能是其他相应需要的

对于第二种情况:时间分片允许单个处理器处理那些只要求少量处理的请求看,而不必要等待处理完那些需要很长时间的请求

对于第三种情况:可以允许一个处理器为一个请求计算响应,另一个处理器为令一个请求计算响应

主线程与从线程

大多数并发服务器使用多线程,它们可以划分为两类:

  • 主线程(master)最先开始允许,在熟知端口上打开一个套接字,等待下一个请求,并为处理每个请求创建一个从线程(可能在一个新进程中)
  • 主线程不与客户直接通信-----它将这个任务交给一个从线程,每个从线程处理与一个客户的通信,在从线程构成响应并将它发送给客户之后,这个从线程就退出

服务器并发性的实现

Linux提供了两种形式的并发性-------进程和线程,所以由两种常见的主-从模式实现。一种是服务器创建多个进程,每个进程都有一个执行线程。另一种是在一个进程中创建多个执行线程:

在这里插入图片描述

单线程并发TCP服务器的算法

由单个执行线程实现并发的,面向连接的服务器:服务器等待下一个准备就绪的描述符,这个新的描述符意味着一个新的链接到达,或者是某个客户在已有的连接中发送了一个请求。其一般性算法如下

  1. 创建套接字并将其绑定到这个服务的熟知端口上。将该套接字加到一个表中,该表中的项是可以进行IO的描述符
  2. 使用select在已有的套接字上等待IO
  3. 如果最初的套接字准备就绪,使用accept获得下一个连接,并将这个新套接字加入到表中,该表中的项是可以进行IO的描述符
  4. 如果是最初的套接字意外的某些套接字就绪,就使用recv或者read获得下一个请求,构造响应,用send或者write将响应发回给客户
  5. 回到第2步进行处理

其他

用INADDR_ANY绑定熟知端口

服务器需要创建套接字并将其绑定到所提供服务的熟知端口上。如同客户一样,服务器使用过程getportbyname()将服务名映射到熟知的端口上。比如TCP/IP定义了ECHO服务,实现ECHO服务的服务器利用getportbyname()将字符串"echo"映射到端口7

当bind为某个套接字指明某个连接端口时,它使用了结构sockaddr_in,该结构中含有IP地址和协议端口号。因此,对一个套接字,bind不能只指定端口而不指定IP地址。但是,选择指明的IP地址,对于像路由器或多接口机这样的有多个IP地址的机器来说就会带来麻烦:这个套接字将不能接收来自该机器的其他IP地址上的通信内容

为了解决这个问题,套接字接口定义了一个特殊的常量INADDR_ANY,它可以代替IP地址。INADDR_ANY指明了一个通配地址,它与该主机的任何一个IP地址都匹配。使用INADDR_ANY使得多接口机上的单个服务器可以接受这样的通信,即传入的目的地址是该主机上的任何一个IP地址。

即:当为套接字指定本地端点时,服务器使用INADDR_ANY以取代某个特定的IP地址,这就允许套接字接受发给该机器的任何一个IP地址的数据报

将套接字置于被动模式

使用TCP服务器调用listen将套接字置于被动模式。listen还有一个参数用来指明该套接字的内部请求队列的长度。这个请求队列保存着一组TCP传入连接请求,这些连接请求来自客户,每个客户都向这个服务器请求了一个连接。

接收连接并使用这些连接

TCP服务器调用accept获取下一个传入连接请求(即把它从请求队列中取出)。该调用返回用于新的连接的套接字描述符。服务器一旦接受了新的连接,它就使用read获取来自客户端的请求,并使用write应答。最后,服务器一旦结束使用这个连接,就调用close释放该套接字

在无连接的服务器中收发

在客户调用connect之后,可以使用send和write发送数据,因为套接字的内部数据结构中包含了远程地址和本地地址。然而,无连接的服务器不能使用connect,因为这样做会限制套接字,使其只能与其他特定的远程主机通信。因此,无连接服务器使用一个非连接的套接字。它明确的产生应答的地址,并使用sendto发送数据:

sendto(sockfd, message, len,  flags,  dest_addr,addrlen);

服务器能够从收到的请求的源地址中获得应答的地址:

/*
* 因为UDP没有连接的概念,所以我们每次读取数据时都需要获取发送端的socket地址,也就是src_dest的内容
* addrlen指定该地址的长度
*/
recvfrom(sockfd,buf,size_t len,flags,src_dest,addrlen);

各服务器类型所适用的场合

循环的和并发的:循环服务器容易设计、实现和维护,但是并发服务器可以对请求提供更快的响应。

真正的和表面上的并发性:

  • 只有一个线程的服务器依靠异步IO管理多个连接;而多线程(多个单线程的进程/一个进程有多个线程)的实现允许操作系统自动提供并发性。
  • 如果创建线程或者切换环境的开销很大,或者服务器必须在多个连接之间共享或者交换数据,那么可以使用单线程的方案;
  • 如果创建线程或者切换环境的开销不大,而且服务器必须在多个连接之间共享或者交换数据,那么可以使用多线程的方案;
  • 如果每个从进程可以独立的运行或者为了要获得最大的并发性(比如在多个处理器上),那么可以使用多进程的方案

面向连接和无连接的:

  • 因为面向连接的访问意味着使用TCP,所以它暗示着可靠的交付;无连接的传输意味着使用UDP,所以它暗示着不可靠的交付
  • 只有在应用协议处理可靠性问题(几乎没有这样做的)或者每个客户访问它的服务器都是在同一个局域网中进程的(这只有极小的分组丢失率并且没有分组失序),这时才使用无连接的传输
  • 只要客户和服务器被广域网所分隔,就要使用面向连接的传输
  • 在没有检测应用程序是否处理了可靠性问题之前,绝不要将无连接的客户和服务器转移到广域网中

重要问题—服务器死锁

许多服务器实现都有一个共同的缺陷:服务器可能会被死锁困扰

要立即为什么会出现死锁,考虑一个循环的、面向连接的服务器。假设某个客户应用程序C不能正常工作。在最简单的情况下,假设C同某个服务器建立了一个连接,但从未发送过一个请求。服务器将接收这个新的连接,并且将调用recv和read来取出下一个请求。服务器进程将在该系统调用上被阻塞,它将在这里等待一个永远也不会到来的请求。

如果客户不能正常工作是由于不能处理响应,那么服务器可能会以一种更为微妙的方式产生死锁。例如,假设客户C同某个服务器建立了连接,向它发送了一系列请求,但从未读取响应。服务器不停地接收请求、产生响应,并将响应发回给客户。在服务器里,TCP软件在这个连接上把最初的几个字节发送给了客户。TCP最终会将客户的接收窗口填满并将停止传输数据。如果服务器应用程序继续产生响应,TCP用于为该连接存储外发数据的本地缓存将会被填满,于是服务器被阻塞。

当操作系统不能满足一个系统调用时,会因调用程序的阻塞而产生死锁。特别是,如果TCP没有本地缓存(这用来存放已经发送的数据),那么对send或write的调用将阻塞调用者;对recv和read的调用也将阻塞调用者,直到TCP收到数据。对并发服务器来说,如果某个客户发送请求或者读取响应失败了,只有与这个特定客户相关的一个从线程会被阻塞。然而,对一个单线程的实现来说,这个中央服务器将被阻塞,从而导致不能处理其他连接。即任何只有一个线程的服务器可能会被死锁所困扰

如果服务器使用了与客户通信时可能会阻塞的系统调用,一个不能正常工作的客户可能会引起单线程服务器死锁。在服务器中,死锁是一个严重的问题,因为它意味着一个客户的行为会使服务器不能处理其他客户的请求

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值