0、前言
本文所有代码可见 => 【gitee code demo】
本文涉及的主题:
1、BIO、NIO的业务实践和缺陷
2、Redis IO多路复用:redis快的主要原因
3、epoll 架构
部分图片 via 【epoll 原理分析】
1、BIO单线程版
1.1 业务代码
client
client代码相同 启动多个即可
public class RedisClient1 {
public static void main(String[] args) throws IOException {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
Socket socket = new Socket("127.0.0.1", 6300);
log("{} {}> 尝试连接服务 {}", sdf.format(new Date()) ,socket.getLocalPort(), socket.getPort());
OutputStream outputStream = socket.getOutputStream();
while (true) {
Scanner scanner = new Scanner(System.in);
log("{} {}> ", sdf.format(new Date()) ,socket.getLocalPort());
String string = scanner.nextLine();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
log("{} {}> 发送数据:{}", sdf.format(new Date()) ,socket.getLocalPort(), string);
}
outputStream.close();
socket.close();
}
}
server
public class RedisServerBIO {
public static void main(String[] args) throws IOException {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ServerSocket serverSocket = new ServerSocket(6300);
while (true) {
log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());
Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
log("{} {}> {} 连接到服务", sdf.format(new Date()), socket.getLocalPort(), socket.getPort());
InputStream inputStream = socket.getInputStream();
int length = -1;
byte[] bytes = new byte[1024];
log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());
while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
{
log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), serverSocket.getLocalPort(), socket.getPort(), new String(bytes, 0, length));
}
inputStream.close();
socket.close();
}
}
}
1.2 结果演示
现象:
1、client1 连接到server,client2尝试连接被阻塞
2、client2 先发送的消息未被server接受,client1后发送的repeat消息被server接受
结论:
BIO会一直阻塞,单线程下只能处理一个socket连接
存在的问题:
多 client 访问时效率低
2、BIO多线程版
2.1 业务代码
public class RedisServerBIOMultiThread {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6300);
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());
while (true) {
Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
log("{} {}> {} 连接到服务", sdf.format(new Date()), socket.getLocalPort(), socket.getPort());
new Thread(() -> {
try {
InputStream inputStream = socket.getInputStream();
int length = -1;
byte[] bytes = new byte[1024];
while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
{
log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), serverSocket.getLocalPort(), socket.getPort(), new String(bytes, 0, length));
}
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}, Thread.currentThread().getName()).start();
System.out.println(Thread.currentThread().getName());
}
}
}
2.1 结果演示
现象:
client1 、client2 都能正常连接到 server且正常发送、接受消息
结论:
BIO多线程提高处理能力,可以同时处理多个socket连接
存在的问题:
每个线程只能处理一个socket,当client数量大时,需要消耗大量线程资源
3、NIO
3.1 业务代码
当一个客户端与服务端进行连接,这个socket就会加入到一个容器中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了
public class RedisServerNIO {
static ArrayList<SocketChannel> socketList = new ArrayList<>();
static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocket = ServerSocketChannel.open();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
serverSocket.bind(new InetSocketAddress("127.0.0.1", 6300));
serverSocket.configureBlocking(false);//设置为非阻塞模式
while (true) {
for (SocketChannel element : socketList) {
int read = element.read(byteBuffer);
if (read > 0) {
byteBuffer.flip();
byte[] bytes = new byte[read];
byteBuffer.get(bytes);
//System.out.println(JSONUtil.toJsonStr(element));
log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), element.socket().getLocalPort(), element.socket().getPort(), new String(bytes, 0, read));
byteBuffer.clear();
}
}
SocketChannel socketChannel = serverSocket.accept();
if (socketChannel != null) {
log("{} {}> {} 连接到服务", sdf.format(new Date()), socketChannel.socket().getLocalPort(), socketChannel.socket().getPort());
socketChannel.configureBlocking(false);//设置为非阻塞模式
socketList.add(socketChannel);
log("{} {}> socket 数量: {} ", sdf.format(new Date()), socketChannel.socket().getLocalPort(), socketList.size());
}
}
}
}
3.2 结果演示
现象:
1、client1 、client2 都能正常连接到 server且正常发送、接受消息
2、server 没有创建额外线程
结论:
NIO 可以实现一个线程处理多个 socket 连接
存在的问题:
1、每次遍历所有socket,有很多无用功
2、遍历过程在用户态,还需要将数据从内核态读取到用户态
4、IO多路复用
1、使用 epoll() 实现,多个网络连接 socket 复用同一个线程
2、基于事件驱动机制,socket 中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有 socket
3、减少了内核态和用户态的切换
Redis IO多路复用实现
4.1 epoll_create()
创建内核中的fd容器
4.2 epoll_ctl()
epoll_ctl函数用于增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中
4.3 epoll_wait
用于监听套接字事件,可以通过设置超时时间timeout来控制监听的行为为阻塞模式还是超时模式