本文是在上完网络部分的课程后,结合之前学到的知识以及查找相关资料,总结的个人对于网络上层应用维度的浅显理解,包括网络与IO的关系,IO模型以及系统的调用RPC等知识。
问题抛出
- 网络和IO的关系,IO面向的是谁?
- 精通IO模型,BIO
- 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)
端口号是可以复用的。
心跳检查
(对应服务器是否还可用,可以保证客户端需要用的时候有对应的服务端可用)
- 内核开启心跳检查:
健康检查级别,检查tcp对应的socket,检查连接可靠性,检查不到应用层。
- 应用程序级别也需要心跳检查:
用HTTP协议request,返回200 OK 说明服务器应用还可用;
ping也属于内核级别,ICMP,IP层次(网络层)。
-
心跳的实现:双方约定;协议捡漏
-
长连接与短连接:(应用层级的概念)
短连接生命周期很短(HTTP中keepalive是off状态),
长连接有多种情况(HTTP中keepalive是on状态),
无论是长连接还是短连接都可以开启心跳 ,但是一旦开始心跳,短连接就是每次之后都会断开,而长连接就是会一直不断开。
四次分手
当客户端和服务端分手时,四次分手(基于TCP可靠性以及ack确认包的机制):
- 客户端(C)先给服务端(S)发想分手的信息(F);
- S回C一个确认收到分手消息的信息(F.);
- S再给C回想分手的信息(F);
- 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模型中,通常的流程是:
- 阻塞等待:当程序发起一个I/O操作(如读取文件或网络通信),它会一直阻塞,直到操作完成或出现错误。
- 数据读写:一旦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语言中的多路复用器使用以下几个关键组件:
- netpoll:
netpoll是Go语言的网络多路复用器,它可以同时监听多个网络连接上的事件,包括读就绪、写就绪等。 - goroutine:Go语言的
goroutine是轻量级的协程,通过goroutine可以在并发中实现多个I/O操作,而无需创建大量的线程。 - 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的基本步骤:
- 定义接口:首先,需要定义一个接口,其中包含想要远程调用的函数。这个接口将会被客户端和服务器端共享。例如:
go
type Calculator interface {
Add(args *Args, reply *int) error
}
- 实现接口:然后,在服务器端实现这个接口的具体函数,这些函数将会被客户端远程调用。例如:
go
type CalculatorImpl struct{}
func (c *CalculatorImpl) Add(args *Args, reply *int) error {
*reply = args.A + args.B
return nil
}
- 注册服务:在服务器端,你需要使用
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)
}
}
- 客户端调用:在客户端,使用
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。
总结
在这篇文章中,我把网络底层原理捋了捋,发现很多知识都很零碎,不成体系,还得把计算机的基础打好才行,特别是计算机组成原理和计算机网络需要我仔细深入学习一下。