客户端与服务器的区别

   
      服务器和客户端有什么区别呢根据用途服务器可以分为很多种类,其硬件和操作系统与客户端是有所不同的但是网络相关的部分, 如网卡、协议栈Socket 库等功能和客户端却并无二致无论硬件和 OS 如何变化,TCP IP 的功能都是一样的或者说这些功能规格都是统一的。 不过,它们的功能相同不代表用法也相同在连接过程中客户端发起连接操作,而服务器则是等待连接操作因此在 Socket 库的用法上还是有一些区别的,即应用程序调用的 Socket 库的程序组件不同 。此外,服务器的程序可以同时和多台客户端计算机进行通信这也是一点区别。因此服务器程序和客户端程序在结构上是不同的
 
 

服务器的结构

 
     服务器需要同时和多个客户端通信但一个程序来处理多个客户端的请求是很难的因为服务器必须把握每一个客户端的操作状态 。因此一般的做法是,每有一个客户端连接进来,就启动一个新的服务器程序,确保服务器程序和客户端是一对一的状态。 具体来说,服务器程序的结构如图所示
 
    首先我们将程序分成两个模块,等待连接模块a))负责与客户端通信的模块图(b))
 
1)创建等待连接模块
 
当服务器程序启动并读取配置文件完成初始化操作后就会运行等待连接模块(a)。这个模块会创建套接字然后进入等待连接的暂停状态。
 
2)等待连接模块运行并接受连接
 
接下来当客户端连发起连接时这个模块会恢复运行并接受连接, 然后启动客户端通信模块(b),并移交完成连接的套接字接下来客户端通信模块(b就会使用已连接的套接字与客户端进行通信。
 
3)通信结束后
 
通信结束后每次有新的客户端发起连接都会启动一个新的客户端通信模块b),因此(b与客户端是一对一的关系
 
 
     这样,b在工作时就不必考虑其他客户端的连接情况,只要关心自己对应的客户端就可以了通过这样的方式,可以降低程序编写的难度服务器操作系统具有多任务多线程功能,可以同时运行多个程序服务器程序的设计正是利用了这一功能。 当然,种方法在每次客户端发起连接时都需要启动新的程序,这个过程比较耗时,响应时间也会相应增加因此还有一种方法是事先启动几个客户端通信模块,当客户端发起连接时,从空闲的模块中挑选一个出来将套接字移交给它来处理。这个模块就退出了。
 

 
 

服务器端的套接字与端口号

 
       刚才我们介绍了服务器程序的大体结构但如果不深入挖掘调用Socket 库的具体过程我们还是无法理解服务器是如何使用套接字来完成通信的。因此下面就来看一看服务器程序是如何调用 Socket 库的
 
 
通过数据收发来区分
 
        首先,我们来看看客户端与服务器的区别从数据收发的角度来看,区分客户端服务器这两个固定的角色似乎不是一个好办法。现在大多数应用都是由客户端去访问服务器但其实应用的形态不止这一种。为了能够支持各种形态的应用,最好是在数据收发层面不需要区分客户端和服务器,而是能够以左右对称的方式自由发送数据。TCP 也正是在这样的背景下设计出来的。不过,这其中还是存在一个无法做到左右对称的部分,那就是连接操作连接这个操作是在有一方等待连接的情况下另一方才能发起连接,如果双方同时发起连接是不行的,因为在对方没有等待连接的状态下无法单方面进行连接。因此,只有这个部分必须区分发起连接和等待连接这两个不同的角色从数据收发的角度来看这就是客户端与服务器的区别也就是说,发起连接的一方是客户端,等待连接的一方是服务器。 这个区别体现在如何调用 Socket 库上首先客户端的数据收发需要经过下面 4 个阶段。
 
客户端
 
1创建套接字创建套接字阶段
2用管道连接服务器端的套接字连接阶段
3收发数据收发阶段
4断开管道并删除套接字断开阶段
 
 
服务器
 
相对地服务器是将阶段2改成了等待连接具体如下
 
1创建套接字创建套接字阶段
2-1将套接字设置为等待连接状态等待连接阶段
2-2接受连接接受连接阶段
3收发数据收发阶段
4断开管道并删除套接字断开阶段
 
     下面我们像前面介绍客户端时一样用伪代码来表示这个过程如图所示。
 
      首先,协议栈调用 socket 创建套接字1)),这一步和客户端是相同的 。接下来调用 bind 将端口号写入套接字中2-1))。在客户端发起连接的操作中,需要指定服务器端的端口号这个端口号也就是在这一步设置的。具体的编号是根据服务器程序的种类按照规则来确定的,例如Web 服务器使用 80 号端口。

 

 
       设置好端口号之后协议栈会调用 listen 向套接字写入等待连接状态这一控制信息(2-1))。这样一来套接字就会开始等待来自客户端的连接网络包。然后,协议栈会调用 accept 来接受连接2-2))。由于等待连接的模块在服务器程序启动时就已经在运行了,所以在刚启动时应该还没有客户端的连接包到达。可是,包都没来就调用 accept 接受连接,可能大家会感到有点奇怪,不过没关系,因为如果包没有到达,就会转为等待包到达的状态,并在包到达的时候继续执行接受连接操作
 
     因此在执行accept 的时候一般来说服务器端都是处于等待包到达的状态这时应用程序会暂停运行。在这个状态下一旦客户端的包到达就会返回响应包并开始接受连接操作。接下来协议栈会给等待连接的套接字复制一个副本,然后将连接对象等控制信息写入新的套接字中刚才我们介绍了调用 accept 时的工作过程到这里我们就创建了一个新的套接字, 并和客户端套接字连接在一起了。
 
   当 accept 结束之后等待连接的过程也就结束了这时等待连接模块会启动客户端通信模块,然后将连接好的新套接字转交给客户端通信模块, 由这个模块来负责执行与客户端之间的通信操作。之后的数据收发操作和刚才说的一样与客户端的工作过程是相同的。

 

 
 
    其实在这一系列操作中,还有一部分没有讲到那就是在复制出一个新的套接字之后,原来那个处于等待连接状态的套接字会怎么样呢
 
 
等待连接的套接字仅作为生产新副本的模板:
 
     其实它还会以等待连接的状态继续存在当再次调用 accept客户端连接包到达时,它又可以再次执行接受连接操作接受新的连接之后和刚才一样协议栈会为这个等待连接的套接字复制一个新的副本,然后让客户端连接到这个新的副本套接字上。像这样每次为新的连接创建新的套接字就是这一步操作的一个关键点。如果不创建新副本而是直接让客户端连接到等待连接的套接字上,那么就没有套接字在等待连接了这时如果有其他客户端发起连接就会遇到问题。为了避免出现这样的情况协议栈采用了这种创建套接字的新副本,并让客户端连接到这个新副本上的方法
 
 
新副本与模板套接字同端口号
 
      此外,创建新套接字时端口号也是一个关键点端口号是用来识别套接字的,因此我们以前说不同的套接字应该对应不同的端口号但如果这样做,这里就会出现问题因为在接受连接的时候新创建的套接字副本就必须和原来的等待连接的套接字具有不同的端口号才行。这样一来比如客户端本来想要连接 80 端口上的套接字结果从另一个端口号返回了包,这样一来客户端就无法判断这个包到底是要连接的那个对象返回的, 还是其他程序返回的。因此,新创建的套接字副本必须和原来的等待连接的套接字具有相同的端口号。但是这样一来又会引发另一个问题。端口号是用来识别套接字的如果一个端口号对应多个套接字,就无法通过端口号来定位到某一个套接字了。当客户端的包到达时如果协议栈只看 TCP 头部中的接收方端口号,是无法判断这个包到底应该交给哪个套接字的。 这个问题可以用下面的方法来解决,即要确定某个套接字时,不仅使用服务器端套接字对应的端口号,还同时使用客户端的端口号再加上 IP 地 址总共使用下面 4 种信息来进行判断
 
 

 

      服务器上可能存在多个端口号相同的套接字,但客户端的套接字都是对应不同端口号的,因此我们可以通过客户端的端口号来确定服务器上的某个套接字不过使用不同端口号的规则仅限一台客户端的内部当有多个客户端进行连接时,它们之间的端口号是可以重复的因此我们还必须加上客户端的 IP 地址才能进行判断
 
 
描述符和端口号的区别
 
     例如IP 地址为 198.18.203.154的客户端的 1025 端口就和 IP 地址为 198.18.142.86 的客户端的 1025 口对应不同的套接字如果能够理解上面这些内容,那么关于套接字和端口号的知识就已经掌握得差不多了。说句题外话,既然通过客户端 IP 地址客户端端口号服务器 IP 地址、服务器端口号这 4 种信息可以确定某个套接字那么要指代某个套接字时用这 4 种信息就好了为什么还要使用描述符呢这个问题很好不过我们无法用上面 4 种信息来代替描述符原因是在套接字刚刚创建好还没有建立连接的状态下,这 4 种信息是不全的。此外,为了指代一个套接字,使用一种信息(描述符)比使用 4 种信息要简单。出于上面两个原因,应用程序和协议栈之间是使用描述符来指代套接字的
 

 

 

 

服务器的接收操作

     
1)电信号转数字信号
    

      现在, 客户端发送的网络包已经到达了服务器。到达服务器的网络包其本质是电信号或者光信号,接收信号的过程和客户端是一样的。接收操作的第一步是网卡接收到信号,然后将其还原成数字信息 域网中传输的网络包信号是由 1 0 组成的数字信息与用来同步的时钟信号叠加而成的,因此只要从中分离出时钟信号然后根据时钟信号进行同步,就可以读取并还原出 1 0 的数字信息了。 信号的格式随传输速率的不同而不同,因此某些操作过程可能存在细微差异,例如 10BASE-T 的工作方式如图所示首先从报头部分提取出时钟信号),报头的信号是按一定频率变化的只要测定这个变化的频率就可以和时钟信号同步了。接下来按照相同的周期延长时钟信号图  ②),并在每个时钟周期位置检测信号的变化方向 )。图中用向上和向下的箭头表示变化方向,实际的信号则是正或负的电压这里需要检测电压是从正变为负,还是从负变为正这两种变化方向分别对应 0 1 (图 )。在图中向上的箭头为 1向下的箭头为 0实际上是从负到正变化为 1从正到负变化为 0这样信号就被还原成数字信息了

 

 

2)FCS校验

      接下来需要根据包末尾的帧校验序列(FCS)来校验错误即根据校验公式计算刚刚接收到的数字信息然后与包末尾的 FCS 值进行比较。 FCS 值是在发送时根据转换成电信号之前的数字信息进行计算得到的, 因此如果根据信号还原出的数字信息与发送前的信息一致,则计算出的 FCS 也应该与包末尾的 FCS一致如果两者不一致则可能是因为噪声等影响导致信号失真,数据产生了错误这时接收的包是无效的因此需要丢弃
 
3)检查MAC头部
 
     当 FCS 一致即确认数据没有错误时接下来需要检查 MAC 头部中的接收方 MAC 地址,看看这个包是不是发给自己的以太网的基本工作方式是将数据广播到整个网络上,只有指定的接收者才接收数据因此网络中还有很多发给其他设备的数据在传输,如果包的接收者不是自己那么就需要丢弃这个包。
 
4)中断:提醒CPU
 
    到这里,接收信号并还原成数字信息的操作就完成了还原后的数字信息被保存在网卡内部的缓冲区中。上面这些操作都是由网卡的 MAC 来完成的。在这个过程中,服务器的 CPU 并不是一直在监控网络包的到达而是在执行其他的任务,因此 CPU 并不知道此时网络包已经到达了但接下来的接收操作需要 CPU 来参与因此网卡需要通过中断将网络包到达的事件通知给 CPU。接下来,CPU 就会暂停当前的工作并切换到网卡的任务
 
5)运行网卡驱动,确定协议类型
 
      然后网卡驱动会开始运行,从网卡缓冲区中将接收到的包读取出来根据 MAC头部的以太类型字段判断协议的种类,并调用负责处理该协议的软件这里,以太类型的值应该是表示 IP 协议因此会调用 TCP/IP 协议栈并将包转交给它
 
6)调用IP模块
 
    当网络包转交到协议栈时IP 模块会首先开始工作检查 IP 头部IP模块首先会检查 IP 头部的格式是否符合规范,然后检查接收方 IP 地址, 看包是不是发给自己的当服务器启用类似路由器的包转发功能时对于不是发给自己的包会像路由器一样根据路由表对包进行转发确认包是发给自己的之后,接下来需要检查包有没有被分片检查 IP头部的内容就可以知道是否分片 如果是分片的包则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包如果没有分片,则直接保留接收时的样子不需要进行重组到这里我们就完成了包的接收。接下来需要检查 IP 头部的协议号字段,并将包转交给相应的模块例如,如果协议号为 06十六进制),则将包转交给 TCP 模块如果是 11十六进制),则转交给 UDP 模块这里我们假设这个包被交给 TCP 模块处理,然后继续往下看。
 

 

7)调用TCP模块:TCP处理连接包

      前面的步骤对于任何包都是一样的但后面的 TCP 模块的操作则根据包的内容有所区别
   
     首先我们来看一下发起连接的包是如何处理的。 当 TCP 头部中的控制位 SYN 1 表示这是一个发起连接的包(图 )。这时TCP 模块会执行接受连接的操作不过在此之前,需要先检查包的接收方端口号,并确认在该端口上有没有与接收方端口号相同且正在处于等待连接状态的套接字如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包。如果存在等待连接的套接字,则为这个套接字复制一个新的副本将发送方 IP 地址端口号、序号初始值、窗口大小等必要的参数写入这个套接字中,同时分配用于发送缓冲区和接收缓冲区的内存空间然后生成代表接收确认的 ACK 用于从服务器向客户端发送数据的序号初始值, 表示接收缓冲区剩余容量的窗口大小,并用这些信息生成 TCP 头部,委托IP模块发送给客户端 。这个包到达客户端之后,客户端会返回表示接收确认的 ACK 当这个 ACK 号返回服务器后连接操作就完成了。这时,服务器端的程序应该进入调用 accept 的暂停状态当将新套接字的描述符转交给服务器程序之后,服务器程序就会恢复运行
 
如果收到的是发起连接的包,则 TCP 模块会:
 
(1) 确认 TCP 头部的控制位 SYN;
(2) 检查接收方端口号;
(3) 为相应的等待连 接套接字复制一个新的副本;
(4) 记录发送方 IP 地址和端口号等信息。
 

 

8)调用TCP模块:TCP处理数据包

      接下来我们来看看进入数据收发阶段之后当数据包 到达时 TCP 模块是如何处理的( )。
 
      首先,TCP 模块会检查收到的包对应哪一个套接字在服务器端可能有多个已连接的套接字对应同一个端口号,因此仅根据接收方端口号无法找到特定的套接字。这时我们需要根据 IP 头部中的发送方 IP 地址和接收方 IP 地址,以及 TCP 头部中的接收方端口号和发送方端口号共 4 种信息,找到上述 4 种信息全部匹配的套接字
 
      找到 4 种信息全部匹配的套接字之后TCP模块会对比该套接字中保存的数据收发状态和收到的包的 TCP 头部中的信息是否匹配以确定数据收发操作是否正常。具体来说,就是根据套接字中保存的上一个序号和数据长度计算下一个序号,并检查与收到的包的 TCP 头部中的序号是否一致如果两者一致就说明包正常到达了服务器没有丢失这时TCP模块会从包中提出数据,并存放到接收缓冲区中与上次收到的数据块连接起来。这样一来数据就被还原成分包之前的状态了当收到的数据进入接收缓冲区后,TCP 模块就会生成确认应答的 TCP头部并根据接收包的序号和数据长度计算出 ACK 号然后委托 IP 模块
发送给客户端。 收到的数据块进入接收缓冲区,意味着数据包接收的操作告一段落了
 
    接下来,应用程序会调用 Socket 库的 read 来获取收到的数据, 这时数据会被转交给应用程序。果应用程序不来获取数据,则数据会被一直保存在缓冲区中但一般来说应用程序会在数据到达之前调用 read等待数据到达,在这种情况下TCP 模块在完成接收操作的同时就会执行将数据转交给应用程序的操作。然后,控制流程会转移到服务器程序对收到的数据进行处理也就是检查 HTTP 请求消息的内容并根据请求的内容向浏览器返回相应的数据。

 

收到数据包时,TCP 模块会:
 
(1) 根据收到的包的发送方 IP 地址、发送方端口号、接收方 IP 地址、接收方端口号找到相对应的套接字;
(2) 将数据块拼合起来并保存在接收缓冲区中;
(3) 向客户端返回 ACK。
 
 
 
9)调用TCP模块:断开连接操作
 
      当数据收发完成后便开始执行断开操作这个过程和客户端是一样的,让我们简单复习一下。在 TCP 协议的规则中断开操作可以由客户端或服务器任何一方发起,具体的顺序是由应用层协议决定的Web 这一顺序随 HTTP 协议版本不同而不同,HTTP1.0 是服务器先发起断开操作。这时,服务器程序会调用 Socket 库的 closeTCP 模块会生成一个控制位 FIN 1 TCP 头部并委托 IP 模块发送给客户端当客户端收到在返回 ACK 号之前,会先等待一段时间,看看能不能和后续的应答包合并。这个包之后会返回一个 ACK 接下来客户端调用 close生成一个FIN 1 TCP 头部发给服务器服务器再返回 ACK 这时断开操作就完成了HTTP1.1 是客户端先发起断开操作这种情况下只要将客户端和服务器的操作颠倒一下就可以了无论哪种情况,当断开操作完成后,套接字会在经过一段时间后被删除。
 
 
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐