系统调用

I/O

io包中接口的好处与优势

提高不同程序实体之间的互操作性。

在io包中,有这样几个拷贝数据的函数:io.Copyio.CopyBufferio.CopyN。这几个函数在功能上略有差别,但它们首先会接收两个参数:

  • 用于代表数据目的地,io.Writer类型的参数dst

  • 用于代表数据来源的,io.Reader类型的参数src

这些函数的功能大致上都是把数据从src拷贝到dst。不论给予的参数是什么类型,只要实现了这两个接口,就几乎可以正常执行。函数中还会对必要的参数值进行有效性检查,如果检查不通过,它的执行也是不能够成功结束的

// 创建字符串读取器
src := strings.NewReader(
 "CopyN copies n bytes (or until an error) from src to dst. " +
  "It returns the number of bytes copied and " +
  "the earliest error encountered while copying.")
//  创建字符串构造器
dst := new(strings.Builder)
written, err := io.CopyN(dst, src, 58)
if err != nil {
 fmt.Printf("error: %v\n", err)
} else {
 fmt.Printf("Written(%d): %q\n", written, dst.String())
}

变量src和dst的类型分别是strings.Readerstrings.Builder,当它们被传入到io.CopyN函数时,就已经被包装成了io.Readerio.Writer类型的值。

为了优化的目的,io.CopyN函数中的代码会对参数值进行在包装,也会检测这些参数值是否还实现了别的接口,甚至还会去探求某个参数值被扮装后的实际类型,是否为某个特殊的类型。从总体上来看,这些代码都是面向参数声明中的接口来做的,极大的扩展了它的适用范围和应用场景

换个角度,正因为strings.Readerstrings.Builder类型都实现了不少接口,所以它们的值才能被使用在更广阔的场景中。Go语言的各种库中,能够操作它们的函数和数据类型明显多了很多。这就是strings和bytes包中的数据类型实现了若干个接口之后的最大好处。这是面向接口编程的最大好处

io.ReaderioWriter这两个核心接口是很多接口的扩展对象和设计源泉,很多数据类型实现了io.Reader接口,是因为它们提供了从某处读取数据的功能。

不少类型的设计初衷是:实现这两个核心接口的某个,或某些扩展接口,以提供比单纯的字节序列读取或写入更加丰富的功能。

在Go语言中,对接口的扩展是通过接口类型之间的嵌入来实现的,称为接口组合。Go语言提倡使用小接口加上接口组合的方式,来扩展程序的行为以及增加程序的灵活性。

io.Reader扩展接口和实现类型及其功能

在io包中,io.Reader的扩展接口:

  1. io.ReaderWriter:既是io.Reader的扩展接口,也是io.Writer的扩展接口。该接口定义了一组行为,包含且仅包含了基本的字节序列读取方法Read和字节序列写入方法Write。

  2. io.ReaderCloser:此接口处理包含基本的字节序列读取方法之外,还有基本的关闭方法Close,一般用于关闭数据读写的通路。

  3. io.ReadWriteCloser:三个接口的组合。

  4. io.ReaderSeeker:此接口的特点是拥有一个用于寻找读写位置的基本方法Seek,该方法可以根据规定的偏移量基于数据的起始位置、末尾为止或者当前读写为止寻找到新的读写位置。新的读写为止用于表明下一次读或写的起始索引。Seek是io.Seeker接口唯一拥有的方法。

  5. io.ReadWriteSeeker:是三个接口的组合。

在io包中的io.Reader接口的实现类型:

  1. *io.LimitedReader:此类型的基本类型会包装io.Reader类型的值,并提供一个外的受限读取的功能。

    受限读取是指,此类型的读取方法Read返回的总数据量会收到限制,无论该方法被调用多少次,这个限制由该类型的字段N致命,单位为字节。

  2. *io.SelectionReader:此类型的基本类型可以包装io.ReaderAt类型的值,并且会限制它的Read方法,只能怪读取原始数据中的某一个部分(或者某一段)。

    这个数据段的起始位置和末尾位置,需要在初始化的时候指明,并且之后无法修改。该类型值的行为与切片类似,只会对外暴露其窗口中的那些数据。

  3. *io.teeReader:此类型是包级私有的数据类型,是io.TeeReader函数结果值的实际类型,这个函数接受两个参数r和w,类型分别是io.Readerio.Writer

    其结果值的Read方法会把r中的数据经过作为方法参数的字节切片p写入到w。

  4. io.multiReader:此类型是包级私有的数据类型,类似的,io包中有一个名为MutliReader的函数,它可以接受若干个io.Reader类型的参数值,并返回一个实际类型为io.mutliReader的结果值。

这个结果值的Reader方法被调用时,他会顺序地从前面那些io.Reader类型的参数值中读取数据。称之为多对象读取器。

  1. io.pipe:此类型是包级私有的数据类型,不但实现io.Reader接口,还实现io.Writer接口。

    io.PipeReaderio.PipeWriter类型拥有的所以指针方法都以它为基础,这些方法只是代理io.pipe类型值所拥有的某一个方法而已。因为io.Pipe函数返回这两个乐行的指针值并分别把它们作为其生成的同步内存管道的两端,所以,*io.pipe类型就是io包提供的同步内存管道的核心实现。

  2. io.Pipereader:被视为io.pipe类型的代理类型,它代理了后者的一部分功能,并基于后者实现了io.ReadCloser接口,同时还定义了同步内存管道的读取端。

io包是Go语言标准库中所有I/O相关API的根基,必须对其中的每一个程序实体都了解

io包中的接口以及它们之间的关系

  • 简单接口:没有嵌入其他接口并且只定义了一个方法的接口。

    在io包中,这样的接口一共有11个。分为四大类:读取(5个)、写入(4个)、关闭(1个)、读写位置设定(1个)。目前三种操作属于基本的I/O操作。

  • 核心接口:有这众多的扩展接口和实现类型。

    在io包中,核心接口只有3个:io.Reader(5个扩展接口、6个实现类型)、io.Writerio.Closer

读取

  1. io.ByteReaderio.RuneReader分别定义了读取方法:ReadByte和ReadRune。与io.Reader接口中的Reader方法不同,这两个方法只能读取下一个单一字节和Unicode字符。

    strings.Readerstrings.Buffer都是io.ByteReaderio.RuneReader的实现类型。

    这两个类型还实现了io.ByteScanner接口和io.RuneScanner接口。

    • io.ByteScanner接口内嵌了简单接口io.ByteReader,并定义了额外的UnreadByte方法,它抽象出了可以读取和读回退单个字节的功能集。

    • io.RuneScanner接口内嵌了简单接口io.RuneReader,并定义了额外的UnreadRune方法,它抽象出了可以读取和读回退单个Unicode字符的功能集。

  2. io.ReaderAt接口,其中只定义了一个方法ReadAt,与前面说过的读取方法都不同,ReadAt是一个纯粹的只读方法。只去读取其所属值总包含的字节,而不对这个值进行任何的改动,比如它绝对不能去修改已读计数的值,这是io.ReaderAt接口与其他实现类型之间最重要的一个约定。如果仅仅并发地调用某一个值的ReadAt方法,那么安全性应该是可以得到保障的。

  3. io.WriteTo接口,其中定义了一个WriteTo方法,这是一个读取方法,它会接受一个io.Writer类型的参数值,并会把其所属值中的数据读出并写入到这个参数中。

在io包中,与写入操作有关的接口都与读取操作的相关接口有着一定的对应关系

写入

  1. io.ReaderFrom接口,其中定义了ReadFrom方法,这是一个写入方法,该方法会接受一个io.Reader类型的参数值,并会从该类型值中读出数据,并写入到其所属值中。

  2. io.Writer核心接口,基于它扩展接口除了io.ReadWriterio.ReadWriteCloserio.ReadWriteSeekerio.WriteCloserio.WriteSeeker

读写位置

io.Seeker接口作为读写位置设定相关的接口,定义了一个方法Seek。

数据类型

*os.File,这个类型不但是io.WriterAt接口的实现类型,还实现了io.ByteWritCloserio.ReadWriteSeeker,该类型支持的I/O操作非常丰富。

总结

io包中的接口体系

bufio

bufio是buffered I/O的缩写,这个代码包中的程序实体实现的I/O操作都内置了缓冲区。bufio包中的数据类型主要有:

  • Reader

  • Scanner

  • Writer

  • ReadWriter

与io包中的数据类型类似,这些类型的值也都需要在初始化的时候,包装一个或多个简单I/O接口类型的值。(简单接口类型值的就是io包中的那些简单接口。)

bufio.Reader类型值中的缓冲区的作用

bufio.Reader类型值内的缓冲区,是一个数据存储中间,它介于底层读取器(初始化此类值的时候传入io.Reader类型的参数值)与读取方法及其调用方之间。

bufio.Reader值的读取方法一般都会先从其所属值的缓冲区中读取数据,必要的时候,它们还会预先从底层读取器那里读取一部分数据,并暂存于缓冲区中以备后用。

有这样一个缓冲区的好处是,可以在大多数时候降低读取方法的执行时间,虽然读取方法有时还要负责填充缓冲区,但从总体来看,读取方法平均执行时间一般会因此有大幅的缩短。

bufio.Reader类型并不是开箱即用的,它包含一些需要显式初始化的字段,如下:

// Reader为io.Reader对象实现缓冲。
type Reader struct {
    // 字节切片,代表缓冲区,
    // 虽然这是切片类型,但是它的长度却是在初始化的时候指定,并且在之后保持不变
    buf          []byte

    rd           io.Reader //客户端提供的reader,代表底层读取器,缓冲区中的数据就是从这里拷贝来的

    r, w         int       // buf 读写位置
    // r 代表对缓冲区进行下一次读取时的开始索引,称为已读计数
    // w 代表对缓冲区进行下一次写入时的开始索引,称为已写计数

    // 它的值用于表示在从底层读取器获得数据时发生的错误
    // 这里值在被读取或忽略之后,该字段会被设置为nil
    err          error  

    // UnreadByte读取的最后一个字节; -1表示无效
    // 用于记录缓冲区中最后一个被读取的字节,读回退时会用到它的值
    lastByte     int

    // UnreadRune读取的最后一个rune的大小; -1表示无效
    // 用于记录缓冲区中最后一个被读取的Unicode字符所占用的字节数,
    // 读回退的时候会用到它的值,这个字段只会在其所属值的ReadRune方法中才会被赋予有意义的值
    // 其他情况下,它都被置为-1
    lastRuneSize int
}

bufio包提供了两个用于初始化Reader值的函数:

  • NewReader:初始化的Reader值会拥有一个默认大小(4096字节,即4KB)的缓冲区,

  • NewReaderSize:将缓冲区的大小的决定权交给使用方

它们都会返回一个*bufio.Reader类型的值。这里的缓冲区在一个Reader值的生命周期内大小是不变的,所以在有些时候需要做一些权衡。

  • 读取Peek和ReadSlice方法,都会调用该类型的一个名为fill的包级私有方法,fill方法的作用是填充内部缓冲区。

  • fill方法,首先检查其所属值的已读计数,如果这个计数不大于0,那么有两种可能:

    1. 缓冲区中的字节都是全新的,它们没有被读取过

    2. 缓冲区刚被压缩过,对缓冲区的压缩操作:

      1. 把缓冲区中在[已读计数,已写计数]范围之内的所有字节都一次拷贝到缓冲区的头部,这一步不会有副作用,因为:

        1. 已读计数之前的字节都已经被读取过,肯定不会再被读取,因此把它们覆盖掉是安全的

        2. 在压缩缓冲区之后,已写计数之后的字节只可能是已经被读取过的字节,或者是已被拷贝到缓冲区头部的未读字节,或者是代表未曾被填入数据的零值(0x00),所以后续的新字节可以被卸载这些位置上。

      2. fill方法会把已写计数的新值设定为原已写计数与已读计数只差,这个差锁代表的索引,就是压缩后第一次写入字节时的开始索引。

缓冲区的压缩过程,如下图所示:

实际上,fill方法只要在开始时发现其所属值的已读计数大于0,就会对缓冲区进行一次压缩,之后,如果缓冲区中还有可写的位置,那么该方法就会对其进行填充。

在填充缓冲区的时候,fill方法会试图从底层读取器哪里,读取足够多的字节,并尽量把从已写计数代表的索引位置到缓冲区末尾之间的空间都填满。

在这个过程中fill方法会及时更新已写计数,以保证填充的正确性和顺序性,它还会判断从底层读取器读取数据的时候,是否有错误发生,如果有,那么它就会把错误值赋予给其所属值的err字段,并终止填充流程。

bufio.Writer类型值中缓冲的数据何时写入底层写入器

// Writer为io.Writer对象实现缓冲。
// 如果在写入Writer时发生错误,将不再接受任何数据,
// 并且所有后续写入和Flush都将返回错误。
// 写入所有数据之后,客户端应调用Flush方法以确保所有数据都已转发到底层io.Writer。
type Writer struct {
    err error   // 它的值用于表示在向底层写入器写数据时发生的错误
    buf []byte  // 代表缓冲区,在初始化之后,它的长度会保持不变
    n   int // 代表对缓冲区进行下一次写入时的开始索引,称为写入计数
    wr  io.Writer   // 代表底层写入器
}

bufio.Writer类型有一个名为Flush的方法,它的主要功能是把相应缓冲区中暂存的所以数据,都写到底层写入器中,数据一旦被写入底层写入器,该方法就会把它们从缓冲区中删除掉。

这里的删除有时候只是逻辑删除。不论是否成功写入了所有暂存数据,Flush方法都会妥当处置,并保证不会出现重写或者漏写的情况。

bufio.Writer类型拥有的所以数据写入方法都会在必要的时候调用它的Flush方法。

  • Write方法有时候会在把数据写进缓冲区之后,调用Flush方法,以便为后续的新数据腾出空间,如果Write方法发现要写入的字节太多,同时缓冲区已空,那么会直接跨过缓冲区,直接把新的数据写到底层写入器中。

  • WriteByte方法和WriteRune方法都会在发现缓冲区中的可写空间不足以容纳新的字节或Unicode字符的时候,调用Flush方法

  • ReadFrom方法,会在发现底层写入器的类型是io.ReaderFrom接口的实现之后,直接调用其ReadFrom方法把参数值持有的数据写进去。

只要缓冲区中的可写空间无法容纳需要写入的新数据,Flush方法就一定会被调用bufio.Writer类型的一些方法有时候还会试图走捷径,跨过缓冲区而直接对接数据供需方。

bufio.Reader类型的读取方法

bufio.Reader类型拥有很多用于读取数据的指针方法,这里有四个方法可以作为不同读取流程的代表:

func (b *Reader) Peek(n int) ([]byte, error)
  • Peek:读取并返回其缓冲区中n个未读字节,并且它会从已读计数代表的索引位置开始读。

    1. 在缓冲区未被填满,并且其中的未读字节的数量小于n的时候,该方法会调用fill方法,以启动缓冲区填充流程,如果发现上次填充缓冲区时有错误,则不再填充。

    2. 如果调用方给定的n比缓冲区的长度还大,或者缓冲区中未读字节的数量小于n,那么:

      1. 所有未读字节组成的序列作为第一个结果

      2. bufio.ErrBufferFull变量的值作为第二个结果,用来表示虽然缓冲区被压缩和填满了,但是仍然不满足要求

    3. 上述情况都未出现,则返回已读计数为起始的n个字节表示未发生任何错误的nil

Peek方法的一个特点,即使它读取了缓冲区中的数据,也不会改变已读计数的值。其他的读取方法不是这样的。

func (b *Reader) Read(p []byte) (n int, err error)
  • Read:把缓冲区中的未读字节,依次拷贝到其参数p代表的字节切片中,并立即根据实际拷贝的字节数增加已读计数的值。

    • 在缓冲区中还有未读字节的情况下,Read方法是这样做的。(当已读计数等于已写计数时,表示此时的缓冲区中没有任何未读的字节)

    • 当缓冲区中无未读字节时,Read方法会先检查参数p的长度是否大于或等于缓冲区的长度。

      • 如果是,Read方法放弃缓冲区中的填充数据,直接从底层读取器中读出数据并拷贝到p中,这意味着它完全跨过了缓冲区,并直连了数据供需的双方。

      • 如果否,会先把已读计数和已写计数都重置为0,然后再尝试(只进行一次)使用从底层读取器那里回去的数据,对缓冲区进行一次从头到尾的填充。

func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
  • ReadSlice:持续地读取数据,直到遇到调用方给定的分隔符为止。

    先在缓冲区的未读部分中寻找分隔符,如果未找到,并且缓冲区未满,那么调动fill方法对缓冲区进行填充,然后再次寻找,如此往复。

    • 如果在填充的过程中遇到错误,会把未读部分作为结果返回,并返回相应的错误值。

    • 如果缓冲区被填满,仍然没有找到分隔符,那么整个缓冲区作为第一个结果,bufio.ErrBufferFull(缓冲区已满的错误)作为第二个结果

func (b *Reader) ReadBytes(delim byte) ([]byte, error)
  • ReadBytes:持续地读取数据,直到遇到调用方给定的分隔符为止。

    • ReadBytes方法依赖ReadSlice方法。

    • ReadLine方法依赖ReadSlice方法。

    • ReadString方法完全依赖ReadBytes方法,只是在返回的结果之上做简单的类型转换。

Peek、ReadSlice、ReadLine方法都可能会造成内容泄露,在正常情况下,它们都会直接返回基于缓冲区的字节切片,调用方可以通过这些方法的结果值访问到缓冲区的其他部分,甚至修改缓冲区中的内容,这是非常危险的。

OS

os包提供操控计算机操作系统的能力,都是与平台不相关的API。

平台不相关API:这些API基于(或者说抽象自)操作系统,为我们使用操作系统的功能提供高层次的支持,但是,它们并不依赖于具体的操作系统。

不论是什么操作系统,os包都提供统一的使用接口,使得我们可以用同样的方式,来操纵不同的操作系统,并得到相似的结果。其中的API帮助使用操作系统的:

  • 文件系统:操作文件系统的API最丰富

    os.File数据类型,代表了操作系统中的文件(对于Unix操作系统,万物皆文件),除了常见的文本文件、二进制文件、压缩文件、目录等,还有符号链接、各种物理设备(内置或外界的面向块或者字符的设备)、命名管道、套接字(socket)等。

  • 权限系统

  • 环境变量

  • 系统进程

  • 系统信号

os.File类型实现了哪些io包的接口

os.File类型拥有的都是指针方法,所以除了空接口,它本身没有任何接口,它的指针类型实现了很多io包中的接口。

*os.File类型实现了io.Readerio.Writerio.Closerio.ReadAtio.Seekerio.WriterAt。没有实现io.ByteReaderio.RuneReader

os.File类型以何种方式操作文件

获取os.File类型的指针值的方法,这些方法都执行同一个系统调用,并且在成功之后得到一个文件描述符,这个文件描述符会被存储在它们返回的File值中:

func Create(name string) (*File, error)
  • Create:根据给定的路径创建一个新的文件,返回一个File值和一个错误值(可能为非nil的错误值),对该函数返回的File值对应的文件进行读写操作。

    • 该函数创建的文件,对操作系统的所有用户都是可读写的

    • 如果函数的路径上已经存在了一个文件,那么会清空文件中全部的内容,然后把它作为第一个结果值返回

func NewFile(fd uintptr, name string) *File
  • NewFile:在被调用时,需要接受一个代表文件描述符的uintptr类型的值,以及一个用于表示文件名的字符串值

    • 如果给定的文件描述符并不有效,那么函数会返回nil

    • 否则返回一个代表文件的File值

      这个函数的功能并不是创建一个新的文件,而是依据已存在的文件的描述符,来创建一个包装了该文件的File值。如下所示,获取一个包装了标准错误输出的File值,然后通过这个File值向标准错误输出中写入一些内容:

      file3 := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
      if file3 != nil {
          defer file3.Close()
          file3.WriteString("The Go language program writes the contents into stderr.\n")
      }
func Open(name string) (*File, error)
  • Open:打开一个文件,并返回包装了该文件的File值。该函数只能以只读模式打开文件,也就是只能读取文件内容,不能写入内容。如果调用这个File值的任何写入方法,都会得到“坏的文件描述符”的错误值。这个只读模式应用与File值所持有的文件描述符上。

    文件描述符,是又通常很小的非负整数代表的,它一般会由I/O相关的系统调用返回,并作为某个文件的一个标识存在。

    从操作系统层面看,针对任何文件的I/O操作都需要用到这个文件描述符。只不过Golang中的一些数据类型,我们隐藏掉了这个描述符,如此一来,就不需要时刻关注和辨别它,如os.File类型。

    os.File类型有一个指针方法,叫Fd,它在被调用之后将会返回一个uintptr类型的值,这个值代表了当前File值所持有的那个文件描述符。在os包中,只有NewFile函数需要用到它,它也没有别的用武之地,如果只是操作常规文件和目录,无需特别注意它。

func OpenFile(name string, flag int, perm FileMode) (*File, error)
  • OpenFile:这个函数是os.Createos.Open函数的底层支持,它最为灵活。

    这个函数的三个参数:

    • name:表示文件的路径

    • flag:需要施加在文件描述符之上的模式(如,只读模式os.O_RDONLY,它是int类型的),称为操作模式,限定操作文件的方式

    • perm:也是模式,它的类型是os.FileMode(此类型是一个基于unit32类型的再定义类型),称为权限模式,控制文件的访问权限

通过os.File类型的值,我们不但可以对文件进行读取、写入、关闭等操作,还可以设定下一次读取或写入是的起始索引位置。os包中:

  • 用于常见权限文件的Create函数

  • 用户包装现存文件的NewFile函数

  • 用以打开已存在的文件的Open函数和OpenFile函数

File的操作模式

针对File值的操作模式主要有:

  • 只读模式:os.O_RDONLY

  • 只写模式:os.O_WRONLY

  • 读写模式:os.O_RDWR

在我们新建或者打开一个文件的时候,必须把这三种模式中的一个设定为此文件的操作模式。

还可以为文件设置额外的操作模式:

  • os.O_APPEND:当向文件中写入内容时,把新内容追加到现有内容的后边

  • os.O_CREATE:当给定路径上的文件不存在时,创建一个新的文件

  • os.O_EXCL:需要与os.O_CREATE一同使用,表示在给定路径上不能有已存在的文件

  • os.O_SYNC:在打开的文件之上实施同步I/O,它会保证读写的内容总会与硬盘上的数据保持同步

  • os.O_TRUNC:如果文件已存在,并且是常规文件,那么就先情况其中已经存在的任何内容

例子

// Create函数
func Create(name string) (*File, error) {
 return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
//  给予的操作模式是 O_RDWR|O_CREATE|O_TRUNC的组合
// 如果参数name代表的路径之上的文件不存在那么就会新建一个
// 否则先情况现存文件中的全部内容
//  返回的File值的读方法和写方法都能可用
}


// Open函数
// 以只读模式打开已存在的文件
func open(name string)(*File, error){
    return OpenFile(name, O_REONLY, 0)
}

注意,多个操作模式通过桉位或操作符组合起来

File的权限模式

os.FileMode类型不但可以代表权限模式,还能代表文件模式(即,文件种类)。os.FileMode是基于uint32类型的在定义类型,所以它的每个值都包含了32个比特位,每个比特位都有特定的含义。

在一个os.FileMode类型值中,只有最低的9个比特位才用于表示文件权限(3个一组,共分为3组),当我们拿到一个此类型的值时,可以把它和os.ModePerm常量的值做按位与操作。这样就可以得到FileMode中所有用于表示文件权限的比特位,即权限模式。

例如,常量0777,是一个八进制无符号整数,它的最低9个比特位都是1,更高的23个比特位都是0.

  • 最高比特位为1,该值的文件模式等同于os.ModeDir,代表一个目录

  • 第26个比特位为1,告知的文件模式等同于os.ModeNamedPipe,代表一个命名管道

从高到底,这3组分别:

  • 表示文件所有者(创建文件的用户)

  • 文件所有者所属的用户组

  • 其他用户对该文件的访问权限

每组中的3个比特位从高到底分别表示:

  • 读权限

  • 写权限

  • 执行权限

如果在其中的某个比特位上的是1,就意味着相应的权限开启,否则,就标书相应的权限关闭。

在调用os.OpenFile函数的时候,可以根据上面的说明设置第三个参数,但是需要注意,只有在新建文件的时候,这里的第三个参数才是有效的,在其他情况下,即使设置了此参数,也不会对目标文件产生任何的影响。

net

socket与IPC

socket是一种IPC方法(Inter-Process Communication,进程间通信),IPC主要定义的是多个进程之间,相互通信的方法,这些方法主要包括:

  • 系统信号(signal)

  • 管道(pipe)

  • 套接字(socket)

  • 文件锁(file lock)

  • 消息队列(message queue)

  • 信号灯(semaphore,或称为信号量)等

主流操作系统大都对IPC提供了强力的支持,尤其是socket。

Golang对IPC也提供了一定的支持,如:

  • os包和os/signal包中针对系统信号的API

  • os.Pipe函数可以创建命名管道

  • os/exec包对匿名管道提供支持

  • net包对socket提供支持

在众多的IPC方法中,socket是最为通用和灵活的一种,与其他的IPC方法不同,利用socket进行通信的进程,可以不局限于同一台计算机当中。通信双方只要能够通过计算机的网卡端口以及网络进行通信,就可以使用socket。

支持socket的操作系统一般都会对外提供一套API。跑在它们之上的应用程序利用这套API,就可以与互联网上的另一台计算机中的程序、同一台计算机中的其他程序,甚至同一个程序中的其他线程进行通信

Linux操作系统中,用于创建socket实例的API,就是一个名为socket的系统调用,这个系统调用是Linux内核的一部分。

所谓系统调用,可以理解为特殊的C语言函数,它们是连接应用程序和操作系统内核的桥梁,也是应用程序使用操作系统功能的唯一渠道

  • syscall包中有一个与socket系统调用相对应的函数,这两者的函数签名基本一致,都会接收三个int类型的参数,并会返回一个可以代表文件描述符的结果。

      func Socket(domain, typ, proto int) (fd int, err error)

syscall包中的Socket函数本身是与平台不相关的,在其底层,Go语言为它支持的每个操作系统都做了适配,这才使得这个函数无论在哪个平台上,总是有效的。

  • net包中的很多程序实体都会直接或间接地使用syscall.Socket函数

  • 调用net.Dial函数的时候会为它的两个参数设定值,第一个参数名为network,它决定了Go程序底层会创建什么样的socket实例,并使用什么样的协议与其他程序通信

net.Dial函数的一个参数network的可选值

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

net.Dial函数接受两个参数,network和address,都是string类型的。

network参数常用的可选值有9个,这些值分别代表了socket实例可使用的不同通信协议:

  • tcp:代表TCP协议,其基于的IP协议的版本根据参数address的值自适应

  • tcp4:代表基于IP协议第四版的TCP协议

  • tcp6:代表基于IP协议第六版的TCP协议

  • udp:代表UDP协议,其基于的IP协议的版本根据参数address的值自适应

  • udp4:代表基于IP协议第四版的UDP协议

  • udp6:代表基于IP协议第六版的UDP协议

  • unix:代表Unix通信域下的一种内部socket协议,以SOCK_STREAM为socket类型

  • unixgram:代表Unix通信域下的一种内部socket协议,以SOCK_DGRAM为socket类型

  • unixpacket:代表Unix通信域下的一种内部socket协议,以SOCK_SEQPACKET为socket类型

syscall.Socket函数接受的三个参数

func Socket(domain, typ, proto int) (fd int, err error)

这三个参数都是int类型,这些参数代表分别是:

domain:socket通信域,主要有:

  • IPv4域:基于IP协议第四版的网络(syscall中的常量AF_INET表示)

  • IPv6域:基于IP协议第四版的网络(syscall中的常量AF_INET6表示)

  • Unix域:一种类Unix操作系统中特有的通信域,装有此类操作系统的同一台计算机中,应用程序可以基于此域创建socket连接(syscall中的常量AF_UNIX表示)

typ:类型,共有四种:

  • SOCK_DGRAM:代表datagram即数据报文,一种有消息边界,但没有逻辑连接非可靠socket类型,基于UDP协议的网络通信属于此类。

    有消息边界指的是,与socket相关的操作系统内核中的程序在发送或接收数据的时候是以消息为单位的。把消息理解为带有固定边界的一段数据,内核程序自动识别和维护这种边界,在必要的时候,把数据切割成一个个消息,或者把多个消息串接成连续的数据,这样应用程序只需要面向消息进处理就可以了。 有逻辑连接指的是,通信双发在收发数据之前必须先建立网络连接,待连接建立好之后,双方就可以一对一地进行数据传输,基于UDP协议的网络通信是没有逻辑连接的。只要应用程序指定好对方的网络地址,内核程序就可以立即把数据报文发送出去。 优势:发送速度快,不长期占用网络资源,并且每次发送都可以指定不同的网络地址。 劣势:每次都需要指定网络地址使得数据报文更长,无法保证传输的可靠性,不能实现数据的有序性,数据只能单向进行传输。

  • SOCK_STREAM:与SOCK_DGRAM相反,它是没有消息边界,但有逻辑连接,能够保证传输的可靠性和数据的有序性,同时还可以实现数据的双向传输。基于TCP协议的网络通信属于此类。

    这样的网络通信传输数据的形式是字节流(字节流是以字节为单位的),而不是数据报文。内核程序无法感知一段字节流中包含了多少个消息,以及这些消息是否完整,这完全需要应用程序自己把控。 此类网络通信中的一端,总是会忠实地按照另一端发死你个数据时的字节排序,接收和缓存它们,所以应用程序需要更具双方的约定去数据中查找消息边界,并按照边界切割数据。

  • SOCK_SEQPACKET

  • SOCK_RAW

syscall包中都有同名常量与之对应。

proto:协议;表示socket实例所使用的协议,通常明确了前两个参数,就不在需要确定第三个参数值了,一般设置为0即可,内核程序会自动选择最适合的协议。

  • 当两个参数分别为syscall.AF_INETsyscall.SOCK_DGRAM的时候,内核程序会选择UDP作为协议

  • 当两个参数分别为syscall.AF_INET6syscall.SOCK_STREAM的时候,内核程序会选择TCP作为协议

在使用net包中的高层次API的时候,前两个参数(domain和typ)也不需要给定,只需要把前面罗列的9个可选值字符串字面量的其中一个,作为network参数的值就好了。

调用net.DialTimeout函数时设定超时时间

func DialTimeout(network, address string, timeout time.Duration) (Conn, error)

超时时间代表这函数为网络连接建立完成而等待的最长时间,这是一个相对时间,由函数的参数timeout的值表示。

开始的时间点几乎是调用net.DialTimeout函数的那一刻,之后的时间,主要会花费在:

  • 解析参数network和address的值:在这个过程中,函数会确定网络服务的IP地址、端口号等必要信息、并在需要时访问DNS服务。

如果解析出的IP地址有多个,那么函数会串行或并发地尝试建立连接,无论以什么方式尝试,函数总会以最先建立成功的那个连接为准。会根据超时前剩余的时间,去设定每次连接尝试的超时时间,以便让它们都有适当的时间执行

  • 创建socket实例并建立网络连接。

不论执行到哪一步,只要绝对的超时时间到达的那一刻,网络连接还没有建立完成,该函数就会返回一个代表I/O操作超时的错误值。

net包中有一个名为Dialer的结构体类型,该类型有一个Timeout字段,与上述timeout参数的含义完全一致,实际上,net.DialTimeout函数正是利用了这个类型的值才得以实现功能的。

net/http

使用net.Dialnet.DialTimeout函数访问基于HTTP协议的网络服务是完全没有问题的,HTTP协议是基于TCP/IP协议栈,并且是一个面向普通文本的协议。如果需要方便的访问基于HTTP协议的网络服务,则使用net/http包。其中最便捷的是使用http.Get函数,在调用它的时候只需要传入一个URL就可以,如下所示:

url1 := "http://google.cn"
fmt.Printf("Send request to %q with method GET ...\n", url1)
resp1, err := http.Get(url1)
if err != nil {
    fmt.Printf("request sending error: %v\n", err)
}
defer resp1.Body.Close()
line1 := resp1.Proto + " " + resp1.Status
fmt.Printf("The first line of response:\n%s\n", line1)
func (c *Client) Get(url string) (resp *Response, err error)

http.Get函数返回两个结果值:

  • 第一个结果值的类型是*http.Response,它是网络服务给我们传回的函数内容的结构化表示

  • 第二个结果值是error类型,它代表了在创建和发送HTTP请求,以及接收和解析HTTP相应的过程中可能发生的错误

http.Get函数会在内部使用缺省的HTTP客户端,并且调用它的Get方法以完成功能,这个缺省的HTTP客户端由net/http包中的公开变量DefaultClient代表,其类型是*http.Client,它的基本类型也是可以被拿来使用的,甚至它是开箱即用的,如下代码所示:

var httpClient1 http.Client
resp2, err := httpClient1.Get(url1)

// 等价于
resp1, err := http.Get(url1)

http.Client是一个结构体类型,并且它包含的字段都是公开的。之所以该类型的零值仍然可用,是因为它的这些字段要么存在着相应的缺省值,要么其零值直接可以使用,且代表着特定的含义。

http.Client类型中Transport字段的含义

http.Client类型中的Transport字段代表着:向网络服务发送HTTP请求,并从网络服务接收HTTP相应的操作过程。也就是说,该字段的方法RoundTrip应该实现单次HTTP事务(或者说基于HTTP协议的单次交互)需要的所有步骤。

这个字段是http.RoundTripper接口类型的,它有一个由http.DefaultTransport变量代表的缺省值,当我们在初始化一个http.Client类型的值的时候,如果显式地为该字段赋值,那么这个Client值就会直接使用http.DefaultTransport

DefaultTransport

DefaultTransport的实际类型是*http.Transport,后者即为http.RoundTripper接口的默认实现,这个类型是可以被复用的,它是并发安全的,因此http.Client类型也拥有同样的特质。

http.Transport类型会在内部使用一个net.Dialer类型的值,并且会把该值的Timeout字段的值,设定为30秒。如果30秒之内没有建立好连接就会判断为操作超时。在DefaultTransport的值被初始化的时候,这样的net.Dialer值的DialContext方法会被赋给前者的DialContext字段。

http.Transport类型还包含很多其他的字段,其中有一些字段是关于操作超时的:

  • IdleConnTimeout:空闲的连接在多久之后就应该被关闭。

    DefaultTransport会把该字段的值设置为90秒,如果该值设置为0 ,那么就表示不关闭空闲的连接。这样会造成资源的泄露

    与该字段相关的一些字段:

    • MaxIdleConns:无论当前的http.Transport类型的值访问了多少个网络服务,这个字段都只会对空闲连接的总数做出限定。

    • MaxIdleConnsPerHost:这个字段限定的是http.Transport值访问的每一个网络服务的最大空闲连接数。

      每个网络服务都会有自己的网络地址,可能会使用不同的网络协议,对一些HTTP请求也可能会使用代理,`http.Transport`值就是通过这三方面的具体情况,来鉴别不同的网络服务的。MaxIdleConnsPerHost字段的缺省值,由`http.DefaultMaxIdleConnsPerHost`变量代表,值为2。即,在默认情况下,对某个`http.Transport`值访问的每一个网络服务,它的空闲连接数最多只能有两个。
    • MaxConnsPerHost:针对某个http.Transport值访问的每一个网络服务的最大连接数,不论这些连接是否空闲的,该字段没有相应的缺省值,它的零值表示不对此设限制。

  • ResponseHeaderTimeout:从客户端把请求完全递交给操作系统到从操作系统那里接收到响应报文头的最大时长。

    DefaultTransport没有设定该字段的值。

  • ExpectContinueTimeout:在客户端递交了请求报文头之后,等待接收第一个响应报文头的最长时间。

    在客户端想要使用HTTP的POST方法把一个很大的报文体发送给服务端的时候,它可以先通过发送一个包含了Expect:100-continue的请求报文头,来询问服务端是否愿意接收这个大报文体。

    这个字段是用于设定在这种情况下的超时时间的,注意,如果该字段的值不大于0 ,那么无论多大的请求报文都将会立即发送出去,这可能会造成网络资源的浪费。DefaultTransport把该字段的值设定为1秒。

  • TLSHandshakeTimeout:(TLS是Transport Layer Security的缩写,翻译为传输层安全),这个字段代表了基于TLS协议的连接在被建立时的握手阶段的超时时间。若该值为0 ,则表示对这个时间不设限。

    DefaultTransport把该字段的值设定为10秒。

产生空闲连接的原因:HTTP协议有一个请求报文头叫做“Connection”,在HTTP 1.1 中,这个报文头的值默认是“keep-alive”,在这种情况下的网络连接都是持久连接,它们会在当前的HTTP事物完成后仍然保持着连通性,因此是可以被复用的,那就有两种可能:

  1. 针对同一个网络服务,有新的HTTP请求被递交,该连接被再次使用

  2. 不再有针对该网络服务的HTTP请求,该连接被闲置(这就会产生空闲的连接)

如果分配给某个网络服务的连接过多的话,也可能会导致空闲连接的产生,因为每一个新递交的HTTP请求,都只会征用一个空闲的连接,所以为空闲连接设定限制,在大多数情况下是很有必要的。

如果要杜绝空连接产生,可以在初始化http.Transport值的时候把它的DisableKeepAlives字段的值设置为true,这时HTTP请求的“Connection”报文头的值就会被设置为“close”,这会告诉网络服务,这个网络连接不必保持,当前的HTTP事物完成后就可以断开它了。这样每一个HTTP请求被递交时,就会产生一个新的网络连接,明显加重网络服务以及客户端的负载,并会让每个HTTP事物都消耗更多的时间。一般情况下不会设置DisableKeepAlives。

net.Dialer类型中也有一个keepAlive字段,它是直接作用在底层的socket上的,一种针对网络连接(TCP连接)的存活探测机制。它的值用于表示每隔多长时间发送一次探测包,当该值不大于0是,则表示不开启这个机制。DefaultTransport会把这个字段的值设定为30秒。

http.Server类型的ListenAndServer方法

http.Server代表的是基于HTTP协议的网络服务,它的ListenAndServer方法的功能是:监听一个基于TCP协议的网络地址,并对接收到的HTTP请求进行处理。这个方法会默认开启针对网络连接的存活探测机制,以保证连接是持久的。该方法会一直执行,直到有严重的错误发送或者被外界关闭。当被外界关闭时,会返回一个由http.ErrServerClosed变量代表的错误值。

ListenAndServer方法主要做下面几件事情:

  1. 检查当前的http.Server类型的值的Addr字段,该字段的值代表了当前的网络服务需要使用的网络地址,即IP地址和端口号。

    如果该字段的值为空字符串,那么就用“:http代替,也就是使用任何可以代表本机的域名和IP地址,并且端口号为80。

  2. 通过调用net.Listen函数在已确定的网络地址上启动基于TCP协议的监听。

  3. 检查net.Listen函数返回的错误值,如果该错误值不是nil,那么直接返回该值,否则通过调用当前值的Serve方法准备接收和处理将要到来的HTTP请求。

    1. net.Listen函数完成如下操作:

      1. 解析参数值中包含的网络地址隐含的IP地址和端口号

      2. 根据给定的网络协议,确定监听的方法,并开始进行监听

最后更新于

这有帮助吗?