go网络编程

架构

本章涵盖了分布式系统架构的主要特性:

前言

如果不知道我们想构建什么,就无法构建一个系统。而如果我们不知道它会在何种环境下工作, 也同样不行。就像 GUI 程序不同于批处理程序,游戏程序不同于商业程序一样,分布式程序也不同于独立的程序。它们都有各自的方法,常见的模式,经常出现的问题以及常用的解决方案。

Protocol Layers 协议层

分布式系统很复杂,它涉及到多台计算机的连接方式。我们编写的程序必须能在该系统中的每一台计算机上运行,它们必须都能协同操作来完成一项分布式任务。

解决这种复杂性的一般方法,就是将它分解为更小更简单的部分。这些部分都有它们自己的 结构,但也定义了与其它相关部分进行通信的方式。在分布式系统中,这种部分称为协议层, 它们的功能都有明确的定义。它们在一起形成层次结构,并与其各自的上下层进行通行。层次之间的通信则由协议来定义。

网络通信所需的协议覆盖了从上层应用通信一直到底层有线通信的所有方式,它们的复杂性 通过在协议层中进行封装来处理。

ISO OSI 协议

尽管 OSI(开放系统互联)协议从未被完整地实现过,但它仍对分布式系统的讨论和设计产 生了十分重要的影响。它的结构大致为下图所示:
在这里插入图片描述

OSI 层

每一层的功能为:

  • 网络层提供交换和路由技术
  • 传输层在终端系统间提供透明的数据传输,并负责对端的错误恢复及流程控制
  • 会话层在应用间建立、管理并结束连接
  • 表现层提供数据的展示
  • 应用层支持应用与最终用户的处理

TCP/IP 协议

一些可选的协议:

尽管现在到处都是 TCP/IP 协议,但它并不是唯一存在的。从长远来看,它甚至不会是最成 功的。还有些协议占有重要的地位,比如:

  • 火线
  • USB
  • 蓝牙
  • Wifi

网络

网络是一个通信系统,它连接了称为主机的最终系统。这种连接机制可以是网线、以太网、光迁或无线,但这些与我们无关。局域网(LAN)将计算机紧密连接在一起,一般为家庭、公司的一部分。

广域网(WAN)连接起一个更大物理区域的计算机,例如城际间。还有些其它的类型,如 城域网(MAN)、个人域网(PAN)甚至人体域网(BAN)。

互联网是多个不同网络的连接,一般为 LAN 或 WAN。内联网是属于某个组织的所有网络加上互联网。

互联网与内联网之间有明显的不同。一般来说,一个内联网处在单一的管控之下,它将被应用一组统一的策略。另一方面,一个互联网则不会在单一主体的控制之下,控制的不同部分 甚至可能会不兼容。

这种不同的一个例子,就是一个内联网通常被少量供应商提供的,运行着特定操作系统标准 化版本的计算机所 制。另一方面,一个互联网通常有各种各样的计算机和操作系统。

网关 Gateways

网关是一个统称,它用于连接起一个或多个网络。其中的中继器在物理层面上进行操作,它将信息从一个子网复制到另一个子网上。桥接在数据连接层面上进行操作,它在网络之间复制帧。路由器在网络层面上进行操作,它不仅在网络之间复制信息,还决定了信息的传输路线。

数据包封装 Packet encapsulation

在 OIS 或 TCP/IP 协议栈层与层之间的通信,是通过将数据包从一个层发送到下一个层,最终穿过整个网络的。每一层都有必须保持其自身层的管理信息。从上层接收到的数据包在向下传递时,会添加头信息。在接收端,这些头信息会在向上传递时移除。

在这里插入图片描述

连接模型 Connection Models

为了两个计算机进行通信,就必须建立一个路径,使他们能够在一个会话中发送至少一条消息。有两个主要的模型:

  • 面向连接模型 Connection oriented

    即为会话建立单个连接,沿着连接进行双向通信。当会话结束后,该连接就会断开。这类似于电话交谈。例子就是 TCP。

  • 无连接模型 Connectionless

    在无连接系统中,消息的发送彼此独立。这类似于普通的邮件。无连接模型的消息可能不按顺序抵达。例子就是 IP 协议。面向连接的传输可通过无连接模型——基于 IP 的 TCP 协议建立。无连接传输可通过面向连接模型——基于 IP 的 HTTP 协议建立。

    这些是可变的。例如,会话可能会强制消息抵达,但可能无法保证它们按照发送的顺序抵达。 不过这两个是最常见的。

通信模型 Communications Models

  • 消息传递 Message passing

    一些非过程化语言建立在消息传递原理上。并发语言经常使用这种机制,最有名的大概要数 Unix 的管道了。Unix 管道就是一管字节,但它并没有固定的制:微软的 PowerShell 可沿着其管道发送对象;而像 Parlog 这样的并发语言,则能在并发的进程之间,将任意的逻辑数据结构当做消息来发送。

    消息传递是分布式系统最基本的机制,也就是建立连接并通过它传输一些数据。在另一端则 需要理解这些消息的意思并做出响应,有时还需要返回一些消息。如下图所示:在这里插入图片描述
    诸如 X 窗口系统之类的底层事件驱动系统功能也采用了类似的方式:等待用户的消息(如 鼠标点击等),对它们进行解码并做出反应。

    更高层的事件驱动系统则假定底层系统已经解码完成,接着该事件被分配给适当的对象,如 ButtonPress 处理程序。这也适用于分布式消息传递系统,通过对从网络接收的消息进行部分 解码,并分配给适当的处理程序。

  • 远程过程调用 Remote procedure call

    在任何系统中,都有信息传输和流程控制来将该系统的一部分传到另一部分。在过程化语言中,它由过程调用来组成,其中的信息被放置到调用栈上,接着控制流程被传递至该程序的另一部分。

    甚至过程调用也有变化。代码可被静态链接,以便于控制从该程序可执行代码的一部分传输到另一部分。随着库例程的使用日益增多,将这类代码作为动态链接库(DLL)也变得司空见惯了,它用来控制传输独立的代码片段。

    DLL 作为调用代码运行在相同的机器上。尽管对于不同机器上运行的过程传输控制来说, 这种机制(在概念上)是一种简单的手段,但它实际上可不怎么简单!不过,这种控制模型却催生了“远程过程调用”(RPC)。如下图所示:
    在这里插入图片描述

分布式计算模型

在最上层,我们可以考虑分布式系统的组件是否等价。最常见的就是不对等的情况:客户端向服务器发送请求,然后服务端响应。这就是客户端-服务器系统。

若两个组件等价,且均可发起并响应信息,那么我们就有了一个点对点系统。注意这是个逻辑上的分类:一点可能是 16,000 个核心主机,而另一点可能只是个移动电话。但如果二者的行为类似,那么它们就都是点。

第三种模型也就是所谓的过滤器。有一个组件将信息传至另一个组件,它在修改该信息后会传至第三个组件。这是个相当普遍的模型:例如,中间组件通过 SQL 从数据库中获取信息, 并将其转换为 HTML 表单提供给第三个组件(它可能是个浏览器)。如下所示:

在这里插入图片描述

客户端/服务器系统

  • 客户端/服务器系统的另一种方式:
    在这里插入图片描述

客户端/服务器应用:

  • 第三种方式:
    在这里插入图片描述

服务器分布

  • 客户端/服务器系统并不简单。其基本模型是单一客户端,单一服务器。
    在这里插入图片描述
  • 不过也可以有多个客户端,单一服务器,这样,主站只需接收请求并处理一次,而无需将它们传递给其它服务器来处理。当客户端可能并发时,这就是个通用的模型。
    在这里插入图片描述
  • 还有单一客户端,多个服务器的情况:当一个服务器需要作为其它服务器的客户端时,这种情况就会经常发生,例如当业务 逻辑服务器从数据库服务器获取信息时。当然,还可以有多个客户端,多个服务器的情况。
    在这里插入图片描述

组件分布

分解一些应用的一个简单有效的方式就是把它们看做三部分:

  • 表现组件

    表现组件负责与用户进行交互,即显示数据和采集输入。它可以是带有按钮、列表和菜单等等的现代 GUI 界面,或较老的命令行式界面,询问问题并获取答案。

  • 应用逻辑

    应用逻辑组件负责解释用户的响应,根据应用业务规则,准备查询并管理来自其组件的响应。

  • 数据访问

    数据访问组件负责存储并检索数据。这一般是通过数据库进行,不过也不一定,也可能是NoSql,或者文件。

Gartner 分类

基于这三部分的应用划分,Gartner 公司考虑了这些组件在客户端-服务器系统中如何分布。 他们想出了五种模型:

  • 分布式数据库

    高德地图的离线缓存形式是一个很好的例子。所有的地图都在 高德 的服务器上。当用户设置离线缓存时,“附近的”地图也会下载为一个小型数据库到手机上。当用户移动了一点地图时,额外的一点请求已经为快速响应在本地存储中了。
    在这里插入图片描述

  • 网络文件服务
    Gartner 第二种分类允许远程客户端访问已共享的文件系统,这里有一些这类系统的例子:NFS、Microsoft 共享和 ceph 等等。
    在这里插入图片描述

  • Web
    在这里插入图片描述

  • 终端仿真
    Gartner 第四种分类就是终端仿真。这允许远程系统在本地系统上作为普通的终端,Telnet 就是最常见的例子。
    在这里插入图片描述

  • 预期(Expect)

    预期(Expect)是 Gartner 第五种分类的一种另类的演示。它的行为类似于命令行接口这样 的经典系统。它在此之上建立了 X 窗口界面,以此来让用户与 GUI 进行交互,然后 GUI 转而与命令行界面进行交互。
    在这里插入图片描述

三层模型

当然,如果有两层,那么也可以有三层、四层甚至多层。下图展示了一些可能的三层模型:
在这里插入图片描述
Web 就是最右边那种模型很好的例子。后端建立为一个数据库,来保存一些数据库逻辑。中间层是一个 Nginx 这样的运行 PHP 脚本(或 Python on GoLand) 的 HTTP 服务器。这会管理一些逻辑和存储在本地的像 HTML 页面这样的数据。

“胖”与“瘦”

组件一般分为“胖”或“瘦”。“胖”组件占用大量的内存来做复杂的处理;“瘦”组件则恰恰相反,只占少量内存,做简单处理。似乎没有任何“正常”大小的组件,只有“胖”或 “瘦”!

中间件模型

中间件是连接器分布式系统组件的“胶水”层。中间件模型如图所示:
在这里插入图片描述

中间件

中间件组件包括:

  • 像 TCP/IP 这样的网络服务
  • 中间件层是应用独立的,使用网络服务的软件
  • 中间件的例子:DCE、RPC、Corba
  • 中间件可能只执行一种功能(比如 RPC)或多种功能(比如 DCE)

中间件示例

中间件的例子包括:

  • 像终端模拟器、文件传输或电子邮件这样的基础服务
  • 像 RPC 这样的基础服务
  • 像 DCE、网络 O/S 这样的一体化服务
  • 像 CORBA、OLE/ActiveX 这样的分布式对象服务
  • 像 RMI、Jini 这样的移动对象服务
  • 万维网

中间件的功能

中间件的功能包括:

  • 在不同计算机上初始化过程
  • 进行会话管理
  • 允许客户端定位服务器的目录服务
  • 进行远程数据访问
  • 允许服务器处理多个客户端的并发控制
  • 保证安全性和完整性
  • 监控
  • 终止本地处理和远程处理

连续处理

Gartner 模型基于将一个应用分解为表现组件、应用逻辑和数据处理。一个更细粒度的分解方式为:
在这里插入图片描述

故障点

分布式应用一般运行在复杂的环境中。这使得它比单一计算机上的独立应用更易发生故障。 故障点包括:

  • 应用可能会在客户端崩溃
  • 客户端系统可能发生硬件问题
  • 客户端的网卡可能发生故障
  • 网络连接可能超时
  • 网络地址可能冲突
  • 像路由器这样的网络基础设备可能发生故障
  • 传输错误可能会失去消息
  • 客户端与服务器的版本可能不兼容
  • 服务器的网卡可能发生故障
  • 服务器系统可能发生硬件问题
  • 服务器的软件可能崩溃
  • 服务器的数据库可能损坏

在设计应用时必须考虑这些可能发生的故障。如果故障发生在系统的其它部分,那么由任何 一个组件执行的操作都必须可恢复。这就需要采用事务和持续错误检测这类的计算来避免错误。

接受因素

  • 可靠性
  • 性能
  • 响应性
  • 可扩展性
  • 可容性
  • 安全性

透明度

分布式系统的“圣杯”就是提供以下几点:

  • 访问透明度
  • 位置透明度
  • 迁移透明度
  • 赋值透明度
  • 并发透明度
  • 扩展透明度
  • 性能透明度
  • 故障透明度

分布式计算的八个误区

常见误区:

  • 网络是可靠的
  • 风险为零
  • 带宽是无限的
  • 网络是安全的
  • 拓扑结构不会改变
  • 没有管理员
  • 传输成本为零
  • 网络是均等的

这些问题直接影响着网络编程。例如,大部分远程过程调用系统的设计都基于网络是可靠的前提,从而导致了远程过程调用的行为与本地调用如出一辙。零风险和无带宽的误区也导 致了 RPC 调用的持续时间与本地调用相同的臆断,但实际上它要比本地调用慢很多。

套接字级编程

TCP/IP 协议栈

TCP/IP 等于传输控 制协议/互联网协议。

TCP/IP 协议栈是 OSI 模型的一部分:
在这里插入图片描述
TCP 是一个面向连接的协议,UDP(User Datagram Protocol,用户数据报协议)是一种无连接的协议。

IP 数据包

IP 层提供了无连接的不可靠的传输系统,任何数据包之间的关联必须依赖更高的层来提供。

IP 层包头支持数据校验,在包头包括源地址和目的地址。

IP 层通过路由连接到因特网,还负责将大数据包分解为更小的包,并传输到另一端后进行 重组。

UDP

UDP 是无连接的,不可靠的。它包括 IP 数据报的内容和端口号的校验。

TCP

TCP 是构建于 IP 之上的面向链接的协议。它提供了一个虚电路使得两个应用进程可以通过它来通信。它通过端口号来识别主机上的服务。

互联网地址

要想使用一项服务,就必须先能找到它。互联网使用地址定位例如计算机的设备。这种寻址方案最初被设计出来只允许极少数的计算机连接上,使用 32 位无符号整形,拥有高达 2^32 个地址。这就是所谓的 IPv4 地址。近年来,连接(至少可以直接寻址)的设备的数量可能 超过这个数字,所以在不久的某一天我们将切换到利用 128 位无符号整数,拥有高 2^128 个 地址的 IPv6 寻址。

IPv4 地址

IP 地址是一个 32 位整数构成。每个设备的网络接口都有一个地址。该地址通常使用’.'符号 分割的 4 字节的十进制数,例如:“127.0.0.1” 或 “14.215.177.38”。

所有设备的 IP 地址,通常是由两部分组成:网段地址和网内地址。从前,网络地址和网内地址的分辨很简单,使用字节构建 IP 地址。

IPv6 地址

IPv6 使用 128 位地址,即使表达同样的地址,字节数变得很麻烦,由’:'分 隔的 4 位 16 进制组成。一个典型的例子如:2002:c0e8:82e7:0:0:0:c0e8:82e7。

IP 类型

"net"包定义了许多类型, 函数,方法用于 Go 网络编程。IP 类型被定义为一个字节数组。

type IP []byte

有几个函数来处理一个 IP 类型的变量, 但是在实践中你很可能只用到其中的一些。例如, ParseIP(String)函数将获取逗号分隔的IPv4或者冒号分隔的IPv6地址, 而IP方法的字符串将 返回一个字符串。请注意,可能无法取回你期望的: 字符串 0:0:0:0:0:0:0:1 是::1。

package main
import (
	"fmt"
	"net"
	"os"
)
func main(){
	if len(os.Args) !=2 {
		fmt.Println(os.Args)
		fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
		os.Exit(1)
	}
	name := os.Args[1]
	addr := net.ParseIP(name)
	if addr == nil{
		fmt.Println("Invalid address")
	}else{
		fmt.Println("The address is ", addr.String())
	}
	os.Exit(0)
}

如果执行文件 IP,那么它可以运行如:

➜ network go run ip.go 127.0.0.1

# 得到结果
The address is  127.0.0.1

IP 掩码

为了处理掩码操作,有下面类型:

type IPMask []byte

下面这个函数用一个 4 字节的 IPv4 地址来创建一个掩码:

func IPv4Mask(a, b, c, d byte) IPMask

另外, 这是一个 IP 的方法返回默认的掩码:

func (ip IP) DefaultMask() IPMask

需要注意的是一个掩码的字符串形式是一个十六进制数,如掩码 255.255.0.0 为 ffff0000。

一个掩码可以使用一个 IP 地址的方法,找到该 IP 地址的网络

func (ip IP) Mask(mask IPMask) IP

IPAddr 类型

在 net 包的许多函数和方法会返回一个指向 IPAddr 的指针。这不过只是一个包含 IP 类型的结构体。

type IPAddr { 
	IP IP
}

这种类型的主要用途是通过 IP 主机名执行 DNS 查找。

func ResolveIPAddr(network, address string) (*IPAddr, error)

其中 network 是"ip","ip4"或者"ip6"的其中一个. 下面的程序中将会展示。

/* resolveIP
* DNS 查找
*/
package main

import (
	"fmt"
	"net"
	"os"
)

func main(){
	if len(os.Args) != 2{
		fmt.Fprintf(os.Stderr,"Usage:%s hostname\n",os.Args[0])
		fmt.Println("Usage: ", os.Args[0], "hostname")
		os.Exit(1)
	}
	name := os.Args[1]
	addr,err := net.ResolveIPAddr("ip",name)
	if err != nil{
		fmt.Println("Resolution error", err.Error())
		os.Exit(1)
	}
	fmt.Println("Resolved address is ",addr.String())
}

运行 resolveIP www.baidu.com 返回

➜ network go run resolveIP.go www.baidu.com
	Resolved address is  14.215.177.38

主机查询

ResolveIPAddr 函数将对某个主机名执行 DNS 查询,并返回一个简单的 IP 地址。然而,通常主机如果有多个网卡,则可以有多个 IP 地址。它们也可能有多个主机名,作为别名。

func LookupHost(host string) (addrs []string, err error) {

这些地址将会被归类为“canonical”主机名。如果你想找到的规范名称,使用 func LookupCNAME(host string) (cname string, err error)

下面是一个演示程序:

package main
/*
* lookupHost
* 查找主机
 */

import (
	"fmt"
	"net"
	"os"
)


func main(){
	if len(os.Args) != 2{
		fmt.Fprintf(os.Stderr, "Usage: %s hostname\n", os.Args[0])
		os.Exit(1)
	}
	name := os.Args[1]
	addr, err := net.LookupHost(name)
	if err != nil{
		fmt.Println("Error: ", err.Error())
		os.Exit(2)
	}
	for _,s := range addr{
		fmt.Println("host:",s)
	}

}

运行 lookupHost www.baidu.com 返回

➜ network go run lookupHost.go www.baidu.com
host: 14.215.177.39
host: 14.215.177.38

服务

服务运行在主机。它们通常长期存活,同时被设计成等待的请求和响应请求。有许多类型的服务,有他们能够通过各种方法向客户提供服务。互联网的世界基于 TCP 和 UDP 这两种通 信方法提供许多这些服务,虽然也有其他通信协议如 SCTP 伺机取代。许多其他类型的服务, 例如点对点, 远过程调用, 通信代理, 和许多其他建立在 TCP 和 UDP 之上的服务之上。

Ports 端口

服务存活于主机内。IP 地址可以定位主机。但在每台计算机上可能会提供多种服务,需要 一个简单的方法对它们加以区分。TCP,UDP,SCTP 或者其他协议使用端口号来加以区分。 这里使用一个 1 到 65,535 的无符号整数,每个服务将这些端口号中的一个或多个相关联。

有很多“标准”的端口。Telnet 服务通常使用端口号 23 的 TCP 协议。DNS 使用端口号 53 的 TCP 或 UDP 协议。SSH 通常使用 22 端口。HTTP 通常使用 80 端口,HTTPS 通常使用 443 端口,但经常使用,端口 8000,8080 和 8088,协议为 TCP。

在 Unix 系统中, /etc/services 文件列出了常用的端口。Go 语言有一个函数可以获取该文件。

func LookupPort(network, service string) (port int, err error) 

network 是一个字符串例如"tcp"或"udp", service 也是一个字符串,如"telnet"或"domain"(DNS)。

示例程序如下:

package main

import (
	"fmt"
	"net"
	"os"
)

/*
lookupPort
*/

func main(){
	if len(os.Args) != 3{
		fmt.Fprintf(os.Stderr,
			"Usage: %s network-type service\n",
			os.Args[0])
		os.Exit(1)
	}
	networkType := os.Args[1]
	service := os.Args[2]
	port,err := net.LookupPort(networkType,service)
	if err != nil {
		fmt.Println("Error: ", err.Error())
		os.Exit(2)
	}
	fmt.Println("Service port ", port)

}

运行 lookupPort tcp http 打印 Service port: 80

TCPAddr 类型

TCPAddr 类型包含一个 IP 和一个 port 的结构:

type TCPAddr struct { 
	IP IP
	Port int
}

函数 ResolveTCPAddr 用来创建一个 TCPAddr

func ResolveTCPAddr(network, address string) (*TCPAddr, error) 

net 是"tcp", “tcp4"或"tcp6"其中之一,addr 是一个字符串,由主机名或 IP 地址,以及”:“后跟 随着端口号组成,例如:“www.baidu.com:80” 或 '127.0.0.1:22”。如果地址是一个 IPv6 地址, 由于已经有冒号,主机部分,必须放在方括号内, 例如:“[::1]:23”。另一种特殊情况是经常用于服务器, 主机地址为 0, 因此,TCP 地址实际上就是端口名称, 例如:“:80” 用来表示 HTTP 服务器。

TCP Sockets

当知道如何通过网络和端口 ID 查找一个服务时,然后呢?如果是一个客户端,需要一个 API,让其连接到服务,然后将消息发送到该服务,并从服务读取回复。

如果是一个服务器,需要能够绑定到一个端口,并监听它。当有消息到来,需要能够读取它并回复客户端。

net.TCPConn 是允许在客户端和服务器之间的全双工通信的 Go 类型。两种主要方法是。

func (c *TCPConn) Write(b []byte) (n int, error)
func (c *TCPConn) Read(b []byte) (n int, error)

TCPConn 被客户端和服务器用来读写消息。

TCP client

一旦客户端已经建立 TCP 服务, 就可以和对方设备"通话"了. 如果成功,该调用返回一个用于通信的 TCPConn。客户端和服务器通过它交换消息。通常情况下,客户端使用 TCPConn 写入请求到服务器, 并从 TCPConn 的读取响应。持续如此,直到任一(或两者)的两侧关 闭连接。客户端使用该函数建立一个 TCP 连接。

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, error)

其中 laddr 是本地地址,通常设置为 nil 和 raddr 是一个服务的远程地址, net 是一个字符串, 根据是否希望是一个 TCPv4 连接,TCPv6 连接来设置为"tcp4", "tcp6"或"tcp"中的一个,当然也可以不关心链接形式。

一个简单的例子,展示个客户端连接到一个网页(HTTP)服务器。

客户端可能发送的消息之一就是“HEAD”消息。这用来查询服务器的信息和文档信息。 服 务器返回的信息,不返回文档本身。发送到服务器的请求可能是。

"HEAD / HTTP/1.0\r\n\r\n"

这是在请求服务器的根文件信息。 一个典型的响应可能是:

HTTP/1.0 200 OK
ETag: "-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23

我们首先通过(GetHeadInfo.go)程序来建立 TCP 连接,发送请求字符串,读取并打印响应。 编译后就可以调用,例如:

控制 TCP 连接

Timeout

服务端会断开那些超时的客户端,如果他们响应不够快,比如没有及时往服务端写一个请求。 这应该是长时间(几分钟)的,因为用户可能花费了时间。相反, 客户端可能希望超时服务器 (一个更短的时间后)。通过下面的来实现这两种:

func (c *TCPConn) SetTimeout(nsec int64) error

存活状态

即使没有任何通信,一个客户端可能希望保持连接到服务器的状态。可以使用:

func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

UDP 数据报

在一个无连接的协议中,每个消息都包含了关于它的来源和目的地的信息。没有"session"建 立在使用长寿命的套接字。UDP 客户端和服务器使用的数据包,单独包含来源和目的地的 信息。除非客户端或服务器这样做,否则消息的状态不会保持。这些消息不能保证一定到达, 也可能保证按顺序到达。

客户端最常见的情况发送消息,并希望响应正常到达。服务器最常见的情况为将收到一条消 息,然后发送一个或多个回复给客户端。而在点对点的情况下, 服务器可能仅仅是把消息 转发到其他点。

Go 下处理 TCP 和 UDP 之间的主要区别是如何处理多个客户端可能同时有数据包到达,没 有一个管理 TCP 会话的缓冲。主要需要调用的是:

func ResolveUDPAddr(network, address string) (*UDPAddr, error) 
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

服务器侦听多个套接字

一个服务器可能不止在一个端口监听多个客户端,或是更多端口,在这种情况下,它在端口之间使用某种轮询机制。

在 C 中, 调用的内核 select()可以完成这项工作。 调用需要一个文件描述符的数字。该进程 被暂停。当 I/O 准备好其中一个,一个唤醒被完成,并且该过程可以继续。This is cheaper than busy polling。在 Go 中, 完成相同的功能,通过为每个端口使用一个不同的 goroutine。低级别 的 select()时发现,I/O 已经准备好该线程,一个线程将运行。

Conn,PacketConn 和 Listener 类型

我们已经区分 TCP 和 UDP API 的不同,使用例子 DialTCP 和 DialUDP 分别返回一 个 TCPConn 和 UDPConn。Conn 类型是一个接口,TCPConn 和 UDPConn 实现了该接口。 在很大程度上,可以通过该接口处理而不是用这两种类型。

可以使用一个简单的函数,而不是单独使用 TCP 和 UDP 的 dial 函数。

func Dial(network, address string) (Conn, error)

net 可以是"tcp", “tcp4” (IPv4-only), “tcp6” (IPv6-only), “udp”, “udp4” (IPv4-only), “udp6” (IPv6-only), “ip”, “ip4” (IPv4-only)和"ip6" (IPv6-only)任何一种。它将返回一个实现了 Conn 接口 的类型。注意此函数接受一个字符串而不是 raddr 地址参数,因此,使用此程序可避免的地址类型。

数据序列化

客户端与服务之间通过数据交换来通信。因为数据可能是高度结构化的,所以在传输前必须进行序列化。下面将研究序列化基础并介绍一些 Go API 提供的序列化技术。

简介

客户端与服务器需要通过消息来交换信息。TCP 与 UDP 是消息传递的两种机制,在这两种 机制之上就需要有合适的协议来约定传输的内容的含义。

程序通常构造一个复杂的数据结构来保存其自身当前的状态。在与远程的客户端或服务的交 互中,程序会通过网络将这样的数据结构传输到 -应用程序所在的地址空间之外的地方。

编程语言使用的结构化的数据类型有:

  • 记录/结构

  • 可变记录

  • 数组 - 固定大小或可变大小

  • 字符串 - 固定大小或可变大小

  • 表 - 例如:记录构成的数组

  • 非线程结构,比如

    循环链表
    二叉树
    含有其他对象引用的对象

IP,TCP 或者 UDP 网络包并不知道这些数据类型的含义,它们只是字节序列的载体。因此, 写入网络包的时候,应用需要将要传输的(有类型的)数据 序列化 成字节流,反之,读取网 络包的时候,应用需要将字节流反序列化成合适的数据结构,这两个操作被分别称为编组和解组。

JSON

JSON 全称是 JavaScript Object Notation,它是一种应用于 JavaScript 系统之间传递数据的轻量级 格式。它使用基于文本的格式,因为足够通用,现在已经成为了多种编程语言采用的通用的 序列化方法了。

JSON 序列化对象,数组和基本值。基本值包括:字符串,数字,布尔值和 NULL 值。数组 是逗号分割的一组值的列表,可以用来表示各种编程语言中的数组、向量、列表或者序列。 它们由方括号来界定,对象则由一个包含在大括号中的“field: values”对构成的列表来表示。

从 Go JSON.Marshal 函数的规范文档可知,JSON 包将在编组时使用以下类型相关的默认编码方法:

  • 布尔值被编码为 JSON 的布尔值。
  • 浮点数与整数被编码为 JSON 的数字值。
  • 字符串值编码为强制为有效UTF-8的JSON字符串,使用Unicode替换符文替换无效字节。
  • 数组与 Slice 会被编码为 JSON 数组,但是[]byte 是会被编码为 base64 字符串。
  • 结构体被编码为 JSON 对象。每一个结构体字段被编码为此对象的对应成员,默认情况下对象的 key 的名字是对应结构体字段名的小写。如果此字段含有tag,则此 tag将是最终对象 key 的名字。
  • map 值被编码为 JSON 对象,此 map 的 key 的类型必须是 string;map 的 key 直接被当作 JSON 对象的 key。
  • 指针值被编码为指针所指向的值。空指针被编码为空 JSON 对象。
  • 接口值被编码为接口实际包含的值。空接口被编码为空 JSON 对象。
  • 通道、复数和函数值不能用JSON编码。如果对这样的值进行编码会导致封送处理UnsupportedTypeError。
  • JSON不能表示循环数据结构,Marshal也不能处理它们。将循环结构传递给封送处理将导致一个错误。

将 JSON 数据存入文件的示例如下:

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Person struct {
	Name Name
	Email []Email
}
type Name struct {
	Family string
	Personal string
}
type Email struct {
	Kind string
	Address string
}
func main(){
	person := Person{
		Name: Name{
			Family: "Newmarch",
			Personal: "Jan",
		},
		Email: []Email{
			Email{
				Kind: "home",
				Address: "jan@newmarch.name",
			},
			Email{
				Kind: "work",
				Address: "j.newmarch@boxhill.edu.au",
			},
		},
	}
	saveJSON("saveJson.json",person)
}
func saveJSON(fileName string,person interface{}){
	outFile,err := os.Create(fileName)
	defer outFile.Close()
	checkError(err)
	encoder := json.NewEncoder(outFile)
	err = encoder.Encode(person)
	checkError(err)

}

func checkError(err error) {
	if err != nil {
		fmt.Println("Fatal error ", err.Error())
		os.Exit(1)
	}
}

可以这样将之重新加载到内存中:

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Person struct {
	Name Name
	Email []Email
}
type Name struct {
	Family string
	Personal string
}
type Email struct {
	Kind string
	Address string
}
func (p Person) String () string{
	s := fmt.Sprintf("personal:%s,family:%s",p.Name.Personal,p.Name.Family)
	for _,v := range p.Email{
		s += fmt.Sprintf("\nkind:%s,address:%s",v.Kind,v.Address)
	}
	return s
}
func main(){
	var person Person
	loadJSON("saveJson.json",&person)
	fmt.Println(person.String())
}

func loadJSON(fileName string,person interface{}){
	intFle,err := os.Open(fileName)
	checkError(err)
	defer intFle.Close()
	decoder := json.NewDecoder(intFle)
	err = decoder.Decode(person)
	checkError(err)
}
func checkError(err error) {
	if err != nil {
		fmt.Println("Fatal error ", err.Error())
		os.Exit(1)
	}
}

安全

管互联网最初被设计为可以承受敌对代理攻击的系统,但它过去一直是在一个可信的实体和合作的环境中发展起来的。可惜现在已经时过境迁。垃圾邮件,拒绝服务攻击,网络钓鱼这些东西使得每一个上网者都需要自行承担风险。

应用程序应当在复杂的互联网环境中仍然可以正确工作。“正确”不光意味着程序功能的正 确,同时还意味着要确保数据传输过程中的保密性和完整性,甚至只允许合法用户进行访问和其它问题。

这自然使得编程更加复杂。在构建安全应用程序的过程中,会出现很复杂和微妙的问题。如果想自己这样做(如实现一个自有安全库),通常都会以失败而告终。相反,需要使用安全专家设计的安全库。

ISO 安全架构

ISO OSI(开放系统互连)七层模型分布式系统是众所周知的,少为人知的是,ISO 在此架构的基础上建立了一系列完整的文档。而我们这里最重要的是 ISO 安全体系结构模型(ISO Security Architecture model)ISO 7498-2。

功能层次

主要的安全系统功能
  • 认证 - 身份校验文件
  • 数据完整性 - 数据不被篡改
  • 保密 - 数据不能暴露给他人
  • 公证/签名
  • 访问控制
  • 保证/可用性
必须的 OSI 协议栈
  • 对等实体认证(3,4,7)
  • 数据源认证(3,4,7)
  • 访问控制服务(3,4,7)
  • 连接保密(1,2,3,4,6,7)
  • 无连接的保密(1,2,3,4,6,7)
  • 选择性字段的保密(6,7)
  • 传输保密(1,3,7)
  • 恢复连接的完整性(4,7)
  • 不可恢复连接的完整性(4,7)
  • 选择字段连接完整性(7)
  • 选择字段的无连接完整性(7)
  • 接受源(7)
  • 接受回执(7)
机制
  • 对等实体认证

    o 加密

    o 数字签名

    o 交换验证

  • 数据源认证

    o 加密

    o 数字签名

  • 访问控制服务

    o 访问控制列表

    o 密码

    o 范围列表

    o 等级

  • 连接保密

    o 密码

    o 路由控制

  • 无连接的保密

    o 密码

    o 路由控制

  • 选择性字段的保密

    o 密码

  • 传输保密

    o 密码

    o 传输填充

    o 路由控制

  • 恢复连接的完整性

    o 密码

    o 数据完整性

  • 不可恢复连接的完整性

    o 密码

    o 数据完整性

  • 选择字段连接完整性

    o 密码

    o 数据完整性

  • 无连接完整性

    o 密码

    o 数字签名

    o 数据完整性

  • 选择字段的无连接完整性

    o 密码

    o 数字签名

    o 数据完整性

  • 接受源

    o 数字签名

    o 数据完整性

    o 公正

  • 接受回执

    o 数字签名

    o 数据完整性

    o 公正

数据完整性

确保数据的完整性意味着要提供一个数据未被篡改的测试方法。通常是通过字节数据生成一个简单的数字。这个操作被称为 hashing,结果数字成为 hash 或者 hash 值。

Go 支持几个 hash 算法,包括 MD4,MD5,RIPEMD-160,SHA1,SHA224,SHA256,SHA384 and SHA512。它们都尽可能按照 Go 程序员关注的,遵循相同的模式:在适当的包中定义 New 或类似的方法,返回一个 hash 包中的 Hash 对象。

一个 Hash 结构体拥有一个 io.Writer 接口,你可以通过 writer 方法写入被 hash 的数据。可以通过 Size 方法获取 hash 值的长度,Sum 方法返回 hash 值。

key 对称加密

数据加密有两种机制。第一种方式是在加密和解密时都使用同一个 key。加密方和解密方都需要知道这个 key。此处如何在这两者之间传输这个 key。

目前有很多使用 hash 算法的加密算法。其中很多都有弱点,而且随着时间的推移,计算机越来越快,通用 hash 算法变得越来越弱。Go 已经支持好几个对称加密算法,如 Blowfish 和 DES。

这些算法都是 block 算法。因为它们必须基于数据块(block)。如果你的数据不匹配 block 的大小,那就必须在最后使用空格来填充多余的空间。

每个算法都被表示为一个 Cipher 对象。可通过在相应的包中使用对称 key 作为参数调用 NewCipher 方法来创建该对象。

公钥加密

公钥加密和解密需要两个 key:一个用来加密,另一个用来解密。加密 key 通常是公开的, 这样任何人都可以给你发送加密数据。解密 key 必须保密,否则任何人都可以解密数据。公钥系统是非对称的,不同的 key 有不同的用途。

Go 支持很多公钥加密系统,RSA 就是一个典型的例子。

X.509 证书

公钥基础架构(PKI)是一个公钥集合框架,它连同附加信息,如所有者名称和位置,以及 它们之间的联系提供了一些审批机制。

目前主要使用的 PKI 是就是基于 X.509 证书的。例如浏览器使用它验证站点的身份。

TLS

当前互联网上最流行的加密 消息传输方案是 TL(S Transport Layer Security 安全传输层),其前身为 SSL(Secure Sockets Layer 安全套接字层)。

在 TLS 中,客户端和服务器之间使用 X.509 证书进行身份验证。身份验证完成后,两者之间 会生成一个密钥,所有的加密和解密过程都使用这个密钥。虽然客户端和服务端协商的过程 相对较慢,但一旦完成就会使用一个较快的私钥机制。

远程过程调用

Socket 和 HTTP 编程 使用的是一种消息传递模式. 一个客户端发送了一个消息给服务器,通常会等回一个响应消息。两边都要创建出一种双方可理解的格式,然后从里面读 出数据的实体。

然而,大多数独立主机应用不会做太多的消息传递技术。一般来说,函数调用(或者被称作 method/procedure)的使用更为普遍。在函数风格下,程序会调用函数时会传入一系列参数, 然后函数调用完毕后会返回一系列返回值。这些返回值会成为函数的值,或者传递进函数的 是参数的地址引用,那么参数值可能最后会被修改。

远程过程调用的初衷就是把这种风格带入网络世界。客户调用时候会让这一切看起来像是函 数调用,而客户端会打包这些数据成为消息,然后传递到远端服务器。服务器再拆解包,然 后把它变成在服务器端的过程调用,而最后的返回结果会被打包传回给客户。

用图示表示的话,看起来就会是这个样子:
在这里插入图片描述
历如下几个步骤:

  1. 客户调用本地存根节点过程, 存根节点会把参数打包成网络消息,这个过程被称为编组。
  2. OS 内核里的网络通信函数会被存根节点调用来发送消息。
  3. 内核把消息传递给远端系统。这个可以使面向连接的或者是无连接传输模式。
  4. 服务器端的存根节点会把参数从网络消息中拆解出来。
  5. 服务器端的存根节点会执行一个本地过程调用。
  6. 等到过程完成,返回之行结果给服务器端的存根节点。
  7. 服务器存根节点会把返回值编组成网络消息。
  8. 消息被返回。
  9. 客户端存根节点用网络通信函数读取消息。
  10. 消息被拆解。然后返回值被放到本地程序的堆栈内。

远程过程调用有两种普遍使用的风格。第一个是以 SUN 开发的 CORBA 的 RPC/ONC 为代表。这里,服务的描述被某种像 CORBA IDL(接口定义语言)抽象语言提供,然后编译成可 执行代码分别部署在 client 端和 server 端。客户接着就可以写一个常规的程序去连接那个生 成出来的方法,而 server 端的代码实际上就是 server 服务的实体,然后连接到你实现的程序。

在第二种风格中,会用到一些特别的 client 端 API,这些 API,包括函数名,和参数是在生成的 client 代码中的。与此不同的是,在 server 端,你必须用你的手把代码敲出来,包括这些远程函数的实现。

很多 RPC 系统都采用了这种方法,比如 Web Services. 当然,Go 的 PRC 也采用了这样的方 法。

Go RPC

Go 的 RPC 是非常独特的。它与别的 RPC 系统不同,所以 Go 的 client 只能跟 Go 的 server 对话。

RPC 系统一般来说是对远程的函数调用的一些限定。这也就是为什么 RPC 系统可以恰当地 决定哪些参数要被传递,哪些引用参数来接受数据,以及如何做错误警报。

  • 函数必须是公共的(也就是首字母大写)
  • 有且仅有 2 个指针参数,第一个指向 “接收器”——接受从 client 端发过来的数据值,第二个指向 “发送器”——存放向 client 端发送的返回值。
  • 有一个eroor 类型返回值

比方说,一个合法的函数应该是如下这样的:

F(&T1, &T2) error

欢迎扫码关注V信公众号一起交流:一苦四甜

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值