Go学习笔记—基于Go的进程间通信

IPC(Inter-Process Communication 进程间通信)

一般方法:(1) 半双工Unix管道 (2) FIFOs(命名管道) (3) 消息队列 (4) 信号量 (5) 共享内存 (6) 网络Socket (7) RPC(远程过程调用)

(一)管道(Pipe)
1.未命名管道(ps aux | grep java)
cmd1 := exec.Command("ps", "aux")
cmd2 := exec.Command("grep", "apipe")

//cmd对象的底层,Stdout与Stdin属性也是通过指向一个字节流实现读写的,这里用新建的字节流代替
var outputBuf1 bytes.Buffer
cmd1.Stdout = &outputBuf1
if err := cmd1.Start(); err != nil {
    fmt.Printf("Error: The first command can not be startup %s\n", err)
    return
}
if err := cmd1.Wait(); err != nil {		//wait会阻塞cmd直到其运行完毕
    fmt.Printf("Error: Couldn't wait for the first command: %s\n", err)
    return
}
//cmd1的输出与cmd2的输入指向同一个字节流地址
cmd2.Stdin = &outputBuf1
var outputBuf2 bytes.Buffer
cmd2.Stdout = &outputBuf2
if err := cmd2.Start(); err != nil {
    fmt.Printf("Error: The second command can not be startup: %s\n", err)
    return
}
if err := cmd2.Wait(); err != nil {
    fmt.Printf("Error: Couldn't wait for the second command: %s\n", err)
    return
}
2.命名管道(mkfifo -m 777 myfifo cat src.log > myfifo)
操作 作用 特性
reader, writer, err := os.Pipe() 创建独立管道 可以被多路复用,不提供原子操作支持
reader, writer, err := os.Pipe()
if err != nil {
    fmt.Printf("Error: Couldn't create the named pipe: %s\n", err)
}
//Read与Write会在另一端还未就绪时对进程进行阻塞,所以二者需要并发运行
go func() {
    output := make([]byte, 100)
    n, err := reader.Read(output)
    if err != nil {
        fmt.Printf("Error: Couldn't read data from the named pipe: %s\n", err)
    }
    fmt.Printf("Read %d byte(s). [file-based pipe]\n", n)
}()
input := make([]byte, 26)
for i := 65; i <= 90; i++ {
    input[i-65] = byte(i)
}
n, err := writer.Write(input)
if err != nil {
    fmt.Printf("Error: Couldn't write data to the named pipe: %s\n", err)
}
fmt.Printf("Written %d byte(s). [file-based pipe]\n", n)
操作 作用 特性
reader, writer := io.Pipe() 创建内存独立管道 基于内存的提供原子操作保证的管道
reader, writer := io.Pipe()
go func() {
    output := make([]byte, 100)
    n, err := reader.Read(output)
    if err != nil {
        fmt.Printf("Error: Couldn't read data from the named pipe: %s\n", err)
    }
    fmt.Printf("Read %d byte(s). [in-memory pipe]\n", n)
}()
input := make([]byte, 26)
for i := 65; i <= 90; i++ {
    input[i-65] = byte(i)
}
n, err := writer.Write(input)
if err != nil {
    fmt.Printf("Error: Couldn't write data to the named pipe: %s\n", err)
}
fmt.Printf("Written %d byte(s). [in-memory pipe]\n", n)
time.Sleep(200 * time.Millisecond)
(二)信号(Signal)

​ IPC中唯一一种异步通信方法,本质是利用软件来模拟硬件的中断机制,信号用来通知某个进程有某个事件发生了。

​ Linux中一共有62种信号,1到31属于标准信号,标准信号只会被处理并记录一次,并且一次发送的多个标准信号,其处理顺序是不确定的;34到64属于实时信号,多个同种类的实时信号都可以记录在案,并且可以按照信号的发送顺序被处理。(Ctrl—C == SIGINT)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z3UGR24Z-1638325000547)(…/…/Pic/image-20211124160241715.png)]

type Signal interface{
    String() string
    Signal()
}
方法 作用
func Notify(c chan<- os.Signal, sig …os.Signal) 当操作系统向当前进程发送指定信号时发出通知
func Stop(c chan<- os.Signal) 删除定义的自处理通知通道,恢复系统默认操作
func FindProcess(pid int) (*Process, error) 根据Pid返回一个进程对象,可以向对象发送Signal信息
syscall.SIGINT… syscall中定义了所有信号常量

os.Signal通道内部维护了一个包级私有字典,用于存放以Signal接收通道为键并以信号集合为元素的键—元素对。调用Notify函数时,若键—元素对不存在,则会向字典中添加一个,否则就更新其元素集合(仅能扩大);调用Stop函数时,将删除该键—元素对。

/**
	1. Notify不会因为sigRecv1满而被阻塞
	2. 接收到信号而不做合适的处理,相当于程序忽略掉了系统发来的信号
    3. SIGKILL和SIGSTOP永远不会被自处理或者忽略(因为他们是提供给超级用户终止或停止进程的可靠方法)即使Notify(,syscall.SIGKILL, syscall.SIGSTOP)也不会改变系统的处理动作
*/
//同一个进程可以建立多个自定义信号处理通道
sigRecv1 := make(chan os.Signal, 1)		
sigs1 := []os.Signal{syscall.SIGINT, syscall.SIGQUIT}
fmt.Printf("Set notification for %s... [sigRecv1]\n", sigs1)

sigRecv2 := make(chan os.Signal, 1)
sigs2 := []os.Signal{syscall.SIGQUIT}
fmt.Printf("Set notification for %s... [sigRecv2]\n", sigs2)
// arg1:监测到信号时由此通知  arg2:监测的信号类型(不填则监控所有类型)
signal.Notify(sigRecv1, sigs1...)
signal.Notify(sigRecv2, sigs2...)
// 删除sigRecv1,之后恢复以系统默认方式处理信号
signal.Stop(sigRecv1)
//此时只剩下sigRecv2还在工作
(三)套接字(Socket)

​ 通过网络连接让多个进程建立通信并相互传递数据,实现进程跨机器通信。socket本身并不是一个协议,它只是提供了一个针对TCP或者UDP编程的接口,即是对TCP/IP协议的封装。

一、三种协议
  • TCP(Transmission Control Protocol 传输控制协议)

    ​ TCP是面向连接的、可靠的流协议。流就是指不间断的数据结构。TCP为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。 此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在才会发送数据,从而可以控制通信流量的浪费。

  • UDP(User Datagram Protocol 用户数据报协议)

    ​ UDP是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在UDP的情况下,虽然可以确保发送信息的大小,却不能保证信息一定会到达。因此,应用有时会根据自己的需要进行重发处理。 UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。由于UDP面向无连接,它可以随时发送数据。

  • SCTP(Stream Control Transmission Protocol 流控制传输协议)

    ​ SCTP是面向消息的(message-oriented)协议。同时也是面向连接、端到端、全双工、带有流量和拥塞控制的可靠传输协议。SCTP的连接称为关联,通过4次握手建立。SCTP能给在所连接的端点之间提供多个流,每个流各自可靠地按序递送消息。一个流上某个消息的丢失不会阻塞同一关联其他流上消息的投递。

    ——TCP VS UDP

    ​ 两者的区别在于TCP接收的是一堆数据,而每次取多少由主机决定;而UDP发的是数据报,客户发送多少就接收多少。

    ​ 拥有这些区别的原因是由于TCP和UDP的特性不同而决定的。TCP是面向连接的,也就是说,在连接持续的过程中,socket中收到的数据都是由同一台主机发出的,因此,知道保证数据是有序的到达就行了,至于每次读取多少数据自己看着办。 而UDP是无连接的协议,也就是说,只要知道接收端的IP和端口,且网络是可达的,任何主机都可以向接收端发送数据。这时候,如果一次能读取超过一个报文的数据,则会乱套。比如,主机A向发送了报文P1,主机B发送了报文P2,如果能够读取超过一个报文的数据,那么就会将P1和P2的数据合并在了一起,这样的数据是没有意义的。

    ——TCP VS SCTP

    (1)TCP是以字节为单位传输的,SCTP是以数据块为单位传输的

    ​ TCP接收端确认的是收到的字节数,SCTP接收端确认的是接收到的数据块。

    ​ 在实际的应用中,TCP发送方的可以将应用程序需要发送的多个消息打包到一个TCP包里面发出去。接收端在收到这个TCP包时,回给对端的ACK只是表明自己接收到了多少个字节,TCP协议本身并不会把收到的数据重新拆散分成两条应用层消息并通知应用程序去接收。应用需要根据应用程序自己定义的格式去拆成两条消息。与TCP不同,SCTP是将应用程序的每次调用sendmsg()发送的数据当作一个整体,放到一个被称为DATA CHUNK的数据块里面,接收端也是以DATA CHUNK为单位接收数据,并重新组包,通知应用程序接收。通常,应用程序每次调用recvmesg()都会收到一条完整的消息。

    (2)TCP通常是单路径传输,SCTP可以多路径传输

    ​ TCP的两端都只能用一个IP来建立连接,连接建立之后就只能用这一对IP来相互收发消息了。如果这一对IP之间的路径出了问题,那这条TCP连接就不可用了。SCTP两端都可以绑定到多个IP上,只要有其中一对IP能通,这条SCTP连接就还可以用。体现在socket API中,TCP只能bind一个IP,而SCTP可以bind到多个IP。

    (3)TCP是单流有序传输,SCTP可以多流独立有序/无序传输

    ​ 一条SCTP连接里面,可以区分多条不同的流(stream),不同的流之间的数据传输互不干扰。在同一条stream里面,SCTP支持有序/无序两种传输方式,应用程序在调用sendmsg()的时候,需要指定用哪一条stream传输,以及指定这条要发送的消息是需要有序传输还是无序传输的。如果在传输过程中丢包,则有序传递模式可能会在接收端被阻塞,而无序传输模式不会在接收端被阻塞。

    (4)TCP连接的建立过程需要三步握手,SCTP连接的建立过程需要四步握手

    ​ TCP连接建立过程,容易受到DoS攻击。在建立连接的时候,client端需要发送SYN给server端,server端需要将这些连接请求缓存下来。通过这种机制,攻击者可以发送大量伪造的SYN包到一个server端,导致server端耗尽内存来缓存这些连接请求,最终无法服务。

    ​ SCTP的建立过程需要四步握手,server端在收到连接请求时,不会立即分配内存缓存起来,而是返回一个COOKIE。client端需要回送这个COOKIE,server端校验之后,从cookie中重新获取有效信息(比如对端地址列表),才会最终建立这条连接。这样,可以避免类似TCP的SYN攻击。

    (5)SCTP有heartbeat机制来管理路径的可用性

    ​ SCTP协议本身有heartbeat机制来监控连接/路径的可用性。SCTP两端都可以bind多个IP,因此同一条SCTP连接的数据可以采用不同的IP来传输。不同的IP传输路径对应一条path,不同的path都可以由heartbeat或者是数据的传输/确认来监控其状态。如果heartbeat没相应,或者是数据在某条path超时没收到确认导致重传,则认为该path有一次传输失败。如果一条连接的连续传输次数超过设定的“连接最大重传次数”,则该连接被认为不可用,该连接会被关闭并通知应用程序。

二、socket基本特性
1. 通信域:
通信域 含义 地址形式 通信范围
AF_INET IPV4域 地址4字节 端口2字节 IPV4网络下两台计算机间的应用程序
AF_INET6 IPV6域 地址16字节 端口2字节 IPV6网络下两台计算机间的应用程序
AF_UNIX Unix域 路径名称 同一计算机下的两个应用程序
2. socket类型:
特性 SOCK_DGRAM SOCK_RAW SOCK_SEQPACKET SOCK_STREAM
数据形式 数据报 数据报 字节流 字节流
数据边界 没有 没有
逻辑连接 没有 没有
数据有序性 不能保证 不能保证 能保证 能保证
传输可靠性 不具备 不具备 具备 具备
3. socket通信过程:
  • 创建socket实例 socket() .
  • (客户端)绑定本机地址 bind() ######(服务器)创建socket实例、绑定本机地址 bind()、在地址上监听 listen()
  • 连接服务器 connect() ———————请求并建立TCP连接———————> >等待连接接入 accept()
  • 发送数据 write() ———————— 数据(请求) —————————> >接收数据 read()
  • 接受数据 read() <——————— 数据(响应) —————————— >发送数据 write()
  • 关闭连接 close() ———————— 文件结束通知 ———————-> >接收数据 read() 关闭连接 close()
4. Go API实现

​ Go底层获取的是操作系统的非阻塞式socket实例,即read()与write()不会阻塞程序运行。

部分读:即调用read()时,即使缓冲区没有任何数据,操作系统不会阻塞该系统调用,而是直接返回一个错误码EAGAIN,应用程序应该忽略他并在稍后再次尝试读取;在缓冲区有数据时,无论有多少数据,都会立即读取该数据并返回。

部分写:即调用write()时,即使缓冲区已满,操作系统不会阻塞该系统调用,而是直接返回一个错误码EAGAIN,应用程序应该忽略他并在稍后再次尝试写入;在缓冲区有部分空间时,无论是否可以全部写入,都会尽可能写入部分数据并返回写入的数据量。

​ Go帮我们屏蔽了系统的EAGAIN错误,屏蔽了其部分写特性,而保留了部分读特性(由于TCP字节流传输,接收方无法感知数据边界,所以不能帮助我们屏蔽部分读特性)

方法 作用
func Listen(net, laddr string) (Listener, error) 获取监听器
func Accept() 监听器开始接收请求
func Dial(network, address string) (Conn, error) 向指定网络发送连接建立申请
func DialTimeout(network, address string, timeout time.Duration) (Conn, error) 申请连接并设定连接超时等待时间
func Read(b []byte) (n int, err error) 从socket接收缓冲区读数据
func Write(b []byte) (n int, err error) 向socket写入缓冲区写数据
func Close() (err error) 关闭连接
func LocalAddr() net.Addr | func RemoteAddr() net.Addr 获取当前连接的本地地址和远程地址
上面两方法的返回值可以调用两个方法: .String() 网络地址 .Network() 协议类型
func SetDeadline(time.Time) error | SetReadDeadline| SetWriteDeadline 设置连接读、写超时时间
// 服务器端开启监听
listener, err := net.Listen("tcp", "127.0.0.1:8085")   //协议名 监听地址
conn, err := listener.Accept()

// 客户端开启发送,注意:客户端可以不绑定本机地址,操作系统内核会为其分配一个
conn1, err := net.Dial("tcp", "127.0.0.1:8085")        //协议名 发送地址
conn2, err := net.DialTimeout("tcp", "127.0.0.1:8085", 2*time.Second)  //协议名 发送地址 TCP连接超时时间
//*************************************************************************************
//1.直接通过Buffer读取
var dataBuffer bytes.Buffer
b:=make([]byte, 10)
for{
    n, err := conn.Read(b)
    if err != nil{
        if err == io.EOF{		//发现TCP连接在另一端已经关闭
            conn.Close()
        }else{
            fmt.Printf("error:%s\n", err)
        }
        break
    }
    dataBuffer.Write(b[:n])
}
//2.通过bufio读取
reader := bufio.NewReader(conn)     
line, err := reader.ReadBytes('\n')
//3.通过bufio写入
writer := bufio.NewWriter(conn1)
//*************************************************************************************
conn.LocalAddr().String()     //获取网络地址
conn.LocalAddr().Network()    //获取协议类型
//*************************************************************************************
// SetDeadline设置的是一个绝对时间(即具体时间),并且会对以后的每次读写都生效
// 所以循环读写的操作,需要不断更新这个时间
b:=make([]byte, 10)
for{
    conn.SetDeadline(time.Now().Add(2*time.Second))
    n, err:=conn.Read(b)
}
conn.SetDeadline(time.Time{})	//取消超时时间设置

你可能感兴趣的:(Golang,go,多进程,ipc)