网络IO的通信原理
首先,对于TCP通信来说,每个TCP Socket的内核中都有一个发送缓冲区和一个接收缓冲区 接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一 直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到 Socket的内核接收缓冲区。
read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。
进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的 内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端。
网卡中的缓冲区既不属于内核空间,也不属于用户空间。它属于硬件缓冲,允许网卡与操作系统之间有 个缓冲; 内核缓冲区在内核空间,在内存中,用于内核程序,做为读自或写往硬件的数据缓冲区; 用 户缓冲区在用户空间,在内存中,用于用户程序,做为读自或写往硬件的数据缓冲区。
网卡芯片收到网络数据会以中断的方式通知CPU,我有数据了,存在我的硬件缓冲里了,来读我啊。 CPU收到这个中断信号后,会调用相应的驱动接口函数从网卡的硬件缓冲里把数据读到内核缓冲区,正 常情况下会向上传递给TCP/IP模块一层一层的处理。
1、BIO(阻塞IO模型)
ServerSocket和Socket,阻塞体现在两个地方:连接阻塞和IO阻塞。
在Java中,如果要实现网络通信,我们会采用Socket套接字来完成。 Socket这不是一个协议,而是一个通信模型。其实它最初是BSD发明的,主要用来一台电脑的两个进程 间通信,然后把它用到了两台电脑的进程间通信。所以,可以把它简单理解为进程间通信,不是什么高 级的东西。主要做的事情不就是:
- A发包:发请求包给某个已经绑定的端口(所以我们经常会访问这样的地址182.13.15.16:1235, 1235就是端口);收到B的允许;然后正式发送;发送完了,告诉B要断开链接;收到断开允许, 马上断开,然后发送已经断开信息给B。
- B收包:绑定端口和IP;然后在这个端口监听;接收到A的请求,发允许给A,并做好接收准备,主 要就是清理缓存等待接收新数据;然后正式接收;接受到断开请求,允许断开;确认断开后,继续 监听其它请求。
可见,Socket其实就是I/O操作,Socket并不仅限于网络通信,在网络通信中,它涵盖了网络层、传输 层、会话层、表示层、应用层——其实这都不需要记,因为Socket通信时候用到了IP和端口,仅这两个 就表明了它用到了网络层和传输层;而且它无视多台电脑通信的系统差别,所以它涉及了表示层;一般 Socket都是基于一个应用程序的,所以会涉及到会话层和应用层。
单线程BIO
我们通过对BIOServerSocket进行改造,关注case1和case2部分。 case1: 增加了while循环,实现重复监听 case2: 当服务端收到客户端的请求后,不直接返回,而是等待20s。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServerSocket {
//先定义一个端口号,这个端口的值是可以自己调整的。
static final int DEFAULT_PORT = 8080;
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = null;
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("启动服务,监听端口:" + DEFAULT_PORT);
while (true) { //case1: 增加循环,允许循环接收请求
Socket socket = serverSocket.accept();
System.out.println("客户端:" + socket.getPort() + "已连接");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String clientStr = bufferedReader.readLine(); //读取一行信息
System.out.println("客户端发了一段消息:" + clientStr);
Thread.sleep(20000); //case2: 修改:增加等待时间
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("我已经收到你的消息了\n");
bufferedWriter.flush(); //清空缓冲区触发消息发送
}
}
}
接着,把BIOClientSocket复制两份(client1、client2),同时向BIOServerSocket发起请求。 运行后看到的现象应该是: client1先发送请求到Server端,由于Server端等待20s才返回,导致 client2的请求一直被阻塞。 这个情况会导致一个问题,如果服务端在同一个时刻只能处理一个客户端的连接,而如果一个网站同时 有1000个用户访问,那么剩下的999个用户都需要等待,而这个等待的耗时取决于前面的请求的处理时长。
基于多线程优化BIO
为了让服务端能够同时处理更多的客户端连接,避免因为某个客户端连接阻塞导致后续请求被阻塞,于 是引入多线程技术,当引入了多线程之后,每个客户端的链接(Socket),我们可以直接给到线程池去执 行,而由于这个过程是异步的,所以并不会同步阻塞影响后续链接的监听,因此在一定程度上可以提升 服务端链接的处理数量。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务器端
**/
public class BIOServerSocketWithThread {
static ExecutorService executorService= Executors.newFixedThreadPool(10);
public static void main(String[] args) {
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(8080);
System.out.println("启动服务:监听端口:8080");
//表示阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
while(true) {
Socket socket = serverSocket.accept(); //连接阻塞
System.out.println("客户端:" + socket.getPort());
//IO变成了异步执行
executorService.submit(new SocketThread(socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* 处理socket连接
**/
public class SocketThread implements Runnable{
private Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//inputstream是阻塞的(***)
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //表示获取客户端的请求报文
String clientStr = bufferedReader.readLine();
System.out.println("收到客户端发送的消息:" + clientStr);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("receive a message:" + clientStr + "\n");
bufferedWriter.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
//TODO 关闭IO流
}
}
}
2、NIO(非阻塞IO)
连接阻塞和IO阻塞都是非阻塞。
使用多线程的方式来解决这个问题,仍然有一个缺点,线程的数量取决于硬件配置,所以线程数量是有 限的,如果请求量比较大的时候,线程本身会收到限制从而并发量也不会太高。那怎么办呢,我们可以 采用非阻塞IO。 NIO 从JDK1.4 提出的,本意是New IO,它的出现为了弥补原本IO的不足,提供了更高效的方式,提出 一个通道(channel)的概念,在IO中它始终以流的形式对数据的传输和接受,下面我们演示一下NIO 的使用。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NIOServerSocket {
//NIO中的核心
//channel
//buffer
//selector
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); //设置连接非阻塞
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while(true){
//是非阻塞的
SocketChannel socketChannel=serverSocketChannel.accept(); //获得一个客户端连接
// socketChannel.configureBlocking(false);//IO非阻塞
if(socketChannel!=null){
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
int i=socketChannel.read(byteBuffer);
Thread.sleep(10000);
byteBuffer.flip(); //反转
socketChannel.write(byteBuffer);
}else{
Thread.sleep(1000);
System.out.println("连接位就绪");
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
所谓的NIO(非阻塞IO),其实就是取消了IO阻塞和连接阻塞,当服务端不存在阻塞的时候,就可以不 断轮询处理客户端的请求,如图4-4所示,表示NIO下的运行流程。
上述这种NIO的使用方式,仍然存在一个问题,就是客户端或者服务端需要通过一个线程不断轮询才能 获得结果,而这个轮询过程中会浪费线程资源。
多路复用IO
我们回到NIOClientSocket中下面这段代码,当客户端通过 read 方法去读取服务端返回的数据时,如果 此时服务端数据未准备好,对于客户端来说就是一次无效的轮询。 我们能不能够设计成,当客户端调用 read 方法之后,不仅仅不阻塞,同时也不需要轮询。而是等到服 务端的数据就绪之后, 告诉客户端。然后客户端再去读取服务端返回的数据呢?
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符, 一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。
常见的IO多路复用方式有【select、poll、epoll】,都是Linux API提供的IO复用方式,那么接下来重 点讲一下select、和epoll这两个模型
- select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这 样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点
1.由于他能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处 于 就绪 状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。
2.同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机 上万的TCP连接来说确实有点少
- epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相 对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那 么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最 大文件句柄,这个数字要远远大于1024。【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO, 当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞, 应该是数据已经完全准备好了,我只需要从用户空间读就行】
I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程 的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线 程,降低了系统的资源开销,它的整体实现思想如图所示。
客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻 塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过 selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完 成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。
单线程Reactor 模型(高性能I/O设计模式)
了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质 上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理 进行分离,通过一个或者多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去 异步非阻塞处理,如图所示。
Reactor模型有三个重要的组件:
- Reactor :将I/O事件发派给对应的Handler
- Acceptor :处理客户端连接请求
- Handlers :执行非阻塞读/写
最基本的单Reactor单线程模型(整体的I/O操作是由同一个线程完成的)。 其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处 理,有IO读写事件之后交给hanlder 处理。 Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的 hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的 IO事件都绑定到selector上,有Reactor分发)。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Reactor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public Reactor(int port) throws IOException {
selector=Selector.open();
serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new Acceptor(selector,serverSocketChannel));
}
@Override
public void run() {
while(!Thread.interrupted()){
try {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
dispatch(iterator.next());
iterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void dispatch(SelectionKey key){
//可能拿到的对象有两个
// Acceptor
// Handler
Runnable runnable=(Runnable)key.attachment();
if(runnable!=null){
runnable.run(); //
}
}
}
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Acceptor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public Acceptor(Selector selector, ServerSocketChannel serverSocketChannel) {
this.selector = selector;
this.serverSocketChannel = serverSocketChannel;
}
@Override
public void run() {
SocketChannel channel;
try {
channel=serverSocketChannel.accept();//得到一个客户端连接
System.out.println(channel.getRemoteAddress()+":收到一个客户端连接");
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ,new Handler(channel));
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Handler implements Runnable{
SocketChannel channe;
public Handler(SocketChannel channe) {
this.channe = channe;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"------");
ByteBuffer buffer=ByteBuffer.allocate(1024);
/*try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
int len=0,total=0;
String msg="";
try {
do {
len = channe.read(buffer);
if(len>0){
total+=len;
msg+=new String(buffer.array());
}
} while (len > buffer.capacity());
System.out.println("total:"+total);
//msg=表示通信传输报文
//耗时2s
//登录: username:password
//ServetRequets: 请求信息
//数据库的判断
//返回数据,通过channel写回到客户端
System.out.println(channe.getRemoteAddress()+": Server receive Msg:"+msg);
}catch (Exception e){
e.printStackTrace();
}finally {
if(channe!=null){
try {
channe.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
多线程单Reactor模型
单线程Reactor这种实现方式有存在着缺点,从实例代码中可以看出,handler的执行是串行的,如果其 中一个handler处理线程阻塞将导致其他的业务处理阻塞。由于handler和reactor在同一个线程中的执 行,这也将导致新的无法接收新的请求,我们做一个小实验: 在上述Reactor代码的DispatchHandler的run方法中,增加一个Thread.sleep()。 打开多个客户端窗口连接到Reactor Server端,其中一个窗口发送一个信息后被阻塞,另外一个窗 口再发信息时由于前面的请求阻塞导致后续请求无法被处理。 为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步 处理,将reactor和handler在不同的线程来执行,如图4-7所示。
在多线程Reactor模型中,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作 者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面 I/O请求的处理。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;
public class MultiReactor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public MultiReactor(int port) throws IOException {
selector=Selector.open();
serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new MultiAcceptor(selector,serverSocketChannel));
}
@Override
public void run() {
while(!Thread.interrupted()){
try {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
dispatch(iterator.next());
iterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void dispatch(SelectionKey key){
//可能拿到的对象有两个
// Acceptor
// Handler
Runnable runnable=(Runnable)key.attachment();
if(runnable!=null){
runnable.run(); //
}
}
}
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class MultiAcceptor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public MultiAcceptor(Selector selector, ServerSocketChannel serverSocketChannel) {
this.selector = selector;
this.serverSocketChannel = serverSocketChannel;
}
@Override
public void run() {
SocketChannel channel;
try {
channel=serverSocketChannel.accept();//得到一个客户端连接
System.out.println(channel.getRemoteAddress()+":收到一个客户端连接");
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ,new MutilDispatchHandler(channel));
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class MutilDispatchHandler implements Runnable{
SocketChannel channel;
private Executor executor= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public MutilDispatchHandler(SocketChannel channel) {
this.channel = channel;
}
@Override
public void run() {
processor();
}
private void processor(){
executor.execute(new ReaderHandler(channel));
}
static class ReaderHandler implements Runnable{
private SocketChannel channel;
public ReaderHandler(SocketChannel channel) {
this.channel = channel;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":-----");
ByteBuffer buffer=ByteBuffer.allocate(1024);
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int len=0,total=0;
String msg="";
try {
do {
len = channel.read(buffer);
if(len>0){
total+=len;
msg+=new String(buffer.array());
}
} while (len > buffer.capacity());
System.out.println("total:"+total);
//msg=表示通信传输报文
//耗时2s
//登录: username:password
//ServetRequets: 请求信息
//数据库的判断
//返回数据,通过channel写回到客户端
System.out.println(channel.getRemoteAddress()+": Server receive Msg:"+msg);
}catch (Exception e){
e.printStackTrace();
}finally {
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}