从网络通信的演进过程彻底搞懂Redis高性能通信的原理(全网最详细,建议收藏

//先定义一个端口号,这个端口的值是可以自己调整的。

//在服务器端,我们需要使用ServerSocket,所以我们先声明一个ServerSocket变量

ServerSocket serverSocket=null;

//接下来,我们需要绑定监听端口, 那我们怎么做呢?只需要创建使用serverSocket实例

//ServerSocket有很多构造重载,在这里,我们把前边定义的端口传入,表示当前

//ServerSocket监听的端口是8080

serverSocket=new ServerSocket(DEFAULT_PORT);

System.out.println(“启动服务,监听端口:”+DEFAULT_PORT);

//回顾一下前面我们讲的内容,接下来我们就需要开始等待客户端的连接了。

//所以我们要使用的是accept这个函数,并且当accept方法获得一个客户端请求时,会返回

//一个socket对象, 这个socket对象让服务器可以用来和客户端通信的一个端点。

//开始等待客户端连接,如果没有客户端连接,就会一直阻塞在这个位置

Socket socket=serverSocket.accept();

//很可能有多个客户端来发起连接,为了区分客户端,咱们可以输出客户端的端口号

System.out.println(“客户端:”+socket.getPort()+“已连接”);

//一旦有客户端连接过来,我们就可以用到IO来获得客户端传过来的数据。

//使用InputStream来获得客户端的输入数据

//bufferedReader大家还记得吧,他维护了一个缓冲区可以减少数据源读取的频率

BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));

String clientStr=bufferedReader.readLine(); //读取一行信息

System.out.println(“客户端发了一段消息:”+clientStr);

//服务端收到数据以后,可以给到客户端一个回复。这里咱们用到BufferedWriter

BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

bufferedWriter.write(“我已经收到你的消息了\n”);

bufferedWriter.flush(); //清空缓冲区触发消息发送

}

}

BIOClientSocket

public class BIOClientSocket {

static final int DEFAULT_PORT=8080;

public static void main(String[] args) throws IOException {

//在客户端这边,咱们使用socket来连接到指定的ip和端口

Socket socket=new Socket(“localhost”,8080);

//使用BufferedWriter,像服务器端写入一个消息

BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

bufferedWriter.write(“我是客户端Client-01\n”);

bufferedWriter.flush();

BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));

String serverStr=bufferedReader.readLine(); //通过bufferedReader读取服务端返回的消息

System.out.println(“服务端返回的消息:”+serverStr);

}

}

上述代码构建了一个简单的BIO通信模型,也就是服务端建立一个监听,客户端向服务端发送一个消息,实现简单的网络通信,那BIO有什么弊端呢?

我们通过对BIOServerSocket进行改造,关注case1和case2部分。

  • case1: 增加了while循环,实现重复监听

  • case2: 当服务端收到客户端的请求后,不直接返回,而是等待20s。

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个用户都需要等待,而这个等待的耗时取决于前面的请求的处理时长,如图4-2所示。

image-20210708152538953

图4-2

基于多线程优化BIO

为了让服务端能够同时处理更多的客户端连接,避免因为某个客户端连接阻塞导致后续请求被阻塞,于是引入多线程技术,代码如下。

ServerSocket

public static void main(String[] args) throws IOException, InterruptedException {

final int DEFAULT_PORT=8080;

ServerSocket serverSocket=null;

serverSocket=new ServerSocket(DEFAULT_PORT);

System.out.println(“启动服务,监听端口:”+DEFAULT_PORT);

ExecutorService executorService= Executors.newFixedThreadPool(5);

while(true) {

Socket socket = serverSocket.accept();

executorService.submit(new SocketThread(socket));

}

}

SocketThread

public class SocketThread implements Runnable{

Socket socket;

public SocketThread(Socket socket) {

this.socket = socket;

}

@Override

public void run() {

System.out.println(“客户端:” + socket.getPort() + “已连接”);

try {

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String clientStr = null; //读取一行信息

clientStr = bufferedReader.readLine();

System.out.println(“客户端发了一段消息:” + clientStr);

Thread.sleep(20000);

BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

bufferedWriter.write(“我已经收到你的消息了\n”);

bufferedWriter.flush(); //清空缓冲区触发消息发送

} catch (IOException e) {

e.printStackTrace();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

如图4-3所示,当引入了多线程之后,每个客户端的链接(Socket),我们可以直接给到线程池去执行,而由于这个过程是异步的,所以并不会同步阻塞影响后续链接的监听,因此在一定程度上可以提升服务端链接的处理数量。

image-20210708160026412

图4-3

NIO非阻塞IO


使用多线程的方式来解决这个问题,仍然有一个缺点,线程的数量取决于硬件配置,所以线程数量是有限的,如果请求量比较大的时候,线程本身会收到限制从而并发量也不会太高。那怎么办呢,我们可以采用非阻塞IO。

NIO 从JDK1.4 提出的,本意是New IO,它的出现为了弥补原本IO的不足,提供了更高效的方式,提出一个通道(channel)的概念,在IO中它始终以流的形式对数据的传输和接受,下面我们演示一下NIO的使用。

NioServerSocket

public class NioServerSocket {

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();

if (socketChannel != null) {

//读取数据

ByteBuffer buffer = ByteBuffer.allocate(1024);

socketChannel.read(buffer);

System.out.println(new String(buffer.array()));

//写出数据

Thread.sleep(10000); //阻塞一段时间

//当数据读取到缓冲区之后,接下来就需要把缓冲区的数据写出到通道,而在写出之前必须要调用flip方法,实际上就是重置一个有效字节范围,然后把这个数据接触到通道。

buffer.flip();

socketChannel.write(buffer);//写出数据

} else {

Thread.sleep(1000);

System.out.println(“连接未就绪”);

}

}

} catch (IOException e) {

e.printStackTrace();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

NioClientSocket

public class NioClientSocket {

public static void main(String[] args) {

try {

SocketChannel socketChannel= SocketChannel.open();

socketChannel.configureBlocking(false);

socketChannel.connect(new InetSocketAddress(“localhost”,8080));

if(socketChannel.isConnectionPending()){

socketChannel.finishConnect();

}

ByteBuffer byteBuffer= ByteBuffer.allocate(1024);

byteBuffer.put(“Hello I’M SocketChannel Client”.getBytes());

byteBuffer.flip();

socketChannel.write(byteBuffer);

//读取服务端数据

byteBuffer.clear();

while(true) {

int i = socketChannel.read(byteBuffer);

if (i > 0) {

System.out.println(“收到服务端的数据:” + new String(byteBuffer.array()));

} else {

System.out.println(“服务端数据未准备好”);

Thread.sleep(1000);

}

}

} catch (IOException | InterruptedException e) {

e.printStackTrace();

}

}

}

所谓的NIO(非阻塞IO),其实就是取消了IO阻塞和连接阻塞,当服务端不存在阻塞的时候,就可以不断轮询处理客户端的请求,如图4-4所示,表示NIO下的运行流程。

image-20210708165359843

图4-4

上述这种NIO的使用方式,仍然存在一个问题,就是客户端或者服务端需要通过一个线程不断轮询才能获得结果,而这个轮询过程中会浪费线程资源。

多路复用IO


大家站在全局的角度再思考一下整个过程,有哪些地方可以优化呢?

我们回到NIOClientSocket中下面这段代码,当客户端通过read方法去读取服务端返回的数据时,如果此时服务端数据未准备好,对于客户端来说就是一次无效的轮询。

我们能不能够设计成,当客户端调用read方法之后,不仅仅不阻塞,同时也不需要轮询。而是等到服务端的数据就绪之后, 告诉客户端。然后客户端再去读取服务端返回的数据呢?

就像点外卖一样,我们在网上下单之后,继续做其他事情,等到外卖到了公司,外卖小哥主动打电话告诉你,你直接去前台取餐即可。

while(true) {

int i = socketChannel.read(byteBuffer);

if (i > 0) {

System.out.println(“收到服务端的数据:” + new String(byteBuffer.array()));

} else {

System.out.println(“服务端数据未准备好”);

Thread.sleep(1000);

}

}

所以为了优化这个问题,引入了多路复用机制。

I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作

什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。

常见的IO多路复用方式有**【select、poll、epoll】**,都是Linux API提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型

  • **select:**进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点

  • 由于他能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。

  • 同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的TCP连接来说确实有点少

  • epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024

【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】

I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图4-5所示。

客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。

image-20210708203509498

图4-5

NIOServer的实现如下

测试访问的时候,直接在cmd中通过telnet连接NIOServer,便可发送信息。

public class NIOServer implements Runnable{

Selector selector;

ServerSocketChannel serverSocketChannel;

public NIOServer(int port) throws IOException {

selector=Selector.open(); //多路复用器

serverSocketChannel=ServerSocketChannel.open();

//绑定监听端口

serverSocketChannel.socket().bind(new InetSocketAddress(port));

serverSocketChannel.configureBlocking(false);//非阻塞配置

//针对serverSocketChannel注册一个ACCEPT连接监听事件

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

}

@Override

public void run() {

while(!Thread.interrupted()){

try {

selector.select(); //阻塞等待事件就绪

Set selected=selector.selectedKeys(); //得到事件列表

Iterator it=selected.iterator();

while(it.hasNext()){

dispatch((SelectionKey) it.next()); //分发事件

it.remove(); //移除当前时间

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

private void dispatch(SelectionKey key) throws IOException {

if(key.isAcceptable()){ //如果是客户端的连接事件,则需要针对该连接注册读写事件

register(key);

}else if(key.isReadable()){

read(key);

}else if(key.isWritable()){

write(key);

}

}

private void register(SelectionKey key) throws IOException {

//得到事件对应的连接

ServerSocketChannel server=(ServerSocketChannel)key.channel();

SocketChannel channel=server.accept(); //获得客户端的链接

channel.configureBlocking(false);

//把当前客户端连接注册到selector上,注册事件为READ,

// 也就是当前channel可读时,就会触发事件,然后读取客户端的数据

channel.register(this.selector,SelectionKey.OP_READ);

}

private void read(SelectionKey key) throws IOException {

SocketChannel channel=(SocketChannel)key.channel();

ByteBuffer byteBuffer= ByteBuffer.allocate(1024);

channel.read(byteBuffer); //把数据从channel读取到缓冲区

System.out.println(“server receive msg:”+new String(byteBuffer.array()));

}

private void write(SelectionKey key) throws IOException {

SocketChannel channel=(SocketChannel)key.channel();

//写一个信息给到客户端

channel.write(ByteBuffer.wrap(“hello Client,I’m NIO Server\r\n”.getBytes()));

}

public static void main(String[] args) throws IOException {

NIOServer server=new NIOServer(8888);

new Thread(server).start();

}

}

事实上NIO已经解决了上述BIO暴露的下面两个问题:

  1. 同步阻塞IO,读写阻塞,线程等待时间过长。

  2. 在制定线程策略的时候,只能根据CPU的数目来限定可用线程资源,不能根据连接并发数目来制定,也就是连接有限制。否则很难保证对客户端请求的高效和公平。

到这里为止,通过NIO的多路复用机制,解决了IO阻塞导致客户端连接处理受限的问题,服务端只需要一个线程就可以维护多个客户端,并且客户端的某个连接如果准备就绪时,会通过事件机制告诉应用程序某个channel可用,应用程序通过select方法选出就绪的channel进行处理。

单线程Reactor 模型(高性能I/O设计模式)


了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去异步非阻塞处理,如图4-6所示。

Reactor模型有三个重要的组件:

  • **Reactor :**将I/O事件发派给对应的Handler

  • **Acceptor :**处理客户端连接请求

  • **Handlers :**执行非阻塞读/写

image-20210708212057895

图4-6

下面演示一个单线程的Reactor模型。

Reactor

Reactor 负责响应IO事件,一旦发生,广播发送给相应的Handler去处理。

public class Reactor implements Runnable{

private final Selector selector;

private final ServerSocketChannel serverSocketChannel;

public Reactor(int port) throws IOException {

//创建选择器

selector= Selector.open();

//创建NIO-Server

serverSocketChannel=ServerSocketChannel.open();

serverSocketChannel.bind(new InetSocketAddress(port));

serverSocketChannel.configureBlocking(false);

SelectionKey key=serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

// 绑定一个附加对象

key.attach(new Acceptor(selector,serverSocketChannel));

}

@Override

public void run() {

while(!Thread.interrupted()){

try {

selector.select(); //阻塞等待就绪事件

Set selectionKeys=selector.selectedKeys();

Iterator it=selectionKeys.iterator();

while(it.hasNext()){

dispatch((SelectionKey) it.next());

it.remove();

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

public void dispatch(SelectionKey key){

//调用之前注册时附加的对象,也就是attach附加的acceptor

Runnable r=(Runnable)key.attachment();

if(r!=null){

r.run();

}

}

public static void main(String[] args) throws IOException {

new Thread(new Reactor(8888)).start();

}

}

Acceptor

public class Acceptor implements Runnable{

private Selector selector;

private 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连接中数据就绪时,调用DispatchHandler来处理channel

//巧妙使用了SocketChannel的attach功能,将Hanlder和可能会发生事件的channel链接在一起,当发生事件时,可以立即触发相应链接的Handler。

channel.register(selector, SelectionKey.OP_READ,new DispatchHandler(channel));

} catch (IOException e) {

e.printStackTrace();

}

}

}

Handler

public class DispatchHandler implements Runnable{

private SocketChannel channel;

public DispatchHandler(SocketChannel channel) {

this.channel = channel;

}

@Override

public void run() {

System.out.println(Thread.currentThread().getName()+“—handler”); //case: 打印当前线程名称,证明I/O是同一个线程来处理。

ByteBuffer buffer=ByteBuffer.allocate(1024);

int len=0,total=0;

String msg=“”;

try {

do {

len = channel.read(buffer);

if (len > 0) {

total += len;

msg += new String(buffer.array());

}

buffer.clear();

} while (len > buffer.capacity());

System.out.println(channel.getRemoteAddress()+“:Server Receive msg:”+msg);

}catch (Exception e){

e.printStackTrace();

if(channel!=null){

try {

channel.close();

} catch (IOException ioException) {

ioException.printStackTrace();

}

}

}

}

}

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

Docker步步实践

目录文档:

①Docker简介

②基本概念

③安装Docker

④使用镜像:

⑤操作容器:

⑥访问仓库:

⑦数据管理:

⑧使用网络:

⑨高级网络配置:

⑩安全:

⑪底层实现:

⑫其他项目:

理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-bApyrAGi-1710409880907)]
[外链图片转存中…(img-Ggs1JTZ2-1710409880908)]
[外链图片转存中…(img-wcKAsfAr-1710409880908)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-YVKIEoqa-1710409880909)]

Docker步步实践

目录文档:

[外链图片转存中…(img-yASEPXtv-1710409880909)]

[外链图片转存中…(img-QWrlcbpN-1710409880910)]

①Docker简介

②基本概念

③安装Docker

[外链图片转存中…(img-Qj29TmER-1710409880910)]

④使用镜像:

[外链图片转存中…(img-Ylyexch0-1710409880910)]

⑤操作容器:

[外链图片转存中…(img-nqCuEKWb-1710409880911)]

⑥访问仓库:

[外链图片转存中…(img-nlj8vguY-1710409880911)]

⑦数据管理:

[外链图片转存中…(img-PeVfz3tv-1710409880911)]

⑧使用网络:

[外链图片转存中…(img-ZheDItko-1710409880912)]

⑨高级网络配置:

[外链图片转存中…(img-Ing6gKHE-1710409880912)]

⑩安全:

[外链图片转存中…(img-zW6ucqmy-1710409880912)]

⑪底层实现:

[外链图片转存中…(img-eKZtfMzU-1710409880913)]

⑫其他项目:

[外链图片转存中…(img-7dmaTggJ-1710409880913)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值