Distributed System


分布式系统 总复习2023春季学期 QingDao University

总复习

概述和体系结构模块

分布式计算中透明性的含义

在分布式计算中,透明性是指隐藏系统内部的复杂性,使得分布式系统对用户和应用程序表现为单一的、统一的实体,而不暴露其分布性和并发性。透明性是为了简化分布式系统的设计、开发和使用,使得用户和应用程序可以像使用单个计算资源一样使用分布式系统,而无需了解其内部的复杂性。

  1. 访问透明性(Access Transparency):用户和应用程序可以透明地访问分布式系统中的资源,无需关心资源的物理位置和访问细节。这使得用户可以像访问本地资源一样访问远程资源。
  2. 位置透明性(Location Transparency):用户和应用程序可以透明地访问分布式系统中的资源,而不需要了解资源的具体物理位置。系统会根据资源的可用性和负载情况进行适当的资源定位和路由。
  3. 迁移透明性(Migration Transparency):系统可以在不中断用户和应用程序的情况下迁移资源或重新配置资源。用户和应用程序无需感知资源的迁移和重新配置过程。
  4. 并发透明性(Concurrency Transparency):系统可以透明地处理并发操作,即使多个用户或应用程序同时访问和修改资源,也不会发生冲突或数据损坏。
  5. 故障透明性(Failure Transparency):系统可以透明地处理组件故障或网络故障。用户和应用程序无需感知故障的发生,并且系统能够自动处理故障,并保证服务的可用性和可靠性。

分布式体系结构有哪些?

  1. 客户端-服务器体系结构(Client-Server Architecture):这是最常见的分布式体系结构之一,其中客户端应用程序通过网络连接到服务器,并向服务器发送请求以获取服务或数据。服务器接收请求并提供相应的服务或数据。这种体系结构可以实现资源集中管理和共享,同时提供高可用性和可扩展性。
  2. 对等(Peer-to-Peer)体系结构:在对等体系结构中,节点之间没有明确的客户端和服务器角色区分,所有节点都可以充当客户端和服务器。节点之间通过直接通信进行交互和资源共享,形成一个去中心化的网络。对等体系结构通常用于文件共享、点对点通信和分布式计算等场景。
  3. 三层体系结构(Three-Tier Architecture):该体系结构将应用程序分为三个层次:客户端界面层、应用程序逻辑层和数据存储层。客户端界面层处理用户与系统的交互,应用程序逻辑层处理业务逻辑和请求处理,数据存储层负责数据的存储和访问。每个层次可以运行在不同的物理或逻辑节点上,实现分布式的功能和负载均衡。
  4. 微服务体系结构(Microservices Architecture):微服务体系结构是一种将应用程序拆分为多个独立的小型服务的架构风格。每个微服务都是独立部署、可独立扩展和管理的,通过轻量级的通信机制进行交互。微服务体系结构可以提高开发速度、灵活性和可伸缩性,同时允许不同的团队独立开发和维护各自的服务。

区分软件体系结构和系统体系结构

在分布式系统中,常见的逻辑体系结构包括以下几种:

  1. 客户端-服务器(Client-Server)体系结构:这是最常见的分布式系统体系结构,其中客户端和服务器之间进行通信和交互。客户端发送请求,服务器提供相应的服务并返回结果。
  2. 对等网络(Peer-to-Peer)体系结构:在对等网络中,系统中的节点可以相互通信和交互,没有明确的客户端和服务器之分。每个节点既可以提供服务,也可以请求服务。节点之间的通信是对等的,数据和功能可以在节点之间共享和分发。
  3. 发布-订阅(Publish-Subscribe)体系结构:在发布-订阅体系结构中,消息的发布者(发布者)将消息发布到一个或多个主题(或频道),而订阅者(订阅者)可以选择订阅感兴趣的主题并接收相应的消息。这种体系结构支持一对多的通信模式。
  4. 分层体系结构:分层体系结构将系统划分为多个层级,每个层级负责特定的功能和任务。上层的组件可以通过下层的组件提供的接口和服务进行通信和交互。这种体系结构简化了系统的设计和维护,提高了系统的可扩展性和可重用性。
  5. 微服务体系结构:微服务体系结构将系统划分为一组小型、自治的服务单元,每个服务单元专注于特定的业务功能。这些服务单元可以独立部署、扩展和管理,通过轻量级的通信机制进行交互。微服务体系结构支持系统的解耦和灵活性。

系统体系结构是指整个计算系统的结构和组织方式,包括硬件、软件和各种外部组件。下面是几种常见的系统体系结构:

  1. 单层体系结构(Single-Tier Architecture):也称为单一体系结构或独立体系结构,系统的所有组件都部署在同一层级上。这种体系结构适用于简单的应用程序,其中用户界面、业务逻辑和数据存储等功能集中在一个单一的环境中。
  2. 客户端-服务器体系结构(Client-Server Architecture):系统被分为客户端和服务器两个部分,客户端负责发送请求和接收响应,服务器负责处理请求并提供相应的服务。客户端和服务器之间通过网络进行通信,可以是两层、三层或多层的体系结构。
  3. 分布式体系结构(Distributed Architecture):系统的组件部署在多个计算机节点上,这些节点通过网络进行通信和协作。分布式体系结构支持并行处理、资源共享和容错性,并允许系统在不同地理位置部署。
  4. 多层体系结构(Multilayer Architecture):系统被分为多个层级,每个层级负责特定的功能和任务。常见的多层体系结构包括三层体系结构(表示层、业务逻辑层和数据访问层)和四层体系结构(表示层、业务逻辑层、应用层和数据访问层)等。多层体系结构提高了系统的可维护性、可扩展性和灵活性。
  5. 云体系结构(Cloud Architecture):系统基于云计算平台构建,利用云服务提供的资源和功能。云体系结构可以包括公有云、私有云或混合云,以满足不同的需求和隐私要求。

C/S模式、P2P模式、边界服务器系统等实例的体系结构

P2P(Peer-to-Peer)是一种分布式网络架构,其中计算机节点之间具有对等的地位,可以相互通信和协作,而无需集中的中央服务器。以下是从不同方面介绍P2P的内容:

  1. 架构特点:
    • 对等通信:P2P网络中的节点相互平等,可以充当客户端和服务器角色,可以请求服务和提供服务。
    • 分散化:P2P网络中没有中央控制节点,系统的决策和管理分布在各个节点上,提高了系统的可扩展性和容错性。
    • 自组织性:P2P网络具有自组织的能力,新节点可以加入网络并参与协作,网络的拓扑结构可以动态调整和优化。
  2. 通信方式:
    • 直接通信:P2P节点可以直接与其他节点进行通信,通过互联网或局域网建立点对点的连接。
    • 中继通信:在某些情况下,由于网络拓扑或防火墙等限制,节点之间无法直接通信,这时可以借助其他节点进行中继传输。
  3. 应用领域:
    • 文件共享:P2P文件共享网络允许用户直接分享和下载文件,如BitTorrent等。
    • 即时通信:P2P即时通信允许用户实时交流和消息传递,如Skype、Bitmessage等。
    • 分布式计算:P2P分布式计算允许多个节点共同参与计算任务,如分布式哈希表、分布式存储等。
    • 区块链技术:区块链是一种基于P2P网络的分布式账本技术,如比特币、以太坊等。
  4. 优点:
    • 去中心化:P2P架构避免了单点故障和集中控制,提高了系统的可靠性和可用性。
    • 可扩展性:P2P网络具有良好的可扩展性,可以容纳大量的节点并处理大规模的数据和请求。
    • 共享资源:P2P网络允许节点共享资源和服务,提高了资源的利用率和效率。
  5. 挑战和解决方案:
    • 网络拓扑:P2P网络的拓扑结构对性能和可用性有影响,需要采取合适的算法和协议来优化拓扑结构。
    • 安全和隐私:P2P网络中的节点可能存在安全和隐私问题,需要采取身份验证、加密和访问控制等措施保护系统安全。
    • 数据一致性:由于节点之间的分散和异步操作,P2P网络中的数据一致性是一个挑战,需要采取合适的一致性协议和算法。

C/S(Client/Server)是一种计算机体系结构,其中客户端和服务器之间存在一种分工合作的关系。以下是从不同方面介绍C/S的内容:

  1. 架构特点:
    • 分工合作:C/S架构将系统功能分为客户端和服务器两部分,客户端负责用户界面和用户交互,服务器负责处理请求和提供服务。
    • 中心化:C/S架构中存在一个中心服务器,负责协调和管理客户端的请求和资源。
    • 高可靠性:C/S架构通过服务器集中处理和管理数据和业务逻辑,提高了系统的可靠性和稳定性。
  2. 通信方式:
    • 请求-响应模式:客户端向服务器发送请求,服务器处理请求并返回响应给客户端。
    • 双向通信:客户端和服务器之间可以进行双向通信,可以发送请求和接收服务器推送的数据。
  3. 应用领域:
    • 客户端应用程序:C/S架构适用于各种客户端应用程序,如桌面应用、移动应用等。客户端应用程序通过与服务器通信获取数据和服务。
    • 数据库管理系统:数据库管理系统(DBMS)采用C/S架构,客户端通过连接到服务器来管理和操作数据库。
  4. 优点:
    • 可扩展性:C/S架构允许在需要时增加服务器的数量,以应对用户量增加和系统负载的增加。
    • 灵活性:C/S架构可以根据不同的需求和应用场景进行定制开发,满足特定的功能和业务需求。
    • 安全性:通过服务器集中管理数据和业务逻辑,可以实施安全控制措施,提高系统的安全性。
  5. 挑战和解决方案:
    • 性能:C/S架构的性能受限于服务器的处理能力和网络带宽,需要优化服务器端的资源管理和通信机制,以提高系统的性能。
    • 协议和通信:客户端和服务器之间的通信需要定义和实现特定的协议和接口,确保数据的正确传输和解释。
    • 单点故障:由于C/S架构中存在中心服务器,一旦服务器出现故障,整个系统可能受到影响。为了避免单点故障,可以使用负载均衡和容错机制。

边界服务器系统是一种常见的计算机系统架构,它在网络中扮演着重要的角色。以下是从各方面介绍边界服务器系统的内容:

  1. 定义与作用:
    • 边界服务器系统是位于网络边界的一组服务器,位于内部网络和外部网络之间。
    • 边界服务器系统充当了内部网络和外部网络之间的中转站和保护屏障,提供了访问控制、安全防护和流量管理等功能。
  2. 功能与特点:
    • 访问控制:边界服务器系统可以对外部网络请求进行认证和授权,限制访问内部网络的权限和范围。
    • 安全防护:边界服务器系统通过防火墙、入侵检测和防护系统等,保护内部网络免受恶意攻击和未经授权的访问。
    • 负载均衡:边界服务器系统可以分担内部服务器的负载,通过负载均衡算法将请求分发到多个内部服务器上,提高系统的性能和可用性。
    • 缓存和加速:边界服务器系统可以缓存静态内容和常用数据,减少对内部服务器的访问压力,并提供快速响应给客户端。
    • 日志记录与分析:边界服务器系统可以记录和分析网络流量、安全事件和访问日志,用于监控和审计网络活动。
  3. 部署方式:
    • 反向代理:边界服务器可以充当反向代理服务器,将客户端请求转发到合适的内部服务器上,并将响应返回给客户端。
    • VPN网关:边界服务器可以作为虚拟专用网络(VPN)的网关,处理来自外部网络的VPN连接请求,将其转发到内部网络中的目标主机。
    • 防火墙与安全网关:边界服务器可以配置为防火墙和安全网关,通过规则和策略过滤和控制网络流量,保护内部网络的安全。
  4. 优势与挑战:
    • 优势:边界服务器系统提供了安全保护、访问控制和性能优化等功能,可以提升网络安全性、可用性和性能。
    • 挑战:边界服务器系统的部署和管理需要一定的专业知识和配置,且需要定期维护和更新以应对新的安全威胁。

中间件的基本概念及其实现

中间件是指位于应用程序和操作系统之间的软件层,用于协调和支持分布式系统中的不同组件和服务之间的通信和交互。它提供了一系列功能和服务,使得应用程序开发者可以更加方便地构建、部署和管理分布式应用。

中间件的基本概念包括以下几个方面:

  1. 抽象和封装:中间件屏蔽了底层系统和网络的细节,提供了高层次的抽象接口和功能,使得应用程序可以独立于具体的操作系统和网络环境。
  2. 通信和协议:中间件提供了通信机制和协议,使得分布式系统中的不同组件可以进行可靠的通信和数据交换。常见的通信方式包括消息传递、远程过程调用(RPC)、消息队列等。
  3. 分布式事务:中间件可以支持分布式事务的管理和协调,确保在分布式环境下多个操作的一致性和原子性。
  4. 容错和可靠性:中间件可以提供容错和故障恢复机制,例如集群、复制和备份等,确保系统在发生故障时能够继续运行并保持可靠性。
  5. 安全和权限管理:中间件提供了安全机制和权限管理,用于保护分布式系统中的数据和资源不受未经授权的访问和恶意攻击。

中间件的实现可以通过各种方式和技术来完成,包括以下几种常见的实现方式:

  1. 消息队列中间件:通过消息队列实现异步通信和解耦,常见的消息队列中间件包括RabbitMQ、Apache Kafka等。
  2. 远程过程调用中间件:通过远程过程调用实现分布式函数调用和方法调用,常见的远程过程调用中间件包括gRPC、Apache Dubbo等。
  3. 数据库中间件:通过数据库中间件实现数据的分片、缓存和路由等功能,常见的数据库中间件包括MySQL Proxy、MyBatis等。
  4. 服务总线中间件:通过服务总线实现服务的发布、订阅和路由,常见的服务总线中间件包括Apache ServiceMix、MuleSoft等。
  5. 分布式缓存中间件:通过分布式缓存实现数据的高速缓存和共享,常见的分布式缓存中间件包括Redis、Memcached等。

总之,中间件在分布式系统中发挥着重要的作用,通过提供统一的接口和功能,简化了分布式应用的开发和部署,提高了系统的可靠性、可扩展性和性能。

进程模块

线程的实现方法

线程的实现方法有多种,下面介绍几种常见的线程实现方式:

  1. 原生线程库:大多数编程语言和操作系统都提供了原生的线程库,可以使用这些库来创建和管理线程。例如,C++中的<thread>库、Java中的java.lang.Thread类以及Python中的threading模块都提供了创建和操作线程的接口。
  2. 线程池:线程池是一种管理和复用线程的机制。通过创建一个线程池,可以预先创建一组线程,并将任务提交给线程池来执行。线程池会自动管理线程的生命周期,包括线程的创建、复用和回收,从而避免了频繁创建和销毁线程的开销。许多编程语言和框架都提供了线程池的实现,例如Java中的ExecutorService接口。
  3. 协程:协程是一种轻量级的线程替代方案,通过协程可以实现并发执行和任务切换,而无需创建和管理多个线程。协程可以在执行过程中主动让出CPU控制权,并在适当的时候恢复执行,从而提高并发性能和资源利用率。许多编程语言都提供了协程的支持,例如Python的asyncio库和Go语言的协程(goroutine)。
  4. 异步编程:异步编程是一种基于事件驱动的编程模型,通过使用异步回调、事件循环或者Promise等机制,可以在单线程中处理多个任务的并发执行。异步编程适用于I/O密集型任务,可以避免阻塞和线程创建的开销。许多编程语言和框架都提供了异步编程的支持,例如JavaScript中的Promise和async/await,以及Python中的asyncio。

单线程进程与多线程进程的区别

单线程进程和多线程进程是指在操作系统中运行的两种不同类型的进程,它们的区别主要体现在以下几个方面:

  1. 执行能力:单线程进程只能同时执行一个任务或指令,而多线程进程可以同时执行多个任务或指令。多线程进程可以将任务划分为多个线程,在多个线程之间共享进程的资源和状态,从而实现并发执行。
  2. 并发性:多线程进程可以提高并发性,即在同一时间内执行多个任务。通过将任务分配给不同的线程,可以利用多核处理器或多个CPU来实现并行处理,提高系统的响应性和效率。而单线程进程只能按照顺序执行任务,无法并行处理。
  3. 资源共享和同步:在多线程进程中,不同的线程可以共享进程的资源和内存空间,因此可以更方便地进行数据共享和通信。然而,多个线程同时访问共享资源可能会引发并发访问的问题,需要采取同步机制来确保数据的一致性和线程的安全性。而单线程进程不需要考虑资源共享和同步的问题,因为只有一个线程在执行。
  4. 编程复杂性:多线程编程相对于单线程编程来说更复杂一些。在多线程环境下,需要考虑线程间的同步、互斥和通信,以避免竞态条件、死锁和资源泄漏等并发编程问题。而单线程编程相对较简单,无需考虑并发性和线程间的交互。

总的来说,多线程进程在并发性和资源利用上具有优势,可以实现更高的系统性能和响应性。但同时也带来了并发编程的复杂性和需要注意的问题。而单线程进程相对简单,适用于一些简单的任务或者无需并发处理的场景。选择使用单线程还是多线程进程取决于具体的应用需求和性能要求。

虚拟机体系结构、虚拟化技术

虚拟机体系结构是一种将物理计算机资源抽象为虚拟计算机的技术架构。它通过虚拟化技术,使得多个虚拟机可以在同一台物理计算机上同时运行,每个虚拟机都具有自己的操作系统和应用程序,彼此之间相互隔离。

虚拟化技术是实现虚拟机体系结构的核心技术,它主要包括以下几种类型:

  1. 完全虚拟化(Full Virtualization):完全虚拟化技术通过在物理计算机上创建一个称为虚拟机监视器(Hypervisor)的软件层,来模拟硬件资源并管理虚拟机。虚拟机监视器负责将虚拟机对硬件资源的请求转发到物理计算机上,并确保不同虚拟机之间的隔离性。常见的完全虚拟化解决方案有VMware、KVM等。
  2. 部分虚拟化(Para-virtualization):部分虚拟化技术通过修改客户操作系统内核,使其与虚拟化层进行协同工作,以提高性能和效率。客户操作系统需要被特别修改才能运行在部分虚拟化的环境中。常见的部分虚拟化解决方案有Xen等。
  3. 操作系统级虚拟化(OS-level Virtualization):操作系统级虚拟化技术利用操作系统内核的功能来实现虚拟化,而无需使用虚拟机监视器。在操作系统级虚拟化中,主机操作系统允许多个独立的虚拟环境(容器或虚拟化实例)共享同一套操作系统内核,每个虚拟环境都被视为一个隔离的操作系统实例。常见的操作系统级虚拟化解决方案有Docker、LXC等。

虚拟机体系结构和虚拟化技术使得计算机资源能够更好地利用和管理,提供了更灵活、可扩展和高效的计算环境。它们被广泛应用于服务器虚拟化、云计算、测试和开发环境等领域,为用户提供了更多的选择和便利。

设计服务器需要注意的问题,及服务器集群组织方式

在设计服务器时,有几个重要的问题需要考虑:

  1. 性能:服务器的性能是关键因素之一。需要考虑处理器性能、内存容量、硬盘速度和网络带宽等方面,以满足预期的工作负载和用户需求。
  2. 可靠性:服务器的可靠性是确保系统连续运行和数据完整性的关键。需要考虑使用可靠的硬件组件、冗余机制和备份策略,以及监控和报警系统来及时发现和处理故障。
  3. 可扩展性:服务器的可扩展性是指能够根据需要动态调整和扩展系统的能力。需要考虑系统架构的弹性,如水平扩展和垂直扩展,以支持更多的用户和增加的工作负载。
  4. 安全性:服务器的安全性是非常重要的。需要采取安全措施来保护服务器和数据,如访问控制、身份验证、加密通信和漏洞修补等。
  5. 管理和监控:服务器需要进行有效的管理和监控,以确保系统的稳定性和高效性。需要考虑使用合适的管理工具和监控系统来跟踪服务器的性能、资源利用率和事件日志。

服务器集群组织方式主要有以下几种:

  1. 对等集群(Peer-to-Peer):在对等集群中,每个服务器都具有相同的地位,可以相互通信和共享资源。对等集群通常用于分布式计算和文件共享等场景。
  2. 主从集群(Master-Slave):在主从集群中,有一个主服务器(Master)和多个从服务器(Slave)。主服务器负责接收请求和处理业务逻辑,而从服务器负责辅助处理和备份。主从集群常用于负载均衡和高可用性要求较高的场景。
  3. 分布式集群(Distributed Cluster):分布式集群是由多台服务器组成的集群,各个服务器相互协作完成任务。分布式集群可以通过分片(Sharding)和副本(Replication)等方式来分散数据和负载,提高系统的吞吐量和可扩展性。
  4. 负载均衡集群(Load Balancing Cluster):负载均衡集群通过在多个服务器之间分配负载,以提高系统的性能和可靠性。负载均衡器(Load Balancer)可以根据不同的负载均衡策略将请求分发到不同的服务器上,实现负载均衡。

命名系统模块

命名的基本概念、3种命名系统类型

命名是指为实体、对象或概念赋予一个可识别和唯一的标识符或名称的过程。命名的基本概念包括:

  1. 标识符(Identifier):用于唯一标识一个实体的符号或名称。标识符可以是字符串、数字、符号或其他形式的标记。
  2. 命名空间(Namespace):命名空间是一个用于组织和管理标识符的容器。它提供了一种避免标识符冲突的机制,使得不同的实体可以在不同的命名空间中被唯一标识。
  3. 解析(Resolution):解析是将标识符映射到相应的实体或对象的过程。解析可以通过查找命名空间中的映射关系、引用表或其他方式来完成。

根据命名系统的不同特点和应用场景,可以将命名系统分为以下三种类型:

  1. 层次命名系统(Hierarchical Naming System):层次命名系统采用树状结构组织命名空间,通过父子关系将命名空间划分为不同的层次。每个层次可以包含子命名空间或实体标识符。常见的层次命名系统包括域名系统(DNS)和文件系统中的路径命名。
  2. 平坦命名系统(Flat Naming System):平坦命名系统不采用层次结构,而是将所有标识符置于同一级别。标识符之间没有父子关系,而是独立存在。平坦命名系统可以更直接地访问标识符,但容易导致命名冲突。常见的平坦命名系统包括全局唯一标识符(GUID)和全局唯一名称(GUN)。
  3. 结构化命名系统(Structured Naming System):结构化命名系统通过采用特定的命名规则和语法,将标识符划分为不同的组成部分,并赋予每个部分特定的含义。这种命名系统可以提供更丰富的语义信息,并支持更复杂的命名模式。常见的结构化命名系统包括统一资源定位符(URL)和统一资源标识符(URI)。

无层次命名中,实现定位实体的方法有哪些,各种方法的过程

在无层次命名的环境中,可以使用以下几种方法来定位实体:

  1. 广播和多播(Broadcasting and Multicasting):通过向网络中的所有节点广播或多播请求消息,让所有节点都能接收到该消息,并根据消息内容来定位实体。广播是将消息发送到网络的所有节点,而多播是将消息发送到预定的一组节点。
  2. 转发指针(Forwarding Pointers):在无层次命名系统中,可以为每个实体分配一个独特的指针或引用,该指针包含实体的位置信息。当其他节点需要访问该实体时,通过使用指针来直接访问实体的位置。
  3. 宿主位置(Host Location):在无层次命名系统中,可以为每个实体指定一个宿主位置,它可以是网络地址或物理地址等。当其他节点需要访问该实体时,可以使用宿主位置来直接定位实体所在的节点。

这些方法的过程如下:

  • 广播和多播:节点发送广播或多播请求消息,该消息会被网络中的所有节点接收到。接收到消息的节点会根据消息的内容来判断是否需要响应该消息。如果需要响应,则通过消息中的标识符或其他信息来定位实体,并返回相应的响应消息给请求节点。
  • 转发指针:节点通过指针或引用来定位实体。当节点需要访问实体时,会直接使用指针或引用来获取实体的位置信息。节点根据位置信息与实体所在的节点进行通信,以访问或操作该实体。
  • 宿主位置:节点通过宿主位置来定位实体。当节点需要访问实体时,会使用宿主位置来直接寻址实体所在的节点。节点根据宿主位置与实体所在的节点进行通信,以访问或操作该实体。

结构化命名中的命名空间管理方法

在结构化命名中,命名空间的管理方法可以包括以下几个方面:

  1. 命名空间的层次结构:结构化命名系统通过定义层次结构来组织和管理命名空间。不同的命名空间可以根据其层次关系进行划分,每个层次可以包含子命名空间或实体标识符。层次结构提供了一种组织和分类命名空间的方式,使得命名空间之间具有父子关系。
  2. 命名空间的命名规则和语法:结构化命名系统定义了命名空间的命名规则和语法,规定了如何命名命名空间和其中的实体标识符。命名规则和语法可以根据具体的命名约定和规范来定义,确保命名空间中的标识符具有一致性和可理解性。
  3. 命名空间的访问控制:结构化命名系统可以使用访问控制机制来管理命名空间的访问权限。通过定义访问策略和权限规则,可以控制哪些实体可以访问特定的命名空间,以及访问命名空间中的哪些实体。这有助于保护命名空间中的数据和资源,并确保合适的实体可以获取所需的信息。
  4. 命名空间的映射和解析:结构化命名系统提供了映射和解析机制,将命名空间中的标识符映射到相应的实体或对象。这可以通过查找命名空间的映射关系表、引用表或其他方式来完成。映射和解析机制使得能够根据标识符来定位和访问命名空间中的实体,实现了命名空间的功能。

综合上述方法,结构化命名系统能够提供一种可管理和可理解的命名空间结构,使得实体和标识符之间的关系清晰,并提供了相应的访问控制和映射解析机制。这有助于在复杂的系统环境中有效管理和使用命名空间。

DNS命名服务的基本原理和解析方法与过程

DNS(Domain Name System)是互联网中用于将域名转换为 IP 地址的分布式命名服务。它提供了将易于记忆的域名映射到相应 IP 地址的功能。以下是DNS命名服务的基本原理和解析方法与过程的简要介绍:

基本原理:

  1. 域名层次结构:DNS采用了层次结构的域名命名空间,以区分不同的域名和子域名。域名由多个标签组成,每个标签之间用点号(.)分隔。
  2. 域名解析:DNS将域名解析为对应的 IP 地址,或者将 IP 地址解析为对应的域名。这是通过域名解析器(DNS Resolver)和DNS服务器(DNS Server)之间的交互来实现的。

解析方法与过程:

  1. 递归查询(Recursive Query):当客户端的DNS解析器接收到域名查询请求时,它会向根域名服务器发送一个递归查询请求。根域名服务器会返回顶级域名服务器的地址给解析器。
  2. 迭代查询(Iterative Query):解析器将查询转发给顶级域名服务器,顶级域名服务器再返回下一级域名服务器的地址给解析器。这个过程会一直重复,直到找到最终的授权域名服务器。
  3. 缓存(Caching):为了提高解析效率,DNS解析器会在解析过程中缓存查询结果。这样,在后续相同域名的查询中,解析器可以直接使用缓存的结果,而无需进行完整的解析过程。
  4. 域名服务器层次结构:DNS服务器也采用层次结构,分为根域名服务器、顶级域名服务器、权威域名服务器等。每个域名服务器存储了一部分域名和相应的IP地址,它们之间通过查询和转发来相互协作完成域名解析的任务。

总结: DNS的基本原理是通过递归查询和迭代查询的方式来将域名解析为对应的IP地址,或将IP地址解析为对应的域名。这一过程涉及到DNS解析器、DNS服务器以及域名服务器的协同工作。在解析过程中,会进行缓存以提高效率。通过DNS命名服务,用户可以使用易于记忆的域名来访问互联网上的各种服务和资源。

通信

Socket、RPC、RMI、MoM/MQ编程的基本方法

  1. Socket编程:Socket编程是一种基于网络套接字的编程模式,用于实现网络通信。通过使用Socket API,开发者可以创建客户端和服务器应用程序,进行网络连接、数据发送和接收等操作。Socket编程提供了底层的网络通信接口,使得应用程序能够在不同主机之间进行数据交换。
  2. RPC(Remote Procedure Call)编程:RPC是一种远程过程调用的编程模式,用于在分布式系统中进行跨网络的函数调用。通过RPC框架,客户端可以调用远程服务器上的函数,就像调用本地函数一样。RPC编程隐藏了底层的网络细节,提供了一种方便的方式来实现分布式应用程序。
  3. RMI(Remote Method Invocation)编程:RMI是一种Java特定的RPC实现,用于在Java应用程序之间进行远程方法调用。通过RMI,Java对象的方法可以在不同的Java虚拟机(JVM)中进行调用,实现分布式的Java应用程序开发。RMI编程利用Java的远程对象和远程接口来定义和调用远程方法。
  4. MoM(Message-oriented Middleware)/MQ(Message Queue)编程:MoM/MQ编程是一种基于消息传递的中间件编程模式,用于在分布式系统中进行异步通信。通过使用消息队列,应用程序可以通过发送和接收消息来实现解耦合的通信方式。MoM/MQ提供了消息的可靠传递、消息队列的管理和消息处理的机制,使得应用程序可以以异步的方式进行通信和处理消息。

远程过程调用

远程过程调用(Remote Procedure Call,RPC)是一种分布式系统中的编程模式,用于实现跨网络的函数调用。它允许一个计算机程序在网络上调用另一个计算机程序的函数或方法,就像调用本地函数一样。

RPC的基本原理如下:

  1. 定义接口:首先,需要定义一个远程接口,其中包含了需要调用的远程函数的签名和参数。
  2. 代理生成:根据远程接口定义,生成客户端和服务器端的代理(Proxy)代码。客户端代理用于向服务器发送请求,服务器端代理用于接收请求并执行相应的函数。
  3. 通信传输:客户端代理将函数调用的请求进行封装,并通过网络发送到服务器端。常用的通信协议包括HTTP、TCP/IP、UDP等。
  4. 服务器端处理:服务器端接收到请求后,通过解析请求的参数和函数名,执行相应的函数,并将结果返回给客户端。
  5. 客户端接收结果:客户端代理接收到服务器返回的结果,将其解析并返回给调用方。

RPC的优点包括:

  • 简化分布式系统开发:RPC隐藏了网络通信的细节,使得开发者可以像调用本地函数一样调用远程函数,简化了分布式系统的开发过程。
  • 提高代码复用性:通过定义远程接口,不同的客户端可以共享相同的接口,从而提高了代码的复用性。
  • 提高系统性能:RPC可以通过数据传输的优化和异步调用的方式提高系统的性能和吞吐量。

RPC也存在一些局限性,以下是一些常见的局限性:

  1. 网络延迟和可靠性:RPC依赖于网络通信,因此受网络延迟和可靠性的影响。如果网络延迟高或网络不可靠,会导致RPC调用的响应时间增加或调用失败。
  2. 跨平台和跨语言支持:RPC通常在特定的编程语言和平台上实现,并且客户端和服务器端需要使用相同的RPC框架和接口定义。这可能限制了跨平台和跨语言的互操作性。
  3. 序列化和反序列化开销:RPC需要将函数调用和返回值进行序列化和反序列化,以便在网络上传输。这涉及到数据的编码和解码,可能会引入一定的开销。
  4. 扩展性和可伸缩性:RPC在大规模系统中的扩展性和可伸缩性可能存在挑战。随着系统规模的增大,RPC的调用频率和负载可能会增加,需要合理设计和调优以保证性能和可扩展性。
  5. 依赖于网络连接:RPC调用需要建立有效的网络连接,如果网络连接不可用或中断,调用可能失败或超时。这也意味着RPC调用对网络稳定性和可靠性要求较高。
  6. 安全性和认证:RPC需要考虑数据的安全性和身份认证,以防止未经授权的访问和数据泄露。这需要在RPC框架中实现适当的安全措施和身份验证机制。

面向消息的通信、消息队列的通信、接口与管理机制

面向消息的通信、消息队列的通信以及接口与管理机制是分布式系统中常见的通信模式和组件,它们有以下特点和功能:

  1. 面向消息的通信(Message-oriented Communication):面向消息的通信是一种基于消息的异步通信模式,其中消息是在不同组件之间进行传递和交换的基本单位。在这种通信模式下,发送者将消息发送到消息传递系统中,并且不需要立即等待接收者的响应。接收者可以异步地接收和处理消息。这种通信模式提供了解耦合、可靠性、灵活性和可伸缩性等优势。
  2. 消息队列的通信:消息队列是一种用于在分布式系统中传递和存储消息的中间件。它通过提供队列机制来实现异步的、可靠的消息传递。消息发送者将消息发送到队列中,而消息接收者可以从队列中获取消息进行处理。消息队列提供了一种解耦合的通信方式,可以平衡系统之间的负载,提高系统的可靠性和可扩展性。
  3. 接口与管理机制:在分布式系统中,接口和管理机制起着关键的作用。接口定义了组件之间的通信规范和约定,它定义了消息的格式、数据结构、操作方法等。通过接口,不同的组件可以协调工作,进行消息的发送和接收。管理机制则负责管理和监控分布式系统中的各个组件和资源。它包括配置管理、错误处理、安全认证、负载均衡等功能,以确保系统的正常运行和性能优化。

流通信的基本含义和机制

流通信是一种在计算机网络中进行数据传输的基本方式,它基于流式数据的传输和处理。在流通信中,数据被切分成小的数据块(也称为数据流或数据包),按顺序通过网络进行传输,并在接收端进行重新组装和处理。

流通信的基本含义是以连续的数据流的形式进行数据传输,而不是一次性地发送和接收固定大小的数据块。这种方式可以在数据流中按需传输和处理数据,而无需等待整个数据块的完整到达。

流通信的机制包括以下关键要素:

  1. 数据分割和封装:发送端将要传输的数据流切分为适当的数据块,并添加必要的控制信息,如标识符、校验和、序列号等,以便在接收端进行正确的数据重组和验证。
  2. 传输和接收:数据块通过网络传输到接收端。流通信可以基于不同的传输协议,如TCP(传输控制协议)或UDP(用户数据报协议)。
  3. 数据重组和处理:接收端根据接收到的数据块的控制信息,按序号进行数据重组,并进行相应的处理,如数据解码、错误校验、数据处理等。
  4. 可靠性和流量控制:流通信可以提供一定的可靠性和流量控制机制,以确保数据的正确传输和接收,并控制发送和接收之间的数据流速率,以避免数据拥塞和丢失。

流通信在许多网络应用中得到广泛应用,如视频流、音频流、实时数据传输等。它提供了实时性和灵活性,并且适用于需要连续数据传输和实时响应的场景。

叠加网络

叠加网络(Overlay Network)是在底层网络之上构建的一种逻辑网络结构。它通过在现有的底层网络中创建一组节点之间的虚拟连接来实现通信。叠加网络可以用于增强现有网络的功能或提供额外的服务。

以下是叠加网络的一些关键特点和用途:

  1. 虚拟连接:叠加网络在底层网络中创建虚拟连接,这些连接可以是点对点的、点对多点的或多点对多点的。通过这些连接,节点可以进行通信和数据交换,而无需了解底层网络的细节。
  2. 路由和转发:叠加网络使用自己的路由和转发机制,将数据从源节点传递到目标节点。这些路由和转发算法可以根据叠加网络的设计目标进行优化,例如最短路径、负载均衡等。
  3. 功能增强:叠加网络可以在现有网络的基础上增加额外的功能和服务。例如,可以构建具有匿名性的叠加网络,用于保护用户的隐私;或者构建用于内容分发的叠加网络,以提高数据的可靠性和传输效率。
  4. 高度可扩展:叠加网络的拓扑结构可以灵活地进行调整和扩展,而不需要对底层网络进行修改。这使得叠加网络能够适应不同规模和需求的应用场景。
  5. 弹性和鲁棒性:叠加网络可以提供一定程度的弹性和鲁棒性。即使底层网络中的节点或连接出现故障,叠加网络可以通过重新路由或创建新的虚拟连接来保持通信的连续性。

叠加网络在互联网和分布式系统中得到广泛应用。例如,虚拟专用网络(VPN)使用叠加网络技术在公共网络上创建私有网络;点对点文件共享系统使用叠加网络来实现节点之间的直接文件传输。叠加网络提供了一种灵活且可定制的方式来构建具有特定功能和性能要求的网络结构。

应用层多播通信

应用层多播通信是一种将数据从一个源节点传输到多个目标节点的通信方式。它可以在应用层上实现,而不依赖底层网络的多播支持。应用层多播通信可以通过以下方式实现:

  1. 源节点广播:源节点将数据广播到所有的目标节点。每个目标节点都可以接收到广播的数据,并进行相应的处理。这种方式简单直接,但会造成网络带宽的浪费,尤其在大规模的多播场景中。
  2. 接收者报名:源节点通过维护一个多播组成员列表,每个目标节点在加入多播组时向源节点报名。源节点将数据仅发送给已经报名的目标节点,减少了带宽的浪费。然而,这种方式需要维护和同步多播组成员列表,对源节点的管理和维护带来了额外的开销。
  3. 树形结构:源节点与多个目标节点之间构建一棵树形结构。源节点作为树的根节点,每个目标节点作为树的叶子节点。当源节点发送数据时,它通过树的分支将数据传递到每个目标节点。这种方式可以有效减少带宽的使用,但需要在每个目标节点和源节点之间建立和维护树形结构。
  4. 混合方式:多播通信也可以采用混合的方式,结合以上提到的方法。例如,可以使用源节点广播和接收者报名相结合的方式,根据网络规模和需求选择最合适的方法。

应用层多播通信适用于需要将数据同时发送给多个接收者的场景,如实时视频流、在线游戏、实时数据分发等。通过在应用层上实现多播通信,可以灵活地控制和管理多播过程,满足不同应用的需求,并提高网络的效率和可靠性。

Chord算法

Chord算法是一种用于分布式哈希表的一致性哈希算法,用于在分布式系统中解决数据的分布和查找问题。Chord算法基于DHT(分布式哈希表)的概念,通过将数据和节点映射到一个圆环上,并使用一致性哈希算法来确定数据的存储位置和节点的路由。

下面是Chord算法的基本原理和步骤:

  1. 节点标识和哈希环:Chord算法中的节点通过一个唯一的标识符(如节点的IP地址或哈希值)表示,并将这些节点映射到一个虚拟的哈希环上。通常使用哈希函数将节点的标识符映射到哈希环上的一个位置。
  2. 节点加入:当一个新节点加入Chord网络时,它会选择一个合适的标识符,并将自己插入到哈希环中的适当位置。然后,它需要与其他节点进行通信以建立网络连接和获取相应的路由信息。
  3. 路由查找:为了定位存储在Chord网络中的数据,节点需要进行路由查找。节点通过比较目标数据的标识符与自己在哈希环上的位置,确定该数据应该存储在哪个节点上。如果当前节点不是数据所在的节点,它将通过一系列的路由表查找和跳跃操作将请求转发到正确的节点。
  4. 节点故障处理:在Chord网络中,节点可能会故障或离开。为了保持网络的连通性和一致性,Chord算法使用了一些机制来处理节点的故障。例如,当一个节点离开网络时,它的后继节点将接管其责任,并更新路由表以反映节点的变化。

Chord算法通过构建一致性哈希环和使用分布式路由表,实现了高效的数据分布和查找。它具有良好的可扩展性和容错性,能够处理节点的加入和离开,并在动态的分布式环境中保持数据的一致性和可用性。Chord算法被广泛应用于分布式系统中的数据存储和路由问题。

假设有一个分布式存储系统,使用Chord算法来管理数据的分布和查找。

在该系统中,有5个节点参与,它们分别是节点A、节点B、节点C、节点D和节点E。

首先,每个节点选择一个唯一的标识符,并将这些节点映射到一个虚拟的哈希环上。假设节点A的标识符为10,节点B的标识符为20,节点C的标识符为30,节点D的标识符为40,节点E的标识符为50。这些节点根据标识符的大小依次在哈希环上的适当位置插入。

接下来,节点加入网络。假设节点D是最新加入的节点。节点D会选择一个合适的标识符,并将自己插入到哈希环中的适当位置,如标识符为35,它会插入到节点C和节点D之间。

当需要查找特定数据时,假设数据的标识符为25。节点A接收到查找请求,通过比较目标数据的标识符与自己在哈希环上的位置,确定数据应该存储在节点B上。节点A将查找请求转发给节点B,节点B确认数据是否存在并返回相应结果。

如果节点C发生故障,节点D将接管其责任。节点D会更新自己的路由表,使其成为节点C的后继节点,从而保持网络的连通性和一致性。

这是一个简化的例子,展示了Chord算法在分布式存储系统中的应用。实际情况中,可能涉及更多节点和更复杂的路由表。通过Chord算法,节点可以根据数据的标识符快速定位数据所在的节点,实现高效的数据存储和检索。

socket编程

平台和编译器

Linux平台PC GNU gcc

HPUX平台 gcc

1)什么是 socket?

socket是使用标准Unix 文件描述符 (file descriptor) 和其它程序通讯的方式。 什么? 你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。但是(注意后面的话),这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其它的东西。Unix 中所有的东西就是文件!所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket(),它返回套接字描述符 (socket descriptor),然后你再通过它来进行send() 和 recv()调用。

“但是…”,你可能有很大的疑惑,“如果它是个文件描述符,那么为什 么不用一般调用read()和write()来进行套接字通讯?”简单的答案是:“你可以使用!”。详细的答案是:“你可以,但是使用send()和recv()让你更好的控制数据传输。”

存在这样一个情况:在我们的世界上,有很多种套接字。有DARPA Internet 地址 (Internet 套接字),本地节点的路径名 (Unix套接字),CCITT X.25地址 (你可以将X.25 套接字完全忽略)。也许在你的Unix 机器上还有其它的。我们在这里只讲第一种:Internet 套接字。

2)Internet 套接字的两种类型

什么意思?有两种类型的Internet 套接字?是的。不,我在撒谎。其实还有很多,但是我可不想吓着你。我们这里只讲两种。除了这些, 我打算另外介绍的 “Raw Sockets” 也是非常强大的,很值得查阅。
那么这两种类型是什么呢?一种是”Stream Sockets”(流格式),另外一种是”Datagram Sockets”(数据包格式)。我们以后谈到它们的时候也会用到 “SOCK_STREAM” 和 “SOCK_DGRAM”。数据报套接字有时也叫“无连接套接字”(如果你确实要连接的时候可以用connect()。) 流式套接字是可靠的双向通讯的数据流。如果你向套接字按顺序输出“1,2”,那么它们将按顺序“1,2”到达另一边。它们是无错误的传递的,有自己的错误控制,在此不讨论。

有什么在使用流式套接字?你可能听说过 telnet,不是吗?它就使用流式套接字。你需要你所输入的字符按顺序到达,不是吗?同样,WWW浏览器使用的 HTTP 协议也使用它们来下载页面。实际上,当你通过端口80 telnet 到一个 WWW 站点,然后输入 “GET pagename” 的时候,你也可以得到 HTML 的内容。为什么流式套接字可以达到高质量的数据传输?这是因为它使用了“传输控制协议 (The Transmission Control Protocol)”,也叫 “TCP” (请参考 RFC-793 获得详细资料。)TCP 控制你的数据按顺序到达并且没有错误。你也许听到 “TCP” 是因为听到过 “TCP/IP”。这里的 IP 是指“Internet 协议”(请参考 RFC-791。) IP只是处理 Internet 路由而已。

那么数据报套接字呢?为什么它叫无连接呢?为什么它是不可靠的呢?有这样的一些事实:如果你发送一个数据报,它可能会到达,它可能次序颠倒了。如果它到达,那么在这个包的内部是无错误的。数据报也使用 IP 作路由,但是它不使用 TCP。它使用“用户数据报协议 (User Datagram Protocol)”,也叫 “UDP” (请参考 RFC-768。)

为什么它们是无连接的呢?主要是因为它并不象流式套接字那样维持一个连接。你只要建立一个包,构造一个有目标信息的IP 头,然后发出去。无需连接。它们通常使用于传输包-包信息。简单的应用程序有:tftp, bootp等等。

你也许会想:“假如数据丢失了这些程序如何正常工作?”我的朋友,每个程序在 UDP 上有自己的协议。例如,tftp 协议每发出的一个被接受到包,收到者必须发回一个包来说“我收到了!” (一个“命令正确应答”也叫“ACK” 包)。如果在一定时间内(例如5秒),发送方没有收到应答,它将重新发送,直到得到 ACK。这一ACK过程在实现 SOCK_DGRAM 应用程序的时候非常重要。

3)网络理论

既然我刚才提到了协议层,那么现在是讨论网络究竟如何工作和一些 关于 SOCK_DGRAM 包是如何建立的例子。当然,你也可以跳过这一段, 如果你认为已经熟悉的话。

现在是学习数据封装 (Data Encapsulation) 的时候了!它非常非常重要。它重要性重要到你在网络课程学习中无论如何也得也得掌握它(图1:数据封装)。主要 的内容是:一个包,先是被第一个协议(在这里是TFTP )在它的报头(也许 是报尾)包装(“封装”),然后,整个数据(包括 TFTP 头)被另外一个协议 (在这里是 UDP )封装,然后下一个( IP ),一直重复下去,直到硬件(物理) 层( 这里是以太网 )。
当另外一台机器接收到包,硬件先剥去以太网头,内核剥去IP和UDP 头,TFTP程序再剥去TFTP头,最后得到数据。

现在我们终于讲到声名狼藉的网络分层模型 (Layered Network Model)。这种网络模型在描述网络系统上相对其它模型有很多优点。例如, 你可以写一个套接字程序而不用关心数据的物理传输(串行口,以太网,连 接单元接口 (AUI) 还是其它介质),因为底层的程序会为你处理它们。实际 的网络硬件和拓扑对于程序员来说是透明的。

不说其它废话了,我现在列出整个层次模型。如果你要参加网络考试, 可一定要记住:

应用层 (Application)

表示层 (Presentation)

会话层 (Session)

传输层(Transport)

网络层(Network)

数据链路层(Data Link)

物理层(Physical)

物理层是硬件(串口,以太网等等)。应用层是和硬件层相隔最远的—它 是用户和网络交互的地方。 这个模型如此通用,如果你想,你可以把它作为修车指南。把它对应 到 Unix,结果是:

应用层(Application Layer) (telnet, ftp,等等)

传输层(Host-to-Host Transport Layer) (TCP, UDP)

Internet层(Internet Layer) (IP和路由)

网络访问层 (Network Access Layer) (网络层,数据链路层和物理层)

现在,你可能看到这些层次如何协调来封装原始的数据了。

看看建立一个简单的数据包有多少工作?哎呀,你将不得不使用 “cat” 来建立数据包头!这仅仅是个玩笑。对于流式套接字你要作的是 send() 发 送数据。对于数据报式套接字,你按照你选择的方式封装数据然后使用 sendto()。内核将为你建立传输层和 Internet 层,硬件完成网络访问层。 这就是现代科技。 现在结束我们的网络理论速成班。哦,忘记告诉你关于路由的事情了。 但是我不准备谈它,如果你真的关心,那么参考 IP RFC。

4)结构体

终于谈到编程了。在这章,我将谈到被套接字用到的各种数据类型。 因为它们中的一些内容很重要了。

首先是简单的一个:socket描述符。它是下面的类型:

int

仅仅是一个常见的 int。

从现在起,事情变得不可思议了,而你所需做的就是继续看下去。注 意这样的事实:有两种字节排列顺序:重要的字节 (有时叫 “octet”,即八 位位组) 在前面,或者不重要的字节在前面。前一种叫“网络字节顺序 (Network Byte Order)”。有些机器在内部是按照这个顺序储存数据,而另外 一些则不然。当我说某数据必须按照 NBO 顺序,那么你要调用函数(例如 htons() )来将它从本机字节顺序 (Host Byte Order) 转换过来。如果我没有 提到 NBO, 那么就让它保持本机字节顺序。

我的第一个结构(在这个技术手册TM中)—struct sockaddr.。这个结构 为许多类型的套接字储存套接字地址信息:

1
2
3
4
struct sockaddr {
  unsigned short sa_family; /* 地址家族, AF_xxx */
  char sa_data[14]; /*14字节协议地址*/
};

sa_family 能够是各种各样的类型,但是在这篇文章中都是 “AF_INET”。 sa_data包含套接字中的目标地址和端口信息。这好像有点 不明智。

为了处理struct sockaddr,程序员创造了一个并列的结构: struct sockaddr_in (“in” 代表 “Internet”。)

1
2
3
4
5
6
struct sockaddr_in {
  short int sin_family; /* 通信类型 */
  unsigned short int sin_port; /* 端口 */
  struct in_addr sin_addr; /* Internet 地址 */
  unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
};

用这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也可以被指向结构体sockaddr并且代替它。这样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,并且在最后转换。同时,注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 “AF_INET”。最后,sin_port和 sin_addr 必须是网络字节顺序 (Network Byte Order)!

你也许会反对道:”但是,怎么让整个数据结构 struct in_addr sin_addr 按照网络字节顺序呢?” 要知道这个问题的答案,我们就要仔细的看一看这 个数据结构: struct in_addr, 有这样一个联合 (unions):

1
2
3
4
/* Internet 地址 (一个与历史有关的结构) */
struct in_addr {
  unsigned long s_addr;
};

它曾经是个最坏的联合,但是现在那些日子过去了。如果你声明 “ina” 是数据结构 struct sockaddr_in 的实例,那么 “ina.sin_addr.s_addr” 就储 存4字节的 IP 地址(使用网络字节顺序)。如果你不幸的系统使用的还是恐 怖的联合 struct in_addr ,你还是可以放心4字节的 IP 地址并且和上面 我说的一样(这是因为使用了“#define”。)

5)本机转换

我们现在到了新的章节。我们曾经讲了很多网络到本机字节顺序的转 换,现在可以实践了! 你能够转换两种类型: short (两个字节)和 long (四个字节)。这个函 数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 “h” 表示 “本机 (host)”,接着是 “to”,然后用 “n” 表 示 “网络 (network)”,最后用 “s” 表示 “short”: h-to-n-s, 或者 htons() (“Host to Network Short”)。

太简单了… ,如果不是太傻的话,你一定想到了由”n”,”h”,”s”,和 “l”形成的正确 组合,例如这里肯定没有stolh() (“Short to Long Host”) 函数,不仅在这里 没有,所有场合都没有。但是这里有:

htons()—“Host to Network Short”

htonl()—“Host to Network Long”

ntohs()—“Network to Host Short”

ntohl()—“Network to Host Long”

现在,你可能想你已经知道它们了。你也可能想:“如果我想改变 char 的顺序要怎么办呢?” 但是你也许马上就想到,“用不着考虑的”。你也许 会想到:我的 68000 机器已经使用了网络字节顺序,我没有必要去调用 htonl() 转换 IP 地址。你可能是对的,但是当你移植你的程序到别的机器 上的时候,你的程序将失败。可移植性!这里是 Unix 世界!记住:在你 将数据放到网络上的时候,确信它们是网络字节顺序的。

最后一点:为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。

6)IP 地址和如何处理它们

现在我们很幸运,因为我们有很多的函数来方便地操作 IP 地址。没有 必要用手工计算它们,也没有必要用”<<”操作来储存成长整字型。 首先,假设你已经有了一个sockaddr_in结构体ina,你有一个IP地 址”132.241.5.10”要储存在其中,你就要用到函数inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法如下:

1
ina.sin_addr.s_addr = inet_addr("132.241.5.10");

注意,inet_addr()返回的地址已经是网络字节格式,所以你无需再调用 函数htonl()。 我们现在发现上面的代码片断不是十分完整的,因为它没有错误检查。 显而易见,当inet_addr()发生错误时返回-1。记住这些二进制数字?(无符 号数)-1仅仅和IP地址255.255.255.255相符合!这可是广播地址!大错特 错!记住要先进行错误检查。

好了,现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?这样的话,你就要用到函数 inet_ntoa()(“ntoa”的含义是”network to ascii”),就像这样:

1
printf("%s",inet_ntoa(ina.sin_addr));

它将输出IP地址。需要注意的是inet_ntoa()将结构体in-addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char *a1, *a2;

……

a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */

a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */

printf("address 1: %s\n",a1);

printf("address 2: %s\n",a2);

输出如下:

address 1: 132.241.5.10

address 2: 132.241.5.10

假如你需要保存这个IP地址,使用strcopy()函数来指向你自己的字符 指针。

上面就是关于这个主题的介绍。稍后,你将学习将一个类 似”http://wintehouse.gov“的字符串转换成它所对应的IP地址(查阅域名服务,稍 后)。

7)socket()函数

我想我不能再不提这个了-下面我将讨论一下socket()系统调用。

下面是详细介绍:

1
2
3
4
5
#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

但是它们的参数是什么? 首先,domain 应该设置成 “AF_INET”,就 象上面的数据结构struct sockaddr_in 中一样。然后,参数 type 告诉内核 是 SOCK_STREAM 类型还是 SOCK_DGRAM 类型。最后,把 protocol 设置为 “0”。(注意:有很多种 domain、type,我不可能一一列出了,请看 socket() 的 man帮助。当然,还有一个”更好”的方式去得到 protocol,同 时请查阅 getprotobyname() 的 man 帮助。) socket() 只是返回你以后在系统调用种可能用到的 socket 描述符,或 者在错误的时候返回-1。全局变量 errno 中将储存返回的错误值。(请参考 perror() 的 man 帮助。)

8)bind()函数

一旦你有一个套接字,你可能要将套接字和机器上的一定的端口关联 起来。(如果你想用listen()来侦听一定端口的数据,这是必要一步—MUD 告 诉你说用命令 “telnet x.y.z 6969”。)如果你只想用 connect(),那么这个步 骤没有必要。但是无论如何,请继续读下去。

这里是系统调用 bind() 的大概:

1
2
3
4
5
#include <sys/types.h>

#include <sys/socket.h>

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

sockfd 是调用 socket 返回的文件描述符。my_addr 是指向数据结构 struct sockaddr 的指针,它保存你的地址(即端口和 IP 地址) 信息。 addrlen 设置为 sizeof(struct sockaddr)。 简单得很不是吗? 再看看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <string.h>

#include <sys/types.h>

#include <sys/socket.h>

#define MYPORT 3490

main()

{

  int sockfd;

  struct sockaddr_in my_addr;

  sockfd = socket(AF_INET, SOCK_STREAM, 0); /*需要错误检查 */

  my_addr.sin_family = AF_INET; /* host byte order */

  my_addr.sin_port = htons(MYPORT); /* short, network byte order */

  my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");

  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

  /* don't forget your error checking for bind(): */

  bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

  ……

这里也有要注意的几件事情。my_addr.sin_port 是网络字节顺序, my_addr.sin_addr.s_addr 也是的。另外要注意到的事情是因系统的不同, 包含的头文件也不尽相同,请查阅本地的 man 帮助文件。 在 bind() 主题中最后要说的话是,在处理自己的 IP 地址和/或端口的 时候,有些工作是可以自动处理的。

my_addr.sin_port = 0; / 随机选择一个没有使用的端口 /

my_addr.sin_addr.s_addr = INADDR_ANY; / 使用自己的IP地址 /

通过将0赋给 my_addr.sin_port,你告诉 bind() 自己选择合适的端 口。同样,将 my_addr.sin_addr.s_addr 设置为 INADDR_ANY,你告诉 它自动填上它所运行的机器的 IP 地址。

如果你一向小心谨慎,那么你可能注意到我没有将 INADDR_ANY 转 换为网络字节顺序!这是因为我知道内部的东西:INADDR_ANY 实际上就 是 0!即使你改变字节的顺序,0依然是0。但是完美主义者说应该处处一 致,INADDR_ANY或许是12呢?你的代码就不能工作了,那么就看下面 的代码:

my_addr.sin_port = htons(0); / 随机选择一个没有使用的端口 /

my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/ 使用自己的IP地址 /

你或许不相信,上面的代码将可以随便移植。我只是想指出,既然你 所遇到的程序不会都运行使用htonl的INADDR_ANY。

bind() 在错误的时候依然是返回-1,并且设置全局错误变量errno。

在你调用 bind() 的时候,你要小心的另一件事情是:不要采用小于 1024的端口号。所有小于1024的端口号都被系统保留!你可以选择从1024 到65535的端口(如果它们没有被别的程序使用的话)。
你要注意的另外一件小事是:有时候你根本不需要调用它。如果你使 用 connect() 来和远程机器进行通讯,你不需要关心你的本地端口号(就象 你在使用 telnet 的时候),你只要简单的调用 connect() 就可以了,它会检 查套接字是否绑定端口,如果没有,它会自己绑定一个没有使用的本地端 口。

9)connect()程序

现在我们假设你是个 telnet 程序。你的用户命令你得到套接字的文件 描述符。你听从命令调用了socket()。下一步,你的用户告诉你通过端口 23(标准 telnet 端口)连接到”132.241.5.10”。你该怎么做呢? 幸运的是,你正在阅读 connect()—如何连接到远程主机这一章。你可 不想让你的用户失望。

connect() 系统调用是这样的:

1
2
3
4
5
#include <sys/types.h>

#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd 是系统调用 socket() 返回的套接字文件描述符。serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr。addrlen 设置 为 sizeof(struct sockaddr)。 想知道得更多吗?让我们来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <string.h>

#include <sys/types.h>

#include <sys/socket.h>

#define DEST_IP "132.241.5.10"

#define DEST_PORT 23

main()

{

  int sockfd;

  struct sockaddr_in dest_addr; /* 目的地址*/

  sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查 */

  dest_addr.sin_family = AF_INET; /* host byte order */

  dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */

  dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);

  bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */

  /* don't forget to error check the connect()! */

  connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));

  ……

再一次,你应该检查 connect() 的返回值—它在错误的时候返回-1,并 设置全局错误变量 errno。 同时,你可能看到,我没有调用 bind()。因为我不在乎本地的端口号。 我只关心我要去那。内核将为我选择一个合适的端口号,而我们所连接的 地方也自动地获得这些信息。一切都不用担心。

10)listen()函数

是换换内容得时候了。假如你不希望与远程的一个地址相连,或者说, 仅仅是将它踢开,那你就需要等待接入请求并且用各种方法处理它们。处 理过程分两步:首先,你听—listen(),然后,你接受—accept() (请看下面的 内容)。

除了要一点解释外,系统调用 listen 也相当简单。

1
int listen(int sockfd, int backlog);

sockfd 是调用 socket() 返回的套接字文件描述符。backlog 是在进入 队列中允许的连接数目。什么意思呢? 进入的连接是在队列中一直等待直 到你接受 (accept() 请看下面的文章)连接。它们的数目限制于队列的允许。 大多数系统的允许数目是20,你也可以设置为5到10。

和别的函数一样,在发生错误的时候返回-1,并设置全局错误变量 errno。

你可能想象到了,在你调用 listen() 前你或者要调用 bind() 或者让内 核随便选择一个端口。如果你想侦听进入的连接,那么系统调用的顺序可 能是这样的:

socket();

bind();

listen();

/ accept() 应该在这 /

因为它相当的明了,我将在这里不给出例子了。(在 accept() 那一章的 代码将更加完全。)真正麻烦的部分在 accept()。

11)accept()函数

准备好了,系统调用 accept() 会有点古怪的地方的!你可以想象发生 这样的事情:有人从很远的地方通过一个你在侦听 (listen()) 的端口连接 (connect()) 到你的机器。它的连接将加入到等待接受 (accept()) 的队列 中。你调用 accept() 告诉它你有空闲的连接。它将返回一个新的套接字文 件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口, 新的在准备发送 (send()) 和接收 ( recv()) 数据。这就是这个过程!

函数是这样定义的:

1
2
3
#include <sys/socket.h>

int accept(int sockfd, void *addr, int *addrlen);

sockfd 相当简单,是和 listen() 中一样的套接字描述符。addr 是个指 向局部的数据结构 sockaddr_in 的指针。这是要求接入的信息所要去的地 方(你可以测定那个地址在那个端口呼叫你)。在它的地址传递给 accept 之 前,addrlen 是个局部的整形变量,设置为 sizeof(struct sockaddr_in)。 accept 将不会将多余的字节给 addr。如果你放入的少些,那么它会通过改 变 addrlen 的值反映出来。

同样,在错误时返回-1,并设置全局错误变量 errno。

现在是你应该熟悉的代码片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define MYPORT 3490 /*用户接入端口*/
#define BACKLOG 10 /* 多少等待连接控制*/
main()
{
  int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
  struct sockaddr_in my_addr; /* 地址信息 */
  struct sockaddr_in their_addr; /* connector's address information */
  int sin_size;
  sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查*/
  my_addr.sin_family = AF_INET; /* host byte order */
  my_addr.sin_port = htons(MYPORT); /* short, network byte order */
  my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */
  /* don't forget your error checking for these calls: */
  bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
  listen(sockfd, BACKLOG);
  sin_size = sizeof(struct sockaddr_in);
  new_fd = accept(sockfd, &their_addr, &sin_size);
  ……

注意,在系统调用 send() 和 recv() 中你应该使用新的套接字描述符 new_fd。如果你只想让一个连接进来,那么你可以使用 close() 去关闭原 来的文件描述符 sockfd 来避免同一个端口更多的连接。

12)send() and recv()函数

这两个函数用于流式套接字或者数据报套接字的通讯。如果你喜欢使 用无连接的数据报套接字,你应该看一看下面关于sendto() 和 recvfrom() 的章节。

send() 是这样的:

1
int send(int sockfd, const void *msg, int len, int flags);

sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。(详细的资料请看 send() 的 man page)。 这里是一些可能的例子:

1
2
3
4
5
6
char *msg = "Beej was here!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
……

send() 返回实际发送的数据的字节数—它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是 发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送其它的数据。但是这里也 有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。

recv() 函数很相似:

1
int recv(int sockfd, void *buf, int len, unsigned int flags);

sockfd 是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为0。(请参考recv() 的 man page。) recv() 返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。

很简单,不是吗? 你现在可以在流式套接字上发送数据和接收数据了。 你现在是 Unix 网络程序员了!

13)sendto() 和 recvfrom()函数

“这很不错啊”,你说,“但是你还没有讲无连接数据报套接字呢?” 没问题,现在我们开始这个内容。 既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之 前需要什么信息呢? 不错,是目标地址!看看下面的:

1
2
3
int sendto(int sockfd, const void *msg, int len, unsigned int flags,

const struct sockaddr *to, int tolen);

你已经看到了,除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。

相似的还有函数 recv() 和 recvfrom()。recvfrom() 的定义是这样的:

1
2
3
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,  

struct sockaddr *from, int *fromlen);

又一次,除了两个增加的参数外,这个函数和 recv() 也是一样的。from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。

recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。

记住,如果你用 connect() 连接一个数据报套接字,你可以简单的调 用 send() 和 recv() 来满足你的要求。这个时候依然是数据报套接字,依 然使用 UDP,系统套接字接口会为你自动加上了目标和源的信息。

14)close()和shutdown()函数

你已经整天都在发送 (send()) 和接收 (recv()) 数据了,现在你准备关 闭你的套接字描述符了。这很简单,你可以使用一般的 Unix 文件描述符 的 close() 函数:

1
close(sockfd);

它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企 图都将返回错误信息。如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭,你可以使用:

1
int shutdown(int sockfd, int how);

sockfd 是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一:

0 – 不允许接受

1 – 不允许发送

2 – 不允许发送和接受(和 close() 一样)

shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可以使用它们的)。

15)getpeername()函数

这个函数太简单了。 它太简单了,以至我都不想单列一章。但是我还是这样做了。 函数 getpeername() 告诉你在连接的流式套接字上谁在另外一边。函 数是这样的:

1
2
3
#include <sys/socket.h>

int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

sockfd 是连接的流式套接字的描述符。addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的 信息。addrlen 是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)。 函数在错误的时候返回 -1,设置相应的 errno。

一旦你获得它们的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 来打印或者获得更多的信息。但是你不能得到它的帐号。(如果它运行着愚 蠢的守护进程,这是可能的,但是它的讨论已经超出了本文的范围,请参 考 RFC-1413 以获得更多的信息。)

16)gethostname()函数

甚至比 getpeername() 还简单的函数是 gethostname()。它返回你程 序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你 的机器的 IP 地址。

下面是定义:

1
2
3
#include <unistd.h>

int gethostname(char *hostname, size_t size);

参数很简单:hostname 是一个字符数组指针,它将在函数返回时保存 主机名。size是hostname 数组的字节长度。

函数调用成功时返回 0,失败时返回 -1,并设置 errno。

17)域名服务(DNS)

如果你不知道 DNS 的意思,那么我告诉你,它代表域名服务(Domain Name Service)。它主要的功能是:你给它一个容易记忆的某站点的地址, 它给你 IP 地址(然后你就可以使用 bind(), connect(), sendto() 或者其它 函数) 。当一个人输入:

$ telnet http://whitehouse.gov

telnet 能知道它将连接 (connect()) 到 “198.137.240.100”。 但是这是如何工作的呢? 你可以调用函数 gethostbyname():

1
2
3
#include <netdb.h>

struct hostent *gethostbyname(const char *name);

很明白的是,它返回一个指向 struct hostent 的指针。这个数据结构 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct hostent {

  char *h_name;

  char **h_aliases;

  int h_addrtype;

  int h_length;

  char **h_addr_list;

};

#define h_addr h_addr_list[0]

这里是这个数据结构的详细资料:

h_name – 地址的正式名称。

h_aliases – 空字节-地址的预备名称的指针。

h_addrtype –地址类型; 通常是AF_INET。

h_length – 地址的比特长度。

h_addr_list – 零字节-主机网络地址指针。网络字节顺序。

h_addr - h_addr_list中的第一地址。

gethostbyname() 成功时返回一个指向结构体 hostent 的指针,或者 是个空 (NULL) 指针。(但是和以前不同,不设置errno,h_errno 设置错 误信息,请看下面的 herror()。) 但是如何使用呢? 有时候(我们可以从电脑手册中发现),向读者灌输 信息是不够的。这个函数可不象它看上去那么难用。

这里是个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
  struct hostent *h;
  if (argc != 2) { /* 检查命令行 */
  fprintf(stderr,"usage: getip address\n");
  exit(1);
  }
  if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */
  herror("gethostbyname");
  exit(1);
  }
  printf("Host name : %s\n", h->h_name);
  printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr)));
return 0;
}

在使用 gethostbyname() 的时候,你不能用 perror() 打印错误信息 (因为 errno 没有使用),你应该调用 herror()。

相当简单,你只是传递一个保存机器名的字符串(例如 “http://whitehouse.gov“) 给 gethostbyname(),然后从返回的数据结构 struct hostent 中获取信息。

唯一也许让人不解的是输出 IP 地址信息。h->h_addr 是一个 char , 但是 inet_ntoa() 需要的是 struct in_addr。因此,我转换 h->h_addr 成 struct in_addr ,然后得到数据。

18)客户-服务器背景知识

这里是个客户—服务器的世界。在网络上的所有东西都是在处理客户进 程和服务器进程的交谈。举个telnet 的例子。当你用 telnet (客户)通过23 号端口登陆到主机,主机上运行的一个程序(一般叫 telnetd,服务器)激活。 它处理这个连接,显示登陆界面,等等。

图 2 说明了客户和服务器之间的信息交换。

注意,客户—服务器之间可以使用SOCK_STREAM、SOCK_DGRAM 或者其它(只要它们采用相同的)。一些很好的客户—服务器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的时候,在远 端都有一个 ftpd 为你服务。

一般,在服务端只有一个服务器,它采用 fork() 来处理多个客户的连 接。基本的程序是:服务器等待一个连接,接受 (accept()) 连接,然后 fork() 一个子进程处理它。这是下一章我们的例子中会讲到的。

19)简单的服务器

这个服务器所做的全部工作是在流式连接上发送字符串 “Hello, World!\n”。你要测试这个程序的话,可以在一台机器上运行该程序,然后 在另外一机器上登陆:

$ telnet remotehostname 3490

remotehostname 是该程序运行的机器的名字。

服务器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 3490 /*定义用户连接端口*/
#define BACKLOG 10 /*多少等待连接控制*/
main()
{
  int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
  struct sockaddr_in my_addr; /* my address information */
  struct sockaddr_in their_addr; /* connector's address information */
  int sin_size;
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
  perror("socket");
  exit(1);
  }
  my_addr.sin_family = AF_INET; /* host byte order */
  my_addr.sin_port = htons(MYPORT); /* short, network byte order */
  my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */
  if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))== -1) {
  perror("bind");
  exit(1);
  }
  if (listen(sockfd, BACKLOG) == -1) {
  perror("listen");
  exit(1);
  }
  while(1) { /* main accept() loop */
  sin_size = sizeof(struct sockaddr_in);
  if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {
  perror("accept");
  continue;
  }
  printf("server: got connection from %s\n", \
  inet_ntoa(their_addr.sin_addr));
  if (!fork()) { /* this is the child process */
  if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
  perror("send");
  close(new_fd);
  exit(0);
  }
  close(new_fd); /* parent doesn't need this */
  while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
}
}

如果你很挑剔的话,一定不满意我所有的代码都在一个很大的main() 函数中。如果你不喜欢,可以划分得更细点。

你也可以用我们下一章中的程序得到服务器端发送的字符串。

20)简单的客户程序

这个程序比服务器还简单。这个程序的所有工作是通过 3490 端口连接到命令行中指定的主机,然后得到服务器发送的字符串。

客户代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define PORT 3490 /* 客户机连接远程主机的端口 */
#define MAXDATASIZE 100 /* 每次可以接收的最大字节 */
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr; /* connector's address information */
if (argc != 2) {
fprintf(stderr,"usage: client hostname\n");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror("gethostbyname");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(PORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */
if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) {
perror("connect");
exit(1);
}
if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
perror("recv");
exit(1);
}
buf[numbytes] = '\0';
printf("Received: %s",buf);
close(sockfd);
return 0;
}

注意,如果你在运行服务器之前运行客户程序,connect() 将返回 “Connection refused” 信息,这非常有用。

21)数据包 Sockets

我不想讲更多了,所以我给出代码 talker.c 和 listener.c。

listener 在机器上等待在端口 4590 来的数据包。talker 发送数据包到 一定的机器,它包含用户在命令行输入的内容。

这里就是 listener.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 4950 /* the port users will be sending to */
#define MAXBUFLEN 100
main()
{
int sockfd;
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector's address information */
int addr_len, numbytes;
char buf[MAXBUFLEN];
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
perror("bind");
exit(1);
}
addr_len = sizeof(struct sockaddr);
if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \
(struct sockaddr *)&their_addr, &addr_len)) == -1) {
perror("recvfrom");
exit(1);
}
printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
printf("packet is %d bytes long\n",numbytes);
buf[numbytes] = '\0';
printf("packet contains \"%s\"\n",buf);
close(sockfd);
}

注意在我们的调用 socket(),我们最后使用了 SOCK_DGRAM。同时, 没有必要去使用 listen() 或者 accept()。我们在使用无连接的数据报套接 字!

下面是 talker.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 4950 /* the port users will be sending to */
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in their_addr; /* connector's address information */
struct hostent *he;
int numbytes;
if (argc != 3) {
fprintf(stderr,"usage: talker hostname message\n");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror("gethostbyname");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(MYPORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */
if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \
(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
perror("sendto");
exit(1);
}
printf("sent %d bytes to %s\n",numbytes,inet_ntoa(their_addr.sin_addr));
close(sockfd);
return 0;
}

这就是所有的了。在一台机器上运行 listener,然后在另外一台机器上 运行 talker。观察它们的通讯!
除了一些我在上面提到的数据套接字连接的小细节外,对于数据套接 字,我还得说一些,当一个讲话者呼叫connect()函数时并指定接受者的地 址时,从这点可以看出,讲话者只能向connect()函数指定的地址发送和接 受信息。因此,你不需要使用sendto()和recvfrom(),你完全可以用send() 和recv()代替。

22)阻塞

阻塞,你也许早就听说了。”阻塞”是 “sleep” 的科技行话。你可能注意 到前面运行的 listener 程序,它在那里不停地运行,等待数据包的到来。 实际在运行的是它调用 recvfrom(),然后没有数据,因此 recvfrom() 说” 阻塞 (block)”,直到数据的到来。

很多函数都利用阻塞。accept() 阻塞,所有的 recv*() 函数阻塞。它 们之所以能这样做是因为它们被允许这样做。当你第一次调用 socket() 建 立套接字描述符的时候,内核就将它设置为阻塞。如果你不想套接字阻塞, 你就要调用函数 fcntl():

#include

#include

……

sockfd = socket(AF_INET, SOCK_STREAM, 0);

fcntl(sockfd, F_SETFL, O_NONBLOCK);

……

过设置套接字为非阻塞,你能够有效地”询问”套接字以获得信息。如 果你尝试着从一个非阻塞的套接字读信息并且没有任何数据,它不允许阻 塞—它将返回 -1 并将 errno 设置为 EWOULDBLOCK。

但是一般说来,这种询问不是个好主意。如果你让你的程序在忙等状 态查询套接字的数据,你将浪费大量的 CPU 时间。更好的解决之道是用 下一章讲的 select() 去查询是否有数据要读进来。

23)select()—多路同步 I/O

虽然这个函数有点奇怪,但是它很有用。假设这样的情况:你是个服 务器,你一边在不停地从连接上读数据,一边在侦听连接上的信息。 没问题,你可能会说,不就是一个 accept() 和两个 recv() 吗? 这么 容易吗,朋友? 如果你在调用 accept() 的时候阻塞呢? 你怎么能够同时接 受 recv() 数据? “用非阻塞的套接字啊!” 不行!你不想耗尽所有的 CPU 吧? 那么,该如何是好?

select() 让你可以同时监视多个套接字。如果你想知道的话,那么它就 会告诉你哪个套接字准备读,哪个又准备写,哪个套接字又发生了例外 (exception)。

闲话少说,下面是 select():

#include

#include

#include

int select(int numfds, fd_set readfds, fd_set writefds,fd_set exceptfds, struct timeval timeout);

这个函数监视一系列文件描述符,特别是 readfds、writefds 和 exceptfds。如果你想知道你是否能够从标准输入和套接字描述符 sockfd 读入数据,你只要将文件描述符 0 和 sockfd 加入到集合 readfds 中。参 数 numfds 应该等于最高的文件描述符的值加1。在这个例子中,你应该 设置该值为 sockfd+1。因为它一定大于标准输入的文件描述符 (0)。 当函数 select() 返回的时候,readfds 的值修改为反映你选择的哪个 文件描述符可以读。你可以用下面讲到的宏 FD_ISSET() 来测试。 在我们继续下去之前,让我来讲讲如何对这些集合进行操作。每个集 合类型都是 fd_set。下面有一些宏来对这个类型进行操作:

FD_ZERO(fd_set *set) – 清除一个文件描述符集合

FD_SET(int fd, fd_set *set) - 添加fd到集合

FD_CLR(int fd, fd_set *set) – 从集合中移去fd

FD_ISSET(int fd, fd_set *set) – 测试fd是否在集合中

最后,是有点古怪的数据结构 struct timeval。有时你可不想永远等待 别人发送数据过来。也许什么事情都没有发生的时候你也想每隔96秒在终 端上打印字符串 “Still Going…”。这个数据结构允许你设定一个时间,如果 时间到了,而 select() 还没有找到一个准备好的文件描述符,它将返回让 你继续处理。

数据结构 struct timeval 是这样的:

1
2
3
4
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};

只要将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待 的微秒数就可以了。是的,是微秒而不是毫秒。1,000微秒等于1毫秒,1,000 毫秒等于1秒。也就是说,1秒等于1,000,000微秒。为什么用符号 “usec” 呢? 字母 “u” 很象希腊字母 Mu,而 Mu 表示 “微” 的意思。当然,函数 返回的时候 timeout 可能是剩余的时间,之所以是可能,是因为它依赖于 你的 Unix 操作系统。

哈!我们现在有一个微秒级的定时器!别计算了,标准的 Unix 系统 的时间片是100毫秒,所以无论你如何设置你的数据结构 struct timeval, 你都要等待那么长的时间。

还有一些有趣的事情:如果你设置数据结构 struct timeval 中的数据为 0,select() 将立即超时,这样就可以有效地轮询集合中的所有的文件描述 符。如果你将参数 timeout 赋值为 NULL,那么将永远不会发生超时,即 一直等到第一个文件描述符就绪。最后,如果你不是很关心等待多长时间, 那么就把它赋为 NULL 吧。

下面的代码演示了在标准输入上等待 2.5 秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 /* file descriptor for standard input */
main()
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
/* don't care about writefds and exceptfds: */
select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
printf("A key was pressed!\n");
else
printf("Timed out.\n");
}

如果你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),否则无论如何它都会超时。
现在,你可能回认为这就是在数据报套接字上等待数据的方式—你是对 的:它可能是。有些 Unix 系统可以按这种方式,而另外一些则不能。你 在尝试以前可能要先看看本系统的 man page 了。

最后一件关于 select() 的事情:如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符加入到 readfds 集合中来看是 否有新的连接。

gRPC编程

python安装

  1. 安装必要的依赖项:

    1
    sudo yum install gcc openssl-devel bzip2-devel libffi-devel zlib-devel
  2. 下载Python 3.7的源代码:

    1
    wget https://www.python.org/ftp/python/3.7.12/Python-3.7.12.tgz
  3. 解压缩源代码文件:

    1
    tar xzf Python-3.7.12.tgz
  4. 进入解压缩后的目录:

    1
    cd Python-3.7.12
  5. 配置编译选项:

    1
    ./configure --enable-optimizations
  6. 编译并安装Python 3.7:

    1
    make && sudo make install
  7. 安装完成后,您可以使用以下命令验证Python 3.7的安装:

    1
    python3.7 --version

2.安装gRPC与gRPC工具

1
2
python3 -m pip install grpcio
python3 -m pip install grpcio-tools

3. git克隆代码

1
git clone -b v1.55.0 --depth 1 https://github.com/grpc/grpc

4. 运行

1
2
python greeter_server.py
python greeter_client.py

Java RMI编程

1. 安装Java

CentOS 7.7 64位

1
2
3
4
5
6
7
8
9
10
sudo yum install wget
wget https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz
tar -xvf openjdk-11.0.2_linux-x64_bin.tar.gz
sudo mv jdk-11.0.2 /usr/local/
sudo vi /etc/profile
# 在文件的末尾添加以下内容:
export JAVA_HOME=/usr/local/jdk-11.0.2
export PATH=$PATH:$JAVA_HOME/bin
source /etc/profile
java -version

2. 定义接口

1
2
3
4
5
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemoteInterface extends Remote{
public String sayHello(String name) throws RemoteException;
}
1
2
3
4
5
6
7
8
9
10
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemoteInterface{
public MyRemoteImpl() throws RemoteException{
super();
}
public String sayHello(String name) throws RemoteException{
return "Hello," + name + "!";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.Naming;
import java.rmi.RemoteException;
public class MyRemoteServer{
public static void main(String[] args){
try{
MyRemoteImpl obj = new MyRemoteImpl();
Naming.rebind("rmi://localhost/MyRemote",obj);
System.out.println("MyRemote is ready.");
}
catch(Exception e){
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.Naming;
import java.rmi.RemoteException;
public class MyRemoteClient{
public static void main(String[] args){
try{
MyRemoteInterface obj = (MyRemoteInterface) Naming.lookup("rmi://localhost/MyRemote");
String response = obj.sayHello("xinbinsun\n2020204337\n2023.5.27");
System.out.println("Response:" + response);
}
catch(Exception e){
e.printStackTrace();
}
}
}

运行RMI注册表

1
rmiregistry

编译文件

1
2
javac MyRemoteClient.java
javac MyRemoteServer.java

运行

1
2
java MyRemoteServer
java MyRemoteClient

问题:运行RMI注册表时需要单独占用一个终端,所以,运行成功RMI实验,至少需要同时打开三个终端

MoM编程

1
2
3
4
5
6
wget https://dist.apache.org/repos/dist/release/rocketmq/5.1.1/rocketmq-all-5.1.1-source-release.zip
yum install unzip zip
unzip rocketmq-all-5.1.1-source-release.zip
sudo yum install maven
mvn -Prelease-all -DskipTests -Dspotbugs.skip=true clean install -U
vim nohup.out

RocketMQ: https://rocketmq.apache.org/zh/docs/quickStart/01quickstart