[CSAPP笔记][第十一章网络编程]

第十一章 网络编程

我们需要理解基本的客户端-服务端编程模型,以及如何编写使用因特网提供的服务的客户端-服务端程序。

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

11.1 客户端 - 服务器编程模型

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

  • 采用这种模型,一个应用是由一个服务器进程
    和一个或多个客户端进程组成。
    • 服务器管理某种资源,并且通过操作这种资源为它的客户端提供某种服务。

      • WEB服务器,代表客户端检索,执行磁盘内容。
      • FTP服务器,为客户端进行存储和检索。
      • 电子邮件服务器,为客户端进行读和更新。
    • 客户端-服务器模型中的基本操作是事务(transaction).

      • 一个客户端-服务器事务由四步组成
        • 客户端需要服务的时候,向服务器发送请求,发送一个事务
        • 服务器收到请求后,解释它,并以适当方式操作它的资源。
        • 服务器给客户端发送一个响应,并等待下一个请求。
        • 客户端收到响应并处理它。

11.2 网络

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

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

  • 对于物理上而言,网络是一个按照地理远近组成的层次系统。
    • 最低层是LAN(Local Area Network,局域网):在一个建筑或校园范围内。
      • 迄今为止,最流行的LAN技术是以太网(Ethernet).
        • Xerox PARC公司在20世纪70年代中期提出。

          • 以太网被证明是适应力极强的,从3 MB/s10 GB/s
        • 一个以太网段(Ethernet segment)

          • 包括一些电缆(通常是双绞线)和一个叫做集线器的小盒子。

            • 每根电缆都有相同的最大位带宽
              • 典型的是100MB/s或者1GB/S.
              • 一端连接在主机的适配器,一端连接到集线器的一个端口
            • 集线器不加分辨地将从一个端口收到的每个位复制到其他所有端口上。
              • 因此每台主机都能看到每个位。
          • 以太网段通常跨越一些小的区域。

            • 例如某建筑物的一个房间或一个楼层。

扩展介绍以太网

每个以太网适配器(网卡)都有一个全球唯一的48位地址,它存储在这个适配器的ROM上(MAC)。

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

      • 一些固定数量的头部(header)
        • 用于表示此的源,和目的地址以及此的长度。
      • 此后就是数据位的有效载荷
    • 每个主机适配器都能看到这个,但是只有目的主机实际读取它。

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

  • 一些电缆连接网桥与网桥,或者 网桥与集线器。
    • 这些电缆的带宽可以是不同的。


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

Internet和internet

我们总是用小写字母的internet表示一般概念,大写的Internet表示一种具体实现,如全球IP因特网。

  • WAN(Wide-Area Network,广域网)

互联网至关重要的特性是:

  • 它能由采用完全不同和不兼容技术的各种局域网和广域网组成。

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

A:解决办法是一层运行在每台主机和路由器上的协议软件,消除不同网络之间的差异。

  • 这个软件实现一种协议:控制主机和路由器如何协调工作来实现数据传输。
    • 必须提供两种基本能力:
      • 命名机制
        • 每台主机会被分配至少一个互联网地址(internet address),这个地址唯一标识了这台主机。
      • 传送机制
        • 协议通过定义一种把数据位捆扎成不连续的片(称为)的方式。
          • 一个是由包头有效载荷组成的。
            • 包头
              • 包的大小
              • 源主机目的主机地址
            • 有效载荷包括从源主机发出的数据位

一个客户端运行在主机A上,主机ALAN1相连,它发送了一串数据字节到运行在主机B上的服务器端,主机B则连接在LAN2上。有如下8个步骤。

  1. 运行在主机A上的客户端进行系统调用,从客户端的虚拟地址空间拷贝到内核缓冲区

  2. 主机A上的协议软件通过在数据前附加互联网络包头LAN1帧头,创建了一个LAN1的帧。

    • 互联网包头寻址到互联网主机B。(最终目的)
    • LAN1帧头寻址到路由器。(中转站)
    • 封装
      • LAN1帧有效载荷互联网络包
      • 互联网络包有效载荷是实际的用户数据。
      • 这种封装是基本的网络互联方法之一。
  3. LAN1适配器拷贝该到网络上。

  4. 到达路由器,路由器的LAN1适配器从电缆上读取它,并传送到协议软件中。

  5. 路由器从互联网包头中提取处目的互联网络地址,用它作为路由器的索引,确定向哪里转发这个包。

    • 路由器剥落旧的LAN1的帧头,加上寻址到主机B的新的LAN2帧头,并把得到的帧传送到适配器。
  6. 路由器的LAN2适配器拷贝该到网络

  7. 到达主机B时,它的适配器从电缆上读到此帧,并将它传送到协议软件。

  8. 最后,主机B上的协议软件剥落包头和帧头。服务器进行一个读取这些数据的系统调用


当然,在这里,我们掩盖了许多非常艰难的问题。

  • 如果不同的网络有不同大小的最大值,该怎么办。
  • 路由器如何知道往哪里转发
  • 网络拓扑变化的时候,如何通知路由器。
  • 包丢失了,会如何?

虽然如此,我们也能大概了解到互联网络思想的精髓。

11.3 全球IP 因特网

每台因特网主机都运行实现TCP/IP协议 (Transmission Control Protocol/Intelnet Protocol,传输控制协议/互联网络协议)的软件,几乎所有计算机系统都支持这个协议

  • 因特网的客户端和服务端混合使用套接字接口函数和Unix I/O函数来进行通信。
    • 套接字函数典型地是作为会陷入内核的系统调用来实现的,并调用各种内核模式的TCP/IP函数。

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

    • IP协议提供基本的命名方法,和传递机制。

      • 这种传递机制能够从一台因特网主机往其他主机发送包,也叫做数据报(datagram)
      • IP机制从某种意义上是不可靠的,如果数据报在网络丢失或重复,并不会试图恢复。
        • UDP(Unreliable Datagram Protocol,不可靠数据报协议)稍微扩展了IP协议。
          • 这样,可以在进程间,而不是主机间传送。
    • TCP是一个构建在IP之上的复杂协议,提供了进程间可靠地全双工(双向)的连接。

为了简化讨论

  • 我们将TCP/IP看作是一个单独的整体协议。
  • 不讨论它的内部工作,只讨论TCPIP为应用程序提供的基本功能。
  • 不讨论UDP

从程序员的角度,我们可以把因特网看作世界范围内主机的集合,满足一下特性。

  • 主机集合被映射为一组32位的IP地址。
  • 这组IP地址可以被映射为一组称为因特网域名(Internet domain name)的标示符。
  • 因特网主机上的进程能够通过连接和任何其他主机上的进程通信。

11.3.1 IP地址

一个IP地址就是一个32位无符号整数。网络程序将IP地址存放在一个IP地址结构中。

/* Internet address structure */
struct in_addr{
    unsigned int s_addr;
}

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

是早期的不幸产物,但是现在更改太迟了。

主机字节序,和网络字节序

因为因特网主机可以有不同的主机字节顺序

TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byte order)(大端,x86是小端)。

Unix提供下面这样的函数实现转换。


IP地址通常是以一种称为点分十进制表示法来表示的

  • 这里,每个字节(8位)都是由它的十进制表示(0~255),并且用句点和其他字节间分开。

  • 在Linux系统上,你能够使用hostname命令来确定你自己主机的点分十进制:

      linux> hostname -i
      10.174.204.145
    
  • 可以使用inet_atoninet_ntoa函数来实现两者之间互相转换。

11.3.2 因特网域名

方便人们记忆的对于IP的映射就是域名

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

  • 叶子结点反向到根的路径就是域名

  • 层次结构第一层 : 未命名的根结点

  • 层次结构第二层 : 一级域名(first-level domain name)

    • 由非盈利组织ICANN(Internet Corporation for Assigned Names and Numbers,因特尔分配名字数字协会)定义。
    • 常见的一级域名:com,edu,gov,orgnet
  • 层次结构第三层: 二级域名(second-level)

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

因特网定义了域名集合IP地址直接的映射。

  • HOSTS.TXT
    • 直到1988年,这个映射都是通过一个叫做HOSTS.TXT的文本文件来手工维护的。
  • DNS:
    • 之后,通过分布世界范围内的数据库(DNSDomain Name System,域名系统),来维护的。
    • DNS数据库由上百万的主机条目结构(host entry structure)组成的。
      • 定义了一组域名(一个官方名字和一个别名)和一组IP地址之间的映射。

因特网应用程序通过调用 gethostbynamegethostbyaddr函数,从DNS数据库中检索任意的主机条目。。


每台主机都有本地定义的域名localhost

  • 这个域名总是映射本地送回地址(loopback address) :127.0.0.1
  • localhost名字为引用运行在同一机器上的客户端和服务端提供了一种便利和可移植的方式。


11.3.3 因特网连接

Internet服务端和客户端通过在连接上发送和接收字节流来通信。

  • 从连接一对进程的意义上而言,连接是点对点的。
  • 从数据可以同时双向流动的角度来说,它是全双工的。
  • 并且从由源进程发出的字节流最终被目的进程按照发送的数据接收来说,它是可靠

一个套接字连接的一个端点。

  • 每个套接字都有相应的套接字地址

    • 是由一个IP地址和一个16位的整数端口组成的,用地址:端口来表示。
  • 当客户端发起一个连接请求时,客户端套接字地址中的端口由内核自动分配的。

    • 称为临时端口
  • 然后,服务器套接字地址中的端口通常是某个知名的端口,和这个服务相对应的。

    • 例如:

      • Web服务器通常使用端口80
      • 电子邮件服务器使用端口25
    • Unix机器上,文件/etc/services 包含一张这台机器提供的服务和他们的知名端口号的综合列表。

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

  • 这对套接字地址叫做套接字对(socket pair),由下列元组来表示:
    • (cliaddr:cliport,servaddr:servport)

11.4 套接字接口

套接字接口(socket interface)是一组函数,他们和Unix I/O函数结合起来,用以创建网络应用。

给出一个典型的客户端-服务器事务的上下文中套接字接口概述,以此导向。

11.4.1 套接字地址结构

不同的角度:

  • Unix内核角度来看,一个套接字就是通信的一个端点
  • Unix程序来看,套接字就是一个有相应描述符的打开文件。

Internet的套接字地址(Internet-sytle)存放在上图所示的类型为sockaddr_in的16字节结构中。

  • sin_family成员是AF_INET,ipv4还是ipv6。

  • sin_port成员是一个16位的端口号。

  • sin_addr成员就是一个32位的IP地址。

    • IP地址和端口号总是以网络字节顺序(大端法)存放的。
  • sin_zero 是填充,使得sockaddr_insockaddr一样大。

sockaddr_in给程序员操作的,sockaddr交由套接字函数使用的,两者可以直接强制转换。

11.4.2 socket函数

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

open差不多

#include<sys/types.h>
#include<sys/socket.h>

int socket(int domain, int type ,int protocol);
                返回:若成功则为非负描述符,出错为-1

我们总是带这样的参数调用socket函数:

clientfd = Socket(AF_INET,SOCK_STREAM,0);
  • AF_INET表面我们在使用IPV4协议。
  • SOCK_STREAM表示这个套接字是Internet连接的一个端点。
  • socket返回的clientfd描述符,仅仅是部分打开,还不能用于读写。
    • 如何完成打开套接字的工作,取决于我们是客户端还是服务器。
    • 下一节描述我们是客户端时如何打开套接字。

11.4.3 connect函数

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

#include<sys/socket.h>

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

connect函数试图于套接字地址为serv_addr的服务器建立一个因特网连接.

  • 其中addrlensizeof(sockaddr_in).
  • connect函数会阻塞,一直到连接成功建立或是发生错误。
  • 如果成功,sockfd描述符就可以读写了。
    • 并且得到链接是由套接字对(x:y,serv_addr.sin_addr,serv_addr.sin_port)刻画的。
      • 其中x是客户端IP地址,而y表示临时端口。
      • 它唯一地确立了客户端主机上的客户端进程。

11.4.4 open_clientfd函数

open_cilentfdsocketconnect的包装函数(不是系统自带)

#include <csapp.h>

int open_clientfd(char *hostname, int port);
                            返回:若成功则为描述符,若`Unix`出错则为-1,DNS出错则为-2.

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

  • 它返回一个打开的套接字描述符。
  • 该描述符准备好了,可以用Unix I/O函数做输入和输出。

11.4.5 bind函数

剩下的套接字函数bindlistenaccept被服务器用来和客户端建立链接。

#include<sys/socket.h>

int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
                //返回: 若成功则为0,若出错则为-1

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

  • 参数addrlen就是sizeof(sockaddr_in)

?

11.4.6 listen函数(主动套接字->监听套接字)

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

  • 默认情况下,内核会认为socket函数创建的描述符对应于主动套接字(active socket).

    • 它存在于一个连接的客户端。
  • 服务器调用listen告诉内核,描述符是被服务器而不是客户端使用的


#include<sys/socket.h>

int listen(int sockfd,int backlog);

                       返回:若成功则为0,若出错则为-1

listen函数将sockfd从一个主动套接字转化为一个监听套接字(listenning socket)

  • 该套接字可以接收来自客户端的连接请求。
  • backlog参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成连接请求的数量。
    • backlog参数的确切含义要求对TCP/IP协议的理解,这超出了我们的讨论的范围。
    • 通常我们会把它设置成一个较大的值,比如1024

11.4.7 open_listenfd函数

socket,bindlisten函数结合称open_listenfd的包装函数。

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

#include<csapp.h>

int open_listenfd(int port)

                            返回:若成功则为描述符,若Unix出错则为-1

open_listenfd函数打开和返回一个监听描述符

  • 这个描述符准备好在知名端口port上接收请求。

  • 创建listenfd套接字描述符。

  • 使用setsockopt函数来配置服务器,使得它能被立即中止和重启。

    • 默认地,一个重启的服务器将在大约30秒内拒绝客户端的连接请求,严重阻碍调试。
  • 接下来,初始化服务器的套接字地址结构

    • INADDR_ANY来告诉内核这个服务器将接收任何IP地址到端口port的请求。
      • INADDR_ANY通配符地址就是指定地址为0.0.0.0的地址
  • 调用blindlisten。将其转换为监听套接字

11.4.8 accept函数

CSAPP这里介绍的十分有问题,所以特地翻了UNIX 网络编程的原话。

  • 用于从已完成连接队列队头返回下一个已完成连接。

    • 如果已完成连接为空,那么进程进入阻塞(假定套接字为默认的阻塞方式)
  • 返回三个值

    • 已连接标示符
    • 客户端地址
    • 客户度地址长度

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

  • 监听描述符是作为客户端连接请求的一个端点。

    • 它被创建一次,并存在于服务器的整个生命周期。
  • 已连接描述符是客户端和服务器之间已经建立起来的连接的一个端点。

    • 服务器每次接收连接请求时都会创建一次。
    • 它只存在于服务器为一个客户端服务的过程中。

11.4.9 echo客户端和服务器的示例

学习套接字接口的最好办法是研究示例代码。

没办法。这个代码估计也有挺多疑惑,还是过段时间啃Unix网络编程把

11.5 WEB服务器

这块写过servlet,就不用复述了,以后再详细补

原文地址:https://www.cnblogs.com/zy691357966/p/5538418.html