0%

网络IO

网络IO

Linux网络IO模型

  • 阻塞IO模型 默认情况下,所有文件操作都是阻塞的。 在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误才返回,在此期间会一直等待。

  • 非阻塞IO模型 recvfrom从应用层到内核的时候,如果缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,然后轮询检查这个状态,看内核是否有数据到来

  • IO复用模型 linux中提供了select/poll,通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll就可以侦测多个fd是否处于就绪状态。不过其是顺序扫描且支持的fd数量有限。

    升级版的epoll基于事件驱动方式代替顺序扫描,当有fd就绪时,调用回调函数进行通知,性能更高

  • 信号驱动IO模型 通过系统调用sigaction执行一个信号处理函数(该系统调用会立即返回,进程继续工作,非阻塞)。当数据准备就绪时,为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据

    内核通知我们何时可以开始一个IO操作,还需要通过recvfrom来读取数据,在将数据从内核复制到自己的缓冲区

  • 异步IO 告知内核启动某个操作,并让内核在整个操作(包括数据从内核复制到用户自己的缓冲区)完成后通知。

    内核通知我们操作何时已经完成

JAVA中IO使用的模型

这里先说一下同步和非同步的概念

同步和非同步是操作系统级别的,主要描述操作系统在收到程序请求网络IO操作后,如果网络IO资源没有准备好,该如何响应程序

  • 同步是指用户进程触发IO操作并等待或者轮询去查看IO操作是否就绪,直到网络IO资源准备好
  • 非同步是指用户进程触发IO操作后返回一个标记就可以开始做自己的事情了,当网络IO资源准备好后,用事件机制通知给程序

再说一下阻塞和非阻塞的概念

阻塞和非阻塞是程序级别的,主要描述程序请求操作系统IO操作后,如果网络IO资源没有准备好,程序如何处理(就是一种读取或写入操作函数的实现方式)

  • 阻塞IO的读写或写入函数会进行等待
  • 非阻塞IO读取或写入函数会立即返回一个状态值,使用线程一直轮询,直到IO资源准备好

然后开始2*2的随机组合

  • 同步阻塞IO 应用程序发起一个IO操作后,必须等待IO操作的完成,只有当真正完成了IO操作后,应用程序才能运行。JAVA传统的IO模型就是用的这种方式
  • 同步非阻塞IO 应用程序发起一个IO操作以后便可以返回做其他事情,然后应用程序需要时不时地询问一下IO操作是否就绪,这个过程也造成了一些CPU的消耗。JAVA的NIO就是用的这种方式
  • 异步阻塞IO 应用程序发起一个IO操作后,不等待内核IO操作完成,等内核完成IO操作后会通知应用程序。阻塞的原因是使用的select来进行系统调用的
  • 异步非阻塞IO 应用程序发起一个IO操作后立即返回,等IO操作真正完成后,应用程序会得到IO操作完成的通知,此时应用程序只需要对数据进行处理就好了,不需要进程实际的IO读写操作,因为真正的IO读取或写入操作已经由内核完成了

阻塞模型

在之前网络通信都是阻塞模型

  • 客户端向服务端发出请求后,客户端会一直处于等待状态,直到服务器端返回结果或网络出现问题
  • 服务器端也是如此,在处理某个客户端A发来的请求时,另一个客户端B发来的请求会等待,直到服务器端的处理线程线程上一个请求的处理

在服务端使用ServerSocket来建立套接字,accept方法会进行阻塞等待客户端的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try(
// 创建一个ServerSocket对象
ServerSocket serverSocket = new ServerSocket(9090);
// accept方法,返回Socket对象,这里会进行阻塞,应用程序向操作系统请求接收已准备好的客户端连接的数据信息
Socket s = serverSocket.accept();
// 获取输入流,这里读取数据也会阻塞
InputStream is = s.getInputStream();
// 输出流,给客户端返回消息
OutputStream os = s.getOutputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(isr);
){
String str;
while ((str = reader.readLine()) != null){
System.out.print(str);
}
os.write("我已收到消息".getBytes());

} catch (IOException e){
e.printStackTrace();
}

serverSocket.accept()阻塞

服务器端发起一个accept动作,询问操作系统是否有新的Socket套接字信息从端口发送过来,如果没有则serverSocket.accept()会一直等待

阻塞模型的问题:

  • 同一时间,服务器只能接收一个客户端的请求信息,第二个客户端需要等待服务器接收完第一个请求数据后才会被接收
  • 服务器一次只能处理一个客户端请求,处理完成并返回后才能进行第二次请求的处理

多线程阻塞模型

由于阻塞模型的弊端,高并发时会导致请求太慢,所以提出了使用多线程来解决上述阻塞问题

  • 服务器收到客户端A的请求后,开启线程去进行数据处理。主线程可以继续接收客户端B的请求

但是这样在进行serverSocket.accept();操作时还是单线程运行,只有业务处理才会使用多线程,对于接收数据的并发能力并没有提升

同步非阻塞模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
boolean flag = true;
try {
ServerSocket serverSocket = new ServerSocket(6666);
// 使用超时时间来设置为非阻塞状态,超过该时间会抛出SocketTimeoutException
serverSocket.setSoTimeout(100);

while (true){
Socket socket = null;
try{
// 设置了超时时间后accept就不会一直阻塞了
socket = serverSocket.accept();
} catch (SocketTimeoutException e){
synchronized (obj){ // 100ms内没有接收到任何数据,可以在这里做一些别的操作
System.out.println("没接收到数据,先歇一歇吧");
try {
obj.wait(10);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
continue;

}
// 开线程处理数据
new Thread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}

serverSocket.setSoTimeout可以使accept方法不一直阻塞,而是到了超时时间后抛出SocketTimeoutException异常,此时就可以用主线程做别的事情了,虽然实际还是使用的accept阻塞模型,但是有所改善

多路复用模型

多路复用模型(也就是NIO)不再使用操作系统级别的同步IO,目前主要实现有select、poll、epoll、kqueue

那如果线程要读数据,结果读了一部分就返回了,线程如何知道何时才应该继续读呢?采用事件轮询select,如果没有任何事件到来,最多等待timeout时间,线程处于阻塞状态,一旦期间有任何事件到来,就立刻返回;timeout时间到了之后还是没有任何事件到来,也会立即返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 绑定8080端口
serverSocketChannel.bind(new InetSocketAddress(8080));

// 注册监听的事件
// ServerSocketChannel只能注册OP_ACCEPT
// SocketChannel可注册OP_READ、OP_WRITE、OP_CONNECT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


while(true){
// 询问selector中准备好的事件
// 100表示每隔100ms唤醒一次
selector.select(100);
// 获取到上述询问拿到的事件类型
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
// 接收到服务端的请求
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
// 处理过了就要移除掉,否则再次select()还会拿到该事件
iterator.remove();
} else if(selectionKey.isReadable()){
SocketChannel sc = (SocketChannel) selectionKey.channel();
byteBuffer.clear();
int n = sc.read(byteBuffer);
if(n > 0){
byteBuffer.flip();
Charset charset = StandardCharsets.UTF_8;
String message = String.valueOf(charset.decode(byteBuffer).array());
System.out.println(message);
}
sc.register(selector,SelectionKey.OP_WRITE);
iterator.remove();
} else if(selectionKey.isWritable()){
SocketChannel sc = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("已接收到消息".getBytes());
buffer.flip();
sc.write(buffer);
iterator.remove();
}
}
}

}

多路复用显然绕过了accept方法的阻塞问题,使得操作系统可以在一个端口上能够同时接收多个客户端的IO事件

  • 相比于每个连接进来创建一个线程来比,NIO使用一个线程管理多个连接,使用的线程数量大大降低。
  • 之前的IO是面向流的,只能从流中读取,并且读完之后就无法在读,只能自己缓存数据。而NIO是面向Buffer的,可以随意读取里边的任何自己数据,不需要自己缓存数据,只需要移动读写指针就行

欢迎关注我的其它发布渠道