客户/服务器程序设计范式

原创 2011年01月10日 23:45:00

 

 

 

        本篇从基于TCP/IP协议出发,探讨现代流行的应对高并发请求网络服务端设计架构;

1. TCP/IP 模型

首先回顾一下TCP/IP模型,并知道各个层次在操作系统的哪一个层次; 

   看上图,OSI模型的底下两层是随系统提供的设备驱动程序和网络硬件。通常情况下,除需知道数据链路的某些特性外,我们不用关心这两层的情况。网络层由IPv4和IPv6两个协议处理,可以选择的传输层有TCP或UDP。OSI模型的顶上三层被合并为一层,称为应用层,这就是web客户(浏览器)、telnet客户、web服务器等都所在的层。

   可见OSI模型以传输层为分界线;套接字编程接口为应用层进入传输层的接口;也符合抽象的目的:对应用层隐藏网络协议的具体通信细节,应用层只处理具体网络应用;而底下四层(从传输层到物理层)对具体网络应用了解不多,却处理所有的通信细节:发送数据,等待确认,给无序到达的数据排序,计算并验证校验和,等等。

   其次,顶上三层通常构成所谓的用户进程,底下四层却通常作为操作系统内核的一部分提供。完全符合Unix和其他现代操作系统都提供分割用户进程和内核的机制。

2. 基本TCP 套接字编程

   TCP:传输控制协议(Transmission Control Protocol).TCP是一个面向连接的协议,为用户进程提供可靠的全双工字节流。TCP套接字是一种流套接字(stream socket). TCP关心确认,超时和重传之类的细节。大多数网络应用程序使用TCP;

    以apache为例子;apache工作在OSI模型的应用层;我们都知道启动apache后他会监听在80端口,并通过线程池来响应每个客户请求;即来一个客户请求,从连接池那出一个线程去处理这个请求;那么apache的内部是如何针对每个请求创建套接字,然后分配一个线程的呢?

    看图2,apache服务器启动后通过创建监听套接字listensocket,通过调用bind(),listen(),accept(),让主线程监听在80端口;客户请求过来后tcp三次握手,握手成功后accept()返回一个已连接套接字connsocket,代表与所返回客户的tcp连接;然后apache从连接池里拉出一个线程,并将这个已连接套接字给这个线程,让次线程处理这个请求;请求处理完毕后关闭连接,已连接套接字相应的回收;

注:监听套接字(listening socket)是在该tcp服务进程生命周期内一直存在。而已连接套接字代表一个和客户请求建立的一个套接字,生命周期为一次请求;

                     图2 : 基本TCP客户/服务器程序套接字函数

注:我们的jms客户端程序也是采用主线程监听队列消息,并将收到的消息给线程池里的一个线程去处理的方式来并发处理消息;

3. 现代并发web服务器的设计范式:

     但是apache真的是一个主线程在监听80端口么? 不是的,一个主线程监听80端口,然后将已连接套接字抛给线程池里的线程去处理,有些缺点;

     缺点如下:1. 只有一个线程监听80端口,并负责三次握手和转发已连接套接字;万一此线程挂了,整个web server就没有服务能力了,容错能力不强;

                    2. 因为connect(),accept()都是阻塞函数。所以每个客户请求来都阻塞在主线程,导致不能很好的应对并发;

     基于这些缺点;apache采用每个线程都去监听80端口,当然同一时刻,只有一个线程在监听80端口;其他线程属于空闲状态;一个客户请求过来,正在监听的线程会处理这个请求并转换为工作者线程;并让出监听者的角色,这样其他线程竞争监听者的角色,并最终有一个线程监听80端口;当一个工作线程处理完请求后,回到连接池中,又处于空闲状态;

但是这样又有问题,如果一个客户请求过来,会导致所有的空闲线程都去竞争监听者的角色。会导致很多空闲线程一下被唤醒,并只有一个线程获得监听者角色,其他线程继续空闲(睡觉),这种大批线程从空闲状态突然被唤醒有突然又睡过去,就是惊群现象;惊群现象会导致性能下降;

     apache通过在每个accept()函数上 增加互斥锁和条件变量 来解决这个惊群问题。保证每个请求只会被一个线程刚好拿到,不会影响其他线程;

      这里详细介绍下:条件变量与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用;互斥锁提供互斥机制,条件变量提供信号机制;
     那么apache是如何利用条件变量和互斥锁来解决每次只有一个空闲线程被唤醒,并且处于监听者角色呢?
     每次一个新的客户请求过来,正在监听的线程与该请求建立连接,并变为worker工作者线程。让出监听者角色时它同时发送信号到条件变量,并释放锁。这样在空闲(idle)状态的一个线程将被唤醒并获得锁。

     也就是说:条件变量保证了其他线程在等待条件变化期间处于睡眠;互斥锁保证一次只有一个线程被唤醒;


                                                      图3 :apache 的preforking 机制

 

 

总结:通过了解了TCP/IP编程模型。和apache的MPM、Preforking机制后,我们再去看jms消息客户端代码,memcache服务器代码,jetty,等流行的web服务器的机制就不是很难了。

                                                                                                           参考:《UNIX网络编程 卷1 :套接字联网API》

 

Unix网络编程代码 第30章 客户/服务器程序设计范式

第30章 客户/服务器程序设计范式 30.3 TCP测试用客户程序 运行:./client 127.0.0.1 8888 1 5000 4000 #define _GNU_SOURCE #inc...

UNP函数笔记十七: 客户/服务器程序设计范式

第三十章  客户/服务器程序设计范式: 示例: #include "err_exit.h" #include /* * Allocate an array of "nchildr...

UNIX网络编程卷1 服务器程序设计范式6 并发服务器,为每个客户请求创建一个线程

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.为每个客户请求创建一个线程,以取代为每个客户派生一个子进程 /*...
  • zhsenl
  • zhsenl
  • 2014年08月26日 11:51
  • 843

UNIX网络编程卷1 服务器程序设计范式8 预先创建线程,由主线程调用accept

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.程序启动阶段创建一个线程池之后只让主线程调用 accept 并把客户连...
  • zhsenl
  • zhsenl
  • 2014年08月27日 11:42
  • 696

UNIX网络编程卷1 服务器程序设计范式3 预先派生子进程,以文件上锁方式保护accept

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.允许多个进程在引用同一个监听套接字的描述符上调用 accept 这种做法并不具备兼容...
  • zhsenl
  • zhsenl
  • 2014年08月26日 11:30
  • 652

UNIX网络编程卷1 服务器程序设计范式7 预先创建线程,以互斥锁上锁方式保护accept

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.预先创建一个线程池,并让每个线程各自调用 accept 2.用互斥锁取代...
  • zhsenl
  • zhsenl
  • 2014年08月26日 11:54
  • 989

UNIX网络编程卷1 服务器程序设计范式2 预先派生子进程,每个子进程调用accept

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.预先派生子进程:在启动阶段预先派生一定数量的子进程,当各个客户连接到达时, 这些子进...
  • zhsenl
  • zhsenl
  • 2014年08月25日 21:56
  • 536

unix网络编程各种TCP客户-服务器程序设计实例(二)

前面我们介绍了unix网络编程各种TCP客户-服务器程序设计实例附环境搭建和编译方法 本节我们接着介绍另外的几种TCP客户-服务器程序; 第四种:TCP并发服务器,每个客户一个子线程 在我们...

unix网络编程各种TCP客户-服务器程序设计实例(三)

第五种  TCP预先派生子进程服务器程序: 对预先派生子进程服务器的最后一种改动就是由父进程调用accept,然后再将所接受的已连接描述字传递给子进程。父进程必须跟踪子进程的忙闲状态,以便给空闲子进...

UNIX网络编程——客户/服务器程序设计示范(七)

TCP预先创建线程服务器程序,每个线程各自accept        前面讨论过预先派生一个子进程池快于为每个客户线程派生一个子进程。在支持线程的系统上,我们有理由预期在服务器启动阶段预先创建一个线程...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:客户/服务器程序设计范式
举报原因:
原因补充:

(最多只允许输入30个字)