【网络原理】基本原理篇:I/O

什么是I/O,I/O阻塞的原因?

到底啥是I/O

input /output,输入/输出,就是I/O

输入输出的到底是什么?
常见I/O又两种,一种是磁盘I/O,一种是网络I/O
磁盘I/O是指硬盘和内存之间的输入输出
例如我们读取本地文件的时候,将磁盘中的数据拷贝到内存中来,修改本地文件的时候,需要把修改了的数据在拷贝到磁盘中去,这就是磁盘I/O
网络I/O是指网卡和内存之间的输入输出
当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里。
看到这里有个问题,那为什么都要跟内存交互呢?
因为指令都是由CPU执行的,CPU与内存的交互速度要远远高于与外部设备的交互,如果没有内存,那么CPU直接和外部设备交互,也是I/O操作
总结:I/O 就是指内存与外部设备之间的交互(数据拷贝)。
好了,明确什么是 I/O 之后,让我们来揭一揭 socket 通信内幕

socket

socket那么要了解我们的网络模型
在这里插入图片描述
我们了解的多一些的都是应用层协议,例如HTTP、FTP、SMTP、DNS、Telnet等,实际上数据传输过程中,传输层都是TCP和UDP协议,提供端对端的接口,到网络层为数据包选择路由,如何发包,发到哪个设备,到数据链路层检错到物理层,实际是个拆包组包的过程,但是实际上顶层的应用层协议都是基于传输层协议实现的,那么有的一些需求应用层协议不足以满足,例如聊天工具,多人语音视频电话,这时候就需要直接和底层协议打交道,如何与TCP和UDP协议直接打交道呢,操作系统为我们提供了sockect,我们把socket理解成api,让我们能直接和传输层打交道

创建一个socket

首先我们要创建一个socket,以python为例

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

domain :这个参数用于选择通信的协议族,比如选择 IPv4 通信,还是 IPv6 通信等等;
type :选择套接字类型,可选字节流套接字、数据报套接字等等;

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

bind

现在我们已经创建了一个 socket,但现在还没有地址指向这个 socket。
众所周知,服务器应用需要指明 IP 和端口,这样客户端才好找上门来要服务。所以,此时我们需要指定一个地址和端口来与这个 socket 绑定一下。

server.bind(('0.0.0.0', 8000))

让客户端的请求都发送到我们服务端机器的8000端口上

listen

执行了 socket、bind 之后,此时的 socket 还处于 closed 的状态,也就是不对外监听的。然后,我们需要调用 listen 方法,让 socket 进入被动监听状态,这样的 socket 才能够监听到客户端的连接请求。

server.listen(backlog)

这个 backlog 我查阅资料的时候,看到了三种解释:

socket 有一个队列,同时存放已完成的连接和半连接。backlog 为这个队列的大小;
socket 有两个队列,分别为已完成的连接队列和半连接队列。backlog 为这个两个队列的大小之和;
socket 有两个队列,分别为已完成的连接队列和半连接队列。backlog 仅为已完成的连接队列大小。

解释下什么叫半连接
我们都知道 TCP 建立连接需要三次握手,当接收方收到请求方的建连请求后会返回 ack。此时,这个连接在接收方就处于半连接状态,当接收方再收到请求方的 ack 时,这个连接就处于已完成状态:
在这里插入图片描述
所以,上面讨论的就是这两种状态的连接的存放问题。
我查阅资料看到,基于 BSD 派生的系统的实现是使用的一个队列来同时存放这两种状态的连接, backlog 参数即为这个队列的大小。
而 Linux 则使用两个队列分别存储已完成连接和半连接,且 backlog 仅为已完成连接的队列大小

accept

现在,我们已经初始化好监听套接字了。此时会有客户端连上来,然后我们需要处理这些已经完成建连的连接。
从上面的分析我们可以得知,三次握手完成后的连接会被加入到已完成连接队列中去。
在这里插入图片描述
这时候,我们就需要从已完成连接队列中拿到连接进行处理,这个拿取动作就由 accpet 来完成。

sock, addr = server.accept()
def accept(self):
     """accept() -> (socket object, address info)

     Wait for an incoming connection.  Return a new socket
     representing the connection, and the address of the client.
     For IP sockets, the address info is a pair (hostaddr, port).
     """
     fd, addr = self._accept()
     sock = socket(self.family, self.type, self.proto, fileno=fd)
     # Issue #7995: if no default timeout is set and the listening
     # socket had a (non-zero) timeout, force the new socket in blocking
     # mode to override platform-specific socket flags inheritance.
     if getdefaulttimeout() is None and self.gettimeout():
         sock.setblocking(True)
     return sock, addr

这个方法返回的 int 值就是拿到的已完成连接的 socket 的文件描述符,之后操作这个 socket 就可以进行通信了。
如果已完成连接队列没有连接可以取,那么调用 accept 的线程会阻塞等待。
至此服务端的通信流程暂告一段落,我们再看看客户端的操作。

connect

客户端也需要创建一个 socket,也就是调用 socket(),这里就不赘述了,我们直接开始建连操作。
客户端需要与服务端建立连接,在 TCP 协议下开始经典的三次握手操作,再看一下上面画的图:
客户端创建完 socket 并调用 connect 之后,连接就处于 SYN_SEND 状态。当收到服务端的 SYN+ACK 之后,连接就变为 ESTABLISHED 状态,此时就代表三次握手完毕

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8000))

调用 connect 需要指定远程的地址和端口进行建连,三次握手完毕之后就可以开始通信了。
客户端这边不需要调用 bind 操作,默认会选择源 IP 和随机端口。

可以看到这里的两个阻塞点:
connect :需要阻塞等待三次握手的完成;
accept :需要等待可用的已完成的连接,如果已完成连接队列为空,则被阻塞。

read、write

连接建立成功之后,就能开始发送和接收消息了。我们来看一下:
read 为读数据,从服务端来看就是等待客户端的请求。如果客户端不发请求,那么调用 read 会处于阻塞等待状态,没有数据可以读,这个应该很好理解。
write 为写数据。一般而言服务端接受客户端的请求之后,会进行一些逻辑处理,然后再把结果返回给客户端,这个写入也可能会被阻塞。
这里可能有人就会问 read 读不到数据阻塞等待可以理解,write 为什么还要阻塞,有数据不就直接发了吗?
因为我们用的是 TCP 协议,TCP 协议需要保证数据可靠地、有序地传输,并且给予端与端之间的流量控制。
所以说发送不是直接发出去,它有个发送缓冲区,我们需要把数据先拷贝到 TCP 的发送缓冲区,由 TCP 自行控制发送的时间和逻辑,有可能还有重传什么的。
如果我们发的过快,导致接收方处理不过来,那么接收方就会通过 TCP 协议告知:别发了!忙不过来了。发送缓存区是有大小限制的,由于无法发送,还不断调用 write 那么缓存区就满了,满了就不允许你 write 了,所以 write 也会发生阻塞。

综上,read 和 write 都会发生阻塞。

为什么网络 I/O 会被阻塞?

因为和通信涉及到的 accept、connect、read、write 这几个方法都可能会发生阻塞。
阻塞会占用当前执行的线程,使之不能进行其他操作,并且频繁阻塞唤醒切换上下文也会导致性能的下降。
由于阻塞的缘故,起初的解决的方案就是建立多个线程。但是随着互联网的发展,用户激增连接数也随着激增,需要建立的线程数也随着一起增加,到后来就产生了 C10K 问题。
服务端顶不住了呀,咋办?
优化呗!

所以后来就弄了个非阻塞套接字,然后 I/O 多路复用、信号驱动 I/O、异步 I/O。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值