服务器内部结构

  1. 服务器启动之后,需要进行准备工作,才能接收客户端的访问。

服务器需要同时和多个客户端进行通信,但一个程序来处理多个客户端的请求是很难的,因为服务器必须把握每个客户端的操作状态。
因此:每一个客户端连接进来,就启动一个新的服务器程序,确保服务器程序和客户端是一对一的

image.png

  1. 程序分为两个模块,等待连接模块和负责与客户端通信的模块
  2. 服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块,
  3. 模块先创建套接字,然后进入等待连接的暂停状态
  4. 接下来客户端发起连接,这个模块就恢复运行并接收连接,然后启动客户端通信模块,并移交完成连接的套接字
  5. 接下来客户端通信模块使用已连接的套接字与客户端进行通信,结束后,退出

每次有新的请求进来的时候,创建新的程序比较耗时,可以实现启动几个客户端通信模块,当客户端发起连接时候,从空闲的模块中挑选一个出来将套接字移交给它来处理。

服务端的套接字和端口号

客户端的数据收发

  • 创建套接字
  • 用管道连接服务器端的套接字
  • 收发数据
  • 断开管道并删除套接字

服务器端

  • 创建套接字
  • 套接字设置为等待连接状态
  • 接受连接
  • 收发数据
  • 断开管道并删除套接字

在服务器创建好套接字等待包到达的时候,程序会暂停运行,一旦客户端的包达到,就会返回响应包并开始接收连接操作
接下来,协议栈会给等待连接的套接字复制一个副本,然后将连接对象等控制信息写入新的套接字中,到这里就创建了一个新的套接字,并和客户端套接字来接在了一起
image.png

这个时候等待连接的过程也就结束了,等待连接模块会启动客户端通信模块,然后将连接好的新套接字转交给客户端通信模块,由这个模块来负责执行与客户端之间的通信操作。

  • 复制新的套接字之后,原来哪个套接字会怎么样?

还会以等待连接的状态继续存在,继续等待新的客户端连接的到来

  • 新创建套接字端口号也是一个关键:

端口号是用来识别套接字的,不同的端口号对应不同的套接字,这样就会出现问题。因为这样的话,新复制出来的套接字必须和原来的套接字端口号不同才行,这样一来,客户端原本想连接的是80端口号,结果中另一个端口返回了包,这样客户端就无法区分是服务端的哪个程序返回的。因此,新建的套接字副本必须和原来的等待连接的套接字具有相同的端口号

  • 但是这样又有一个问题:端口号是用来识别套接字的,如果一个端口号对应多个套接字,就无法用端口号定位某一个套接字了。当客户端的包达到时候,如果协议栈只看TCP头部中的接收方端口号,是无法判断包到底该交给哪个套接字的:
    • 确定某个套接字时候,不仅使用客户端套接字对应的端口,而且使用客户端端口号加上IP地址,总共使用下面四个信息来判断
      • 客户端IP地址
      • 客户端端口号
      • 服务器IP地址
      • 服务器端口号
    • 在刚刚创建端口号的时候,还没有连接,这时候四种信息是不全的,这个时候使用描述符比使用四个信息来定位套接字更简单。

服务器接收

网卡收到MAC帧,校验后检查是不是发给自己的,如果是:

  • 网卡通过中断将网络包到达的事件通知给CPU
  • CPU暂停当前任务,切换到网卡的任务,网卡驱动开始运行,从网卡缓冲区中将收到的包读取出来,根据MAC头部的以太类型字段判断协议的种类,并调用负责处理该协议的软件,比如是IP协议
  • IP模块开始工作,检查IP头部,格式、接收方地址,如不是发给自己的包,会像路由器一样根据路由表对包进行转发
  • 如果是自己的,确认分片,如果分片,则将包暂存到内存中,等所有分片全部到达之后将分片组装起来还原成原始的包
  • 检查IP头部的协议号,将其交给对应的模块。如果是TCP,交给TCP模块;如果是UDP,交给UDP模块,假设是TCP
  • 当TCP头部中控制位SYN=1时候,表示这是一个发起连接的包,TCP执行接收连接的操作,并检查包的接收方端口号,确认该端口号上有等待连接的套接字,如果没有则返回错误的ICMP

为这套接字复制一个新的副本,并将对方的IP地址、端口号、序号初始值、窗口大小等信息写入套接字,分配发送缓冲区和接收缓冲区的内存空间,生成代表确认接收的ACK号,用于从服务器向客户端发送数据的序号初始值,表示接收缓冲区剩余容量的窗口大小,并用这些信息生成TCP头部,委托给IP模块发给客户端

这个包达到客户端之后,客户端返回接受确认的ACK号,这个ACK返回服务器时候,连接完成。这时,服务器端的程序应该进入调用accpet的暂停状态,当新的套接字的描述符转交给应用程序之后,服务器恢复运行

总结:
如果收到的是发起连接的包,TCP模块会

  1. 确认TCP头部的控制位SYN
  2. 检查接收方端口号
  3. 为相应的等待连接套接字复制一个新的副本
  4. 记录发送方IP地址和端口号等信息

处理数据包

  1. TCP模块先检查接收的包对应哪个套接字
    1. 使用下面四个进行确认套接字
      • 客户端IP地址
      • 客户端端口号
      • 服务器IP地址
      • 服务器端口号
  2. 对比该套接字中保存的数据收发状态和收到的包的TCP头部中的信息是否匹配,以确定数据收发操作是否正常
    1. 根据套接字保存的上一个序号和数据长度计算下一个序号,检查收到的包中序号是否一致
    2. 一致则没有丢失,TCP模块从包中提取数据,存放到接收缓冲区,与上次收到的数据连接起来
  3. 数据进入接收缓冲区,TCP模块就会生成确认应答的TCP头部,并根据接收包的序号和计算长度计算出ACK号,然后委托IP模块发送给客户端。
  4. 收到的数据进入接收缓冲区,应用程序调用read进行读取数据,数据被转交给了应用程序
  5. 应用程序对收到的数据进行处理,检查HTTP请求消息的内容,并根据请求的内容向浏览器返回相应的数据

总结:
接收到数据包时候,TCP:

  1. 找到套接字
  2. 数据拼合起来放到缓冲区
  3. 向客户端返回ACK

TCP断开

  1. web中断开的顺序是由应用层协议决定的
    1. 1.0规定服务器先断开
    2. 1.1规定客户端先断开
  2. 具体过程
    1. 客户端调用close,TCP模块生成一个控制位FIN=1的TCP头部,委托IP模块发送给客户端
    2. 服务器收到后应答ACK
    3. 服务器调用close,生成一个FIN=1的TCP头部发给客户端
    4. 客户端返回一个ACK

运行服务器程序

通过read获取HTTP的请求信息,服务器根据收到的信息进行相应的处理,并生成相应的消息,再通过write返回给客户端。
请求消息包括一个称为“方法”的命令,例如get post,以及URI,服务器根据这些来向客户端返回数据,但是对不同的URI和方法,服务器内部的工作过程会有所不同。

  1. 使用GET的情况下,URI是一个HTML文件名
    1. 只要从服务器文件中读出HTML文档,然后将其作为响应消息返回即可。

注意,按照URI读取消息并没有那么简单,如果完全按照URI的路径和文件名读取,那就意味着磁盘上所有的文件都可以访问,Web服务器磁盘内容就全部暴漏了,就很危险。
实际上,Web服务器公开的目录并不是磁盘上的实际目录,而是虚拟目录,URI中的内容就是这个虚拟目录下的路径名。
image.png
因此需要先将虚拟路径名转化为实际路径名后,才能读取并返回数据。

  1. 如果URI指定的文件内容为HTML文档或图片,那么只要直接将文件内容作为响应消息返回给客户端就可以了;但是如果URI指定的是一个程序,服务器会先运行这个程序,然后将程序的输出返回给客户端。
    1. 如果是GET,输入的数据会被放到URI的后面交给服务器
    2. 如果是POST,输入的数据会被放到URI的消息体中发送给服务器
    3. 服务器检查到URI是一个程序以后,委托操作系统运行这个程序,然后从请求消息中取出数据交给运行的程序
      1. GET将URI后面的数据交给程序
      2. POST将消息体中的数据交给程序
    4. 将程序的输出返回给服务器,Web服务器将其作为响应消息返回给客户端

访问控制

Web服务器可以检查实现设置的一些规则,并根据规则允许或者禁止访问。
这种根据规则判断是否允许访问的功能称为访问控制。

访问控制规则主要有以下三种:

  1. 客户端IP地址
    1. 再调用accpet的时候,就已经知道了客户端的IP地址,只要检查是否允许访问就可以了
  2. 客户端域名
    1. 现根据客户端IP地址查询客户端域名,用这个IP生成DNS查询消息发给最近的DNS服务器,接下来找出负责管理这个IP的DNS服务器,找到相应的域名后返回
    2. 保险起见,再用这个域名查询一下IP地址,看看结果是不是与发送方IP地址一样
      1. 因为有一种在DNS服务器上注册假域名的攻击方式,需要进行双重检查
  3. 用户名和密码
    1. 通常请求消息中不包含用户名和密码,因此无法验证用户名和密码,因此Web服务器会向用户发送一条响应消息,告诉用户需要在请求消息中输入用户名和密码,浏览器接到这个消息以后,弹出一个输入用户名和密码的窗口,输入后浏览器将其返回给服务器,服务器进行检查
    2. image.png

返回响应

服务器调用write,将响应消息发送给协议栈
并告诉协议栈套接字的描述符
协议栈将数据拆分成多个网络包,加上头部发送出去

浏览器显示

显示内容需要判断响应消息中的数据属于哪种类型
可以根据消息头中的Content-Type值来判断:
image.png
/左边表示主类型,大分类;右边表示子类型,具体的数据类型

常见的媒体格式类型如下:

  • text/html : HTML格式
  • text/plain :纯文本格式
  • text/xml : XML格式
  • image/gif :gif图片格式
  • image/jpeg :jpg图片格式
  • image/png:png图片格式

以application开头的媒体格式类型:

  • application/xhtml+xml :XHTML格式
  • application/xml: XML数据格式
  • application/atom+xml :Atom XML聚合格式
  • application/json: JSON数据格式
  • application/pdf:pdf格式
  • application/msword : Word文档格式
  • application/octet-stream : 二进制流数据(如常见的文件下载)
  • application/x-www-form-urlencoded :
    中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)

另外一种常见的媒体格式是上传文件之时使用的:

  • multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

如果数据类型是文本,有时候还要加上编码方式:
image.png
如果消息使用了压缩或者编码技术,还需要使用Content-Encoding用来标识主体进行了何种方式的内容编码转换。

(Accept-Encoding用来标识客户端能够理解的内容编码方式)