grooovvve

 所有的网络应用都是基于相同的基本编程模型有着相似的整体逻辑结构,并且依赖相同的编程接口

 

网络应用依赖很多系统研究中已经学习过的概念:进程、信号、字节顺序、存储器映射以及动态存储分配;

还有一些新概念要掌握;

首先要理解客户端-服务器编程模型,以及如何编写使用因特网提供的服务的客户端-服务器程序。

最后把这些概念整合起来,开发一个小的但是功能齐全的Web服务器,能够为真实的Web浏览器提供静态和动态的文本和图形内容。

=====================================================

1、客户端——服务器端编程模型

每个网络应用都是基于客户端——服务器模型的。

采用这个模型,一个应用是由一个服务器进程和一个或者多个客户端进程组成的。

服务器进程管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。

例如Web服务器管理了一组磁盘文件,它会代表客户端进行检索和执行。

例如FTP服务器就管理了一组磁盘文件,它会为客户进程存储和检索。

相似地一个电子邮件服务器管理了一些文件,它为客户端进行读和更新。

 

客户端——服务器模型中的基本操作是事务(transaction)

一个事务由以下四步组成:

1)当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务。

2)服务器收到请求后,解释它,并以适当的方式操作它的资源。

3)服务器给客户端发送一个响应,并等待下一个请求。

4)客户端收到响应并处理它。

 

//这里加一张客户端——服务器事务图

 

认识到服务器和客户端都是进程,而不是常常提到的机器或者主机,这非常重要。

一台主机可以同时运行许多不同的客户端和服务器,而且一个客户端和服务器的事务可以在同一台或是不同的主机上运行。

无论客户端和服务器是怎样映射到主机上的,客户端——服务器模型是相同的。

这里的事务,仅仅是客户端和服务器执行的一系列步骤。

=====================================================

2、网络

客户端和服务器通常运行在不同的主机上,并且通过计算机网络的硬件和软件资源来通信。

网络是复杂的系统,我们的目的是从程序员的角度提供一个可工作的思考模型。

 

对于主机而言,网络只是一个I/O设备,作为数据源和数据接收方。

物理上,插到I/O总线扩展槽的适配器提供了到网络的物理接口。

从网络上接收到的数据从适配器经过I/O和存储器拷贝到存储器,典型的是通过DMA(直接存储器存取方式)传送

相似地,数据也能从存储器拷贝到网络。

 

物理上而言,网络是一个按照地理位置远近组成的层次系统。

最低层是LAN(Local Area Network)局域网。迄今为止,最流行的局域网技术是以太网

一个以太网(Ethernet segment)包括一些电缆和一个叫做集线器的小盒子。

以太网段通常跨越一些小区域。

电缆一端连接到主机的适配器,一端连接到集线器的一个端口上。

集线器不加分辨地将从一个端口上收到的每个位复制到其他所有的端口上。

因此,每台主机都能看到每个位。

 

每个以太网适配器都有一个全球唯一的48位地址,它存储在这个适配器的非易失性存储器上。

一台主机可以发送一段位,称为帧(frame),到这个网段内其他任何主机。

每个帧包括一些固定数量的头部(head)位。用来标识此帧的源和目的地址以及此帧的长度。

紧随其后的是数据位的有效载荷。每个主机适配器都能看到这个帧,但是只有目的主机实际读取它。

 

 

使用一些电缆和叫做网桥的小盒子,多个以太网段可以连接成较大的局域网,称为桥接以太网

网桥比集线器更加充分地利用了电缆带宽。并且利用一种聪明的分配算法,它们随着时间自动学习哪个主机可以通过哪个端口可达,然后只在必要时,有选择地将帧从一个端口拷贝到另一个端口。

 

为了简化局域网的表示,我们把集线器和网桥以及连接它们的电缆画成一条水平线。

在层次更高的级别上,多个不兼容的局域网可以通过叫做路由器的特殊计算机连接起来,组成一个internet(互联网络)

//internet 描述一般概念, Internet描述一种具体的实现,专有名词,即全球IP互联网。

 

每台路由器对于它所连接到的每个网络都有一个适配器(端口)。

路由器也能连接高速点到点电话连接,这是称为WAN(广域网 Wide-Area Network)。

之所以这么叫是因为它们覆盖的地理范围比局域网大。

一般而言,路由器可以用来由各种局域网和广域网构建互联网络。

 

互联网络至关重要的特征是,它能由采用完全不同和不兼容技术的各种局域网和广域网组成。

每台主机和其他每台主机都是物理相连的。

但是如何能够让某台源主机跨过所有这些不兼容的网络发送数据位到另一台目的主机呢?

解决方法是一层运行在每台主机和路由器上的协议软件。

它消除了不同网络之间的差异。

 

接下来讨论协议软件这种软件必须提供两种基本能力:

1)命名机制

  不同的局域网技术有这不同和不兼容的方式来为主机分配地址

  互联网络协议通过定义一种一致的主机地址格式来消除这些差异。

  每台主机会被分配至少一个这种互联网络地址,这个地址唯一标识了这台主机。

2)传送机制

  不同的联网技术有着不同的和不兼容的方式封装帧。

  互联网络协议通过定义一种把数据位捆扎成不连续的片(称为包)的统一方式,从而消除了这些差异。

  一个是由包头和有效载荷组成的。

  其中包头包括包的大小以及源主机和目标主机的地址,有效载荷包括从源主机发出的数据位。

 

互联网络的思想精髓:封装!

 

其实这里掩盖了一些难题:

1)不同网络有不同帧大小的最大值,该怎么办?

2)路由器如何知道往哪里转发帧?

3)当网络拓扑变化时,如何通知路由器?

4)如果一个包丢失会怎么办?

=====================================================

3、全球IP因特网

全球IP因特网时最著名和最成功的互联网络实现;

 

每台主机都运行实现TCP/IP协议的软件,几乎每个现代计算机都支持这个协议。

因特网的客户端和服务器混合使用套接字接口函数和Unix I/O函数来进行通信。

套接字函数典型地是作为会陷入内核的系统调用来实现的,并调用各种内核模式的TCP/IP函数。

 

TCP/IP实际上是一个协议族,其中每个都提供不同的功能。

其中IP协议提供基本的命名方法和递送机制。这种递送机制能够从一台因特网主机往其他主机发送包。也叫做数据报

IP机制从某种意义上而言是不可靠的,因为如果数据报在网络中丢失或者重复,它并不会试图恢复。

UDP(Unreliable Datagram Protocol,不可靠数据报协议)稍微扩展了IP协议,这样一来,包可以在进程间而不是在主机间传送。

TCP是构建在IP上的一个复杂协议。提供了进程间可靠的全双工连接。

 

这里为了简化讨论,将TCP/IP看做是一个单独的整体协议。将不讨论其内部工作。

从程序员角度,可以把因特网看做是世界范围内的主机集合:

1)主机集合被映射为一组32位的IP地址

2)这个IP地址被映射为一组称为因特网域名标识符

3)因特网主机上的进程能够通过连接和任何其他因特网主机上的进程通信。

 

IP地址

  IP地址是一个32位无符号整数。

  存放在一个结构中;

struct in_addr {

  unsigned int s_addr;

} ;

为什么要用结构来存放标量IP地址?

  把一个标量存放在一个结构中,是套接字接口早期实现的不幸产物。为IP地址定义一个标量类型应该更有意义。但现在更改已经太迟了,因为有大量的应用是基于此的了。

因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序,即大端字节顺序。

即使主机字节顺序是小端法。Unix提供了一些函数在网络和主机字节顺序之间实现转换。

 

32位也就是4个字节

IP地址通常是用点分十进制方式表示的;这种表示方式更方便人们阅读。

每个字节由它的十进制数表示,并且用句点和其他字节分开。

例如:128.2.194.242就是地址0x8002c2f2的点分十进制表示。

在Linux上,能够使用HOSTNAME命令来确定你的主机的点分十进制地址。

 

因特网程序使用inet_aton和inet_ntoa 函数来实现IP地址的点分十进制串之间的转换。

 

因特网域名

因特网客户端和服务器相互通信使用的是IP地址。但是对于人们而言,这个大整数实在是难以记住。

于是因特网定义了一组更加人性化的域名(domain name),以及一种将域名映射到IP地址的机制。

域名是一串用句点分隔的单词(字母、数字和破折号),例如:

kittyhawk.cmcl.cs.cmu.edu

域名集合形成了一个层次结构,每个域名编码了它在这个层次中的位置。

一级域名:com、edu、gov、org、net;这是由非盈利组织ICANN定义的。

下一层的二级域名:是有ICANN的各个授权代理按照先到先服务的基础分配的。一旦一个组织得到一个二级域名,那么它就可以在这个子域中创建任何新的域名了。

 

 

因特网还定义了域名集合与IP地址集合之间的映射。

直到1988年这个映射都是通过一个叫HOSTS.TXT的文本文件来手工维护的。

从那以后,这个映射是由通过分布世界范围内的数据库(DNS 域名系统)来维护的。

DNS数据库由上百万条如下图的条目,称为主机条目结构组成的,其中每条定义了一组域名和一组IP地址之间的映射。

从数学意义上来说,你可以认为每条主机条目就是一个域名和IP地址的等价类。

 //此处添加一张DNS主机条目结构图

 

因特网连接

因特网客户端和服务器通过在连接上发送和接受字节流来通信。

从连接一对进程的角度上来讲,连接是点对点的。

从数据可以同时双向流动而言,它是全双工的。

并且从由源进程发出的字节流最终被目的进程以它的发出顺序收到它的角度来说,它也是可靠的。

 

一个套接字是连接的一个端点。每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的。

 每个套接字都有一个对应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的,用“地址:端口”来表示。

当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口

而服务器套接字地址中的端口通常是某个知名的端口。是和服务相对应的。

例如Web服务器常用端口80,而电子邮件服务器常用端口25.

在Unix机器上,文件/etc/services包含一张这台机器提供的服务以及它们的知名端口号的综合列表。

 

一个连接是由它两端的套接字地址唯一确定的。

这对套接字地址叫做套接字对。由下列的元组来表示:

(cliaddr::cliport, servaddr::servport)

=====================================================

4、套接字接口

套接字接口 是一组函数。 

它们和Unix I/O函数结合起来,用以创建网络应用。

大多数现代系统上都提供套接字接口的实现。

 

套接字地址结构

  从Unix内核的角度来看,一个套接字就是通信的一个端点。

  从Unix程序的角度来看,套接字就是一个有相应描述符的打开文件。

  因特网的套接字地址存放在如下所示的类型为sockaddr_in的16字节结构中。

  对于因特网应用,sin_family成员是AF_INET,  sin_port成员是一个16位端口号,而sin_addr成员就是一个32位的IP address。

  IP地址和端口号总是以网络字节顺序(大端法)存放的。

struct sockaddr{

  unsigned short sa_family;

  char         sa_data[14];

}

 

struct sockaddr_in {

  unsigned short   sin_family;

  unsigned short   sin_port;

  struct in_addr     sin_addr;

  unsigned char    sin_zero[8];

}

_in的后缀意味着什么?这个后缀是internet的缩写,而不是输入input的缩写。

早期套接字接口的设计者面临一个问题,如何定义这些函数,使之能够接受各种类型的套接字地址结构。

现在有通用的void* 指针。但是当时并不存在这种类型的指针。

于是解决办法就是定义套接字函数要求一个指向通用sockaddr结构的指针。

然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。 

 

socket函数

客户端和服务器使用socket函数来创建一个套接字描述符

调用如下:

clientfd = Socket(AF_INET, SOCK_STREAM, 0); //出错为-1,若成功为非负描述符

AF_INET表示我们正在使用因特网,SOCK_STREAM表示这个套接字是因特网连接的一个端点。

socket函数返回的clientfd描述符是部分打开的,还不能用于读写。

如何完成打开套接字的工作,取决于我们是客户端还是服务器。

 

connect函数

客户端通过调用connect函数来建立和服务器的连接。

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);  //出错为-1,若成功为0 

connect函数试图与套接字地址为serv_addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。

connect函数会阻塞,一直到连接成功或是发生错误。

如果成功,sockfd描述符现在就准备好可以读写了,并且得到的连接是由套接字对刻画的。

 

open_clientfd函数

将socket和connect函数包装成一个叫做open_clientfd的辅助函数是很方便的。

客户端可以用它来和服务器建立连接。

int open_clientfd(char *hostname, int port);  //若成功,返回为描述符;若失败,Unix出错返回-1,DNS出错返回-2

open_clientfd函数和运行在主机hostname上的服务器建立一个连接,并在知名端口port上监听连接请求。

 

bind函数

bind、listen和accept函数被服务器用来和客户端建立连接。

 

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

bind函数告诉内核将my_addr中的服务器套接字地址和套接字描述符sockfd联系起来。

参数addrlen就是sizeof(sockaddr_in);

 

 

listen函数

客户端是发起连接请求的主动实体;

服务器是等待来自客户端连接请求的被动实体;

默认情况下,内核会认为socket函数创建的描述符为主动套接字,它存在一个连接的客户端。

服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。

 

int listen(int sockfd, int backlog); //若成功返回0 , 若出错则为-1;

listen函数将sockfd从一个主动套接字转化为一个监听套接字。该套接字可以接受来自客户端的连接请求。

backlog参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成的连接请求的数量。

backlog确切含义的理解要求对TCP/IP协议的理解,这里就不展开讨论了。

 

open_listenfd函数

将socket、bind和listen函数结合成一个叫做open_listenfd的辅助函数对我们很有帮助的。

服务器可以用它来创建一个监听描述符。 

int open_listenfd(int port);  //若成功则为描述符,若Unix出错则为-1;

open_listenfd函数打开和返回一个监听描述符,这个描述符准备好在知名端口port上接收连接请求。

 

accept函数

 服务器通过调用accept函数来等待来自客户端的连接请求。

int accept(int listenfd, struct sockaddr *addr, int *addrlen); //若成功则为非负连接描述符,若出错则为-1

 

accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接的描述符。这个描述符可以用来利用Unix I/O函数来与客户端通信。

 

监听描述符已连接描述符之间的区别使很多人感到迷惑。

监听描述符是作为客户端连接请求的一个端点。典型地,它被创建一次,并存在于服务器的整个生命周期。

已连接描述符是客户端和服务器之间已经建立起来的连接的一个端点。服务器每次接受一个连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。

 

为何要区分监听描述符和已连接描述符

事实证明这是非常有用的,因为它使得我们可以建立并发服务器,它能够同时处理很多客户端的请求。

例如,每次一个连接请求到达监听描述符时,我们可以派生一个新的进程,它通过已连接描述符与客户端通信。

 

 

echo客户端和服务器示例

 一次只能处理一个客户端的服务器叫做迭代服务器

更加复杂的服务器,叫做并发服务器,能够同时处理更多客户端。

=====================================================

5、Web服务器

 

Web基础

  Web客户端和服务器之间的交互作用的是一个基于文本的应用级协议。叫做HTTP(超文本传输协议)。 

   HTTP是一个简单的协议。一个Web客户端(浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。

   浏览器读取这些内容,并把它显示在屏幕上。

 

  Web内容可以用一个叫做HTML的超文本标记语言来编写。一个HTML程序(页)包含指令(标记),它们告诉浏览器如何显示这页中的各种文本和图形对象。

  HTML真正强大的地方在于一个页面可以包含指针(超链接),这些指针可以指向存放在任何因特网主机上的内容。

  例如:<a href="http://www.cmu.edu/index.html">Carnegie Mellon</a>

  告诉浏览器高亮显示文本对象“Carnegie Melon”,并且创建一个超链接,它指向存放在CMU Web服务器上叫做index.html的HTML文件。

  如果用户单击了这个高亮文本对象,浏览器就会从CMU服务器中请求响应的HTML文件并显示它。   

Web内容

     对于Web客户端和服务器而言,内容是一个与MIME(多用途的网际邮件扩充协议)类型相关的字节序列。 

     Web服务器以两种不同的方式向客户端提供内容

    取一个磁盘文件,并将它的内容返回给客户端。磁盘文件为静态内容,而返回文件给客户端的过程称为服务静态内容

    运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出称为动态内容。而运行程序并返回它的输出到客户端的过程称为服务动态内容

 

    每条由Web服务器返回的内容都是和它管理的某个文件相关联的。这些文件中的每一个都有一个唯一的名字,叫做URL,即统一资源定位符

    例如:URL  http://www.google.com:80/index.html

    表示因特网主机www.google.com上一个称为/index.html的HTML文件,它是由一个监听端口80的Web服务器来管理的。

    端口号是可选的,而知名的端口80,是HTTP默认的。

          可执行文件的URL可以在文件名之后包括程序参数。“?”字符分隔文件名和参数,而每个参数都用“&”字符分隔开。

     例如:http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213

    这个URL标识了/cgi-bin/adder的可执行文件,会带两个参数字符串15000和213来调用它。

 

   在事务的过程中,客户端和服务器使用的是URL的不同部分

   客户端使用前缀:http://www.google.com:80  来决定与哪类服务器联系,服务器在哪,以及它的监听端口是多少。

   服务器使用后缀:/index.html  来发现在它文件系统中的文件,并确定请求的是静态内容还是动态内容

 

  关于服务器如何解释一个URL的后缀,以下几点需要理解:

  1)确定一个URL指向的是静态内容还是动态内容没有标准的规则。每个服务器对它所管理的文件都有自己的规则。一般常见的方法是,确定一个目录,所有的可执行文件都放到这个目录里。

  2)后缀中最开始的那个/ 不表示Unix的根目录。相反,它表示的是被请求内容类型的主目录。例如,可以将一个服务器配置成这样:把所有的静态内容存放在目录/usr/httpd/html下,把所有的动态内容放在目录/usr/httpd/cig-bin下。

  3)最小的URL后缀就是只有一个 /,所有的服务器自动为其扩展为某个默认的主页,例如/index.html。

HTTP事务

  因为HTTP是基于在因特网连接上传送的文本行的,我们可以使用Unix的TELNET程序来和因特网上任何Web服务器执行事务

  对于调试通过文本行来与客户端对话的服务器来说,TELNET程序是非常便利的。

 

  1、HTTP请求

     一个HTTP请求是这样的:一个请求行(),后面跟随零个或多个请求报头(),再跟随一个空的文本行来终止报头列表(),并指示服务器发送被请求的HTML文件。

    一个请求行的格式:<method><uri><version>

    接下来解释一下这三个参数,第一个参数<method>,HTTP支持多种不同的方法,GET\POST\OPTIONS\HEAD\PUT\DELETE等;

    第二个参数:URI表示统一资源标识符,URI其实就是URL的后缀,包括文件名和可选的参数。

    第三个参数:表明该请求遵循的HTTP版本。最新的HTTP版本是HTTP/1.1[41]。

    例如:GET /HTTP/1.1  表示该请求行要求服务器取出并返回HTML文件/index.html。它也告知服务器请求剩下的部分的格式是HTTP/1.1格式的。

 

    接下来是请求报头:<header name>:<header data>

    Host报头中的数据指示了原始服务器的域名,使得代理链中的代理能够判断它是否可以在本地缓存中拥有一个被请求内容的副本。

    代理缓存会使用Host报头,这个代理缓存有时作为浏览器和管理被请求文件的服务器之间的中介。

    客户端和原始服务器之间,可以有多个代理,即所谓的代理链 

  2、HTTP响应

     HTTP响应和HTTP请求是相似的。一个HTTP响应的组成是这样的:一个响应行后面跟随者零个或多个响应报头,再跟随一个终止报头的空行,再跟随一个响应主体。

    响应行的格式是:<version>  <status code>  <status message>

    版本字段描述的是响应所遵循的HTTP版本。

    状态码是一个三位的正整数,指明对请求的处理。

    状态消息给出与错误状态码等价的英文描述。

 

服务动态内容

接下来思考一个问题,一个服务器是如何向客户端提供动态内容的?

就会发现一些问题:

客户端如何将程序参数传递给服务器?

服务器如何将这些参数传递给它所创建的子进程?

服务器如何将子进程生成的内容所需要的其他信息传递给子进程?

子进程将它的输出发送到哪里?

一个称为CGI(Common Gateway Interface)通用网关接口的实际标准的出现解决了这些问题。

 

1、客户端如何将程序参数传递给服务器

  GET请求的参数在URI中传递,正如我们看到的,一个“?”字符分隔了文件名和参数,而每个参数都用一个“&”字符分隔开。参数中不允许有空格,而必须用字符串“%20”来表示。对其他字符,也存在相似的编码。

2、服务器如何将参数传递给子进程

  在服务器接收到一个如下的请求后,GET /cgi-bin/adder?15000&213  HTTP/1.1

  它将会调用fork来创建一个子进程,并调用execve在子进程的上下文中执行/cig-bin/adder程序。像adder这样的程序统称为CGI程序,应为它们遵守CGI标准的规则。而且,因为许多CGI程序都是用Perl脚本编写的,所以CGI程序一般也叫做CGI脚本

  在调用execve之前,子进程将CGI缓解便利QUERY_STRING设置为15000&213;

  adder程序在运行时可以用Unix getenv函数来引用它。

3、服务器如何将其他信息传递给子进程

   CGI定义了大量的环境变量,一个CGI程序在它运行时可以设置这些环境变量。

  //这里添加一些环境变量的示例

4、子进程将它的输出发送到哪里

   一个CGI程序将它的动态内容发送到标准输出。在子进程加载并运行CGI程序之前,它使用Unix dup2函数将标准输出重定向到和客户端相关联的已连接描述符。

  因此,任何CGI程序写到标准输出的东西都会直接到达客户端。

  注意:由于父进程不知道子进程生成的内容的类型或大小,所以子进程就要负责生成content-type和Content-length响应报头,以及终止报头的空行。

=====================================================

6、综合:TINY Web服务器

 //这里有个编码练习,编写一个服务器

=====================================================

7、小结

每个网络应用都是基于客户端-服务器模型的。

根据这个模型,一个应用是由一个服务器和一个或多个客户端组成的。

服务器管理资源,以某种方式操作资源,为它的客户端提供服务。

客户端——服务器模型中的基本操作是客户端——服务器事务。它是由客户端请求和跟随其后的服务器响应组成的。 

 

客户端和服务器通过因特网这个全球网络来进行通信。

从一个程序员的观点来看,我们可以把因特网看成是全球范围内的主机的集合,具有以下几个属性:

  1)每个因特网主机都有一个唯一的32位名字,我们称它位IP地址;

  2)IP地址的集合被映射为一个因特网域名的集合;

  3)不同因特网主机上的进程能够通过连接相互通信;

 

客户端和服务器通过使用套接字接口建立连接。

一个套接字就是连接的一个端点,连接是以文件描述符的形式提供给应用程序的。

套接字接口提供了打开和关闭套接字描述符的函数。

客户端和服务器通过读写这些描述符来实现彼此间的通信。

 

Web服务器使用HTTP协议和它们的客户端(浏览器)彼此通信。

浏览器像服务器请求静态或动态的内容

静态内容的请求是通过从服务器上取得的文件并把它返回给客户端来服务的。

动态内容的请求时通过在服务器上一个子进程的上下文中运行一个程序并将它的输出返回给客户端来服务的。

CGI标准提供了一组规则,来管理客户端如何将程序参数传递给服务器,服务器如何将这些参数以及其他信息传递给子进程,以及子进程如何将它的输出发送回客户端。

 

分类:

技术点:

相关文章: