网络底层理解 | 青训营

339 阅读9分钟

本文是在上完网络部分的课程后,结合之前学到的知识以及查找相关资料,总结的个人对于网络上层应用维度的浅显理解,包括网络与IO的关系,IO模型以及系统的调用RPC等知识。

问题抛出

  1. 网络和IO的关系,IO面向的是谁?
  2. 精通IO模型,BIO
  3. RPC,全双工,IO框架

网络与IO

OSI 7层参考模型:应用层,表示层,会话层,传输控制层,网络层,链路层,物理层;

TCP/IP协议:

应用层,传输控制层,网络层,链路层,物理层。

两大层:应用层(程序员要做的事,应用层,表示层,会话层),内核公共(封装好的后四层)。

tcpdump -nn -i eth0 port 80 //网络抓包,便于网络分析

TCP内核:面向连接的,可靠的传输机制:ack seq,三次握手,滑动窗口

通过TCP三次握手后,会在客户端和服务端中都产生资源(socket queue,会在结束后删除),是虚无的; 网络IO读写是面向socket queue的单机读写行为。

socket(接字):

把客户端和服务端套在一起,得到四元组(全局唯一,ip:port ip:port ),是规则(recv-queue,send-queue)

端口号是可以复用的。

心跳检查

(对应服务器是否还可用,可以保证客户端需要用的时候有对应的服务端可用)

  1. 内核开启心跳检查:

健康检查级别,检查tcp对应的socket,检查连接可靠性,检查不到应用层。

  1. 应用程序级别也需要心跳检查:

用HTTP协议request,返回200 OK 说明服务器应用还可用;

ping也属于内核级别,ICMP,IP层次(网络层)。

  1. 心跳的实现:双方约定;协议捡漏

  2. 长连接与短连接:(应用层级的概念)

短连接生命周期很短(HTTP中keepalive是off状态),

长连接有多种情况(HTTP中keepalive是on状态),

无论是长连接还是短连接都可以开启心跳 ,但是一旦开始心跳,短连接就是每次之后都会断开,而长连接就是会一直不断开。

四次分手

当客户端和服务端分手时,四次分手(基于TCP可靠性以及ack确认包的机制):

  1. 客户端(C)先给服务端(S)发想分手的信息(F);
  2. S回C一个确认收到分手消息的信息(F.);
  3. S再给C回想分手的信息(F);
  4. C回S一个确认收到分手消息的信息(F.)。

若没有四次分手,会进入异常状态。

其实可以看出来

IO模型

IO模型是用在程序与内核(kernel,queue)之间的,通过内核实现。

当对方没有发送数据我们就在客户端APP进行read等操作的话,会进入blocking阻塞状态,BIO。而每一个连接阻塞,都要对应一个线程,当很多连接时就会需要很多线程。

BIO(Blocking I/O)

是传统的阻塞式I/O模型,也称为同步I/O模型。在BIO模型中,当进行I/O操作时,程序会一直阻塞,直到操作完成或出现错误。这意味着程序在等待I/O操作完成期间无法执行其他任务,可能会导致资源的浪费和性能的下降。

在BIO模型中,通常的流程是:

  1. 阻塞等待:当程序发起一个I/O操作(如读取文件或网络通信),它会一直阻塞,直到操作完成或出现错误。
  2. 数据读写:一旦I/O操作完成,程序会继续执行读取或写入数据的操作。

BIO模型适用于一些简单的场景,但在高并发和大量连接的情况下,它可能会导致性能瓶颈。因为每个I/O操作都会阻塞一个线程,如果有大量的并发连接,就需要创建大量的线程,会造成线程资源的浪费,并且线程切换开销也会增加。

NIO(Non-blocking I/O)

尽管BIO模型在某些情况下很简单,但它不适合高性能和高并发的应用。为了解决BIO模型的一些问题,后来出现了NIO(Non-blocking I/O)和AIO(Asynchronous I/O)等模型,它们允许程序在进行I/O操作时不阻塞,提高了应用的并发性能和响应能力。

BIO是只有在对方发了数据才返回,但是NIO是无论发没发都返回(没法的话就是返回空),这里能解决多线程的问题,但是需要我们处理返回的信息。

但是NIO有弊端,NIO需要循环处理连接,当遇到C10K这种很多连接时,1W个连接没人发送数据,假设很多年都没人发数据,那么这个线程就需要在一个CPU上连续跑很多年,一直在消耗CPU。

在Java中,传统的阻塞式I/O模型对应于Java I/O流(InputStream和OutputStream)的使用,而NIO模型对应于Java NIO库的使用。在Go语言中,阻塞式I/O模型也是默认的,但由于Go天生支持Goroutine和Channel,所以通过合理地使用并发特性,可以避免阻塞造成的性能问题。

多路复用器

多路复用器(Multiplexer,简称MUX)是一种网络编程中的机制,用于管理多个I/O通道(如套接字连接)的状态,并在这些通道上进行事件监视和事件驱动的操作。它允许单个线程同时监听多个通道上的事件,从而实现高效的并发I/O操作。

在多路复用器模型中,通过一个事件循环在一个线程中同时监听多个通道上的事件,包括读就绪、写就绪、连接就绪等。这允许程序同时管理多个I/O操作而无需为每个操作创建一个独立的线程。

简而言之,就是多个连接作为参数传递给一个函数,这个函数就会返回其中有数据的状态/事件,这些路,这些连接复用了这个函数,然后进行有效的read调用。

JAVA中有select,poll,epoll等等,这里就不赘述。

Go中,主要使用的多路复用器是通过标准库中的 net 包提供的 netpoll 机制来实现的。这个机制主要用于实现 net 包中的网络操作,比如 TCP 和 UDP。

具体来说,Go语言中的多路复用器使用以下几个关键组件:

  1. netpollnetpoll 是Go语言的网络多路复用器,它可以同时监听多个网络连接上的事件,包括读就绪、写就绪等。
  2. goroutine:Go语言的 goroutine 是轻量级的协程,通过 goroutine 可以在并发中实现多个I/O操作,而无需创建大量的线程。
  3. channel:Go语言中的 channel 是用于在 goroutine 之间传递数据的通信机制,可以用于在不同的 goroutine 之间通知事件的发生。

通过这些机制,Go语言的多路复用器能够高效地管理并发的I/O操作。开发者可以在单个 goroutine 中同时监听多个连接上的事件,并通过 channel 来通知事件的发生。

异步IO模型与同步IO模型

程序要自己去read的(牺牲线程)都叫做同步IO模型,以上都是同步IO模型。

异步IO模型与同步IO模型的异步处理是不同的。

异步IO模型有windows iocp,比较复杂,我也没搞懂。

我发现还是得把计组深入一下,内核的结构到虚拟化,容器化等等。

系统调用

FC(function call)

sc(system call)

rpc(Remote Procedure Call)

rpc(Remote Procedure Call)

RPC(Remote Procedure Call)是一种远程过程调用的通信协议,允许一个程序调用另一个程序或进程中的函数或方法,就像调用本地函数一样,而无需开发者显式地处理网络通信细节。

在分布式系统中,不同的计算机或进程之间需要进行通信以协同完成任务。RPC提供了一种方便的方式来实现远程通信,使得开发者能够像调用本地函数一样调用远程函数,而不必担心底层的网络通信细节。

在Go中,可以使用标准库中的 net/rpc 包来实现RPC。

net/rpc 包提供了实现基本RPC功能所需的工具和接口,以下是在Go中实现RPC的基本步骤:

  1. 定义接口:首先,需要定义一个接口,其中包含想要远程调用的函数。这个接口将会被客户端和服务器端共享。例如:
go
type Calculator interface {
    Add(args *Args, reply *int) error
}
  1. 实现接口:然后,在服务器端实现这个接口的具体函数,这些函数将会被客户端远程调用。例如:
go
type CalculatorImpl struct{}

func (c *CalculatorImpl) Add(args *Args, reply *int) error {
    *reply = args.A + args.B
    return nil
}
  1. 注册服务:在服务器端,你需要使用 rpc.Register 函数来注册实现了接口的对象,以便客户端能够调用它。例如:
func main() {
    calc := new(CalculatorImpl)
    rpc.Register(calc)

    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal("Listen error:", err)
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal("Accept error:", err)
        }
        go rpc.ServeConn(conn)
    }
}
  1. 客户端调用:在客户端,使用 rpc.Dial 函数连接到服务器,并通过 rpc.Call 函数来调用远程方法。例如:
go
func main() {
    client, err := rpc.Dial("tcp", "localhost:1234")
    if err != nil {
        log.Fatal("Dial error:", err)
    }

    args := &Args{A: 5, B: 3}
    var reply int
    err = client.Call("Calculator.Add", args, &reply)
    if err != nil {
        log.Fatal("Call error:", err)
    }

    fmt.Printf("Result: %d\n", reply)
}

Args 是一个结构体,用于传递参数。Calculator.Add 是接口中定义的函数,通过 client.Call 方法远程调用服务器上的函数。

总之,Go语言的标准库 net/rpc 包提供了基本的RPC实现,使得能在Go中构建分布式应用并实现远程过程调用。如果需要更高级的功能,要使用其他RPC框架,如gRPC。

总结

在这篇文章中,我把网络底层原理捋了捋,发现很多知识都很零碎,不成体系,还得把计算机的基础打好才行,特别是计算机组成原理和计算机网络需要我仔细深入学习一下。