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

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

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

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

Redis 为什么那么快

============

Redis的高性能主要依赖于几个方面。

  • C语言实现,C语言在一定程度上还是比Java语言性能要高一些,因为C语言不需要经过JVM进行翻译。

  • 纯内存I/O,内存I/O比磁盘I/O性能更快

  • I/O多路复用,基于epoll的I/O多路复用技术,实现高吞吐网络I/O

  • 单线程模型,单线程无法利用到多核CPU,但是在Redis中,性能瓶颈并不是在计算上,而是在I/O能力,所以单线程能够满足高并发的要求。 从另一个层面来说,单线程可以避免多线程的频繁上下文切换以及同步锁机制带来的性能开销。

下面我们分别从上述几个方面进行展开说明,先来看网络I/O的多路复用模型。

从请求处理开始分析

=========

当我们在客户端向Redis Server发送一条指令,并且得到Redis回复的整个过程中,Redis做了什么呢?

image-20210707221959664

图4-1

要处理命令,则redis必须完整地接收客户端的请求,并将命令解析出来,再将结果读出来,通过网络回写到客户端。整个工序分为以下几个部分:

  • 接收,通过TCP接收到命令,可能会历经多次TCP包、ack、IO操作

  • 解析,将命令取出来

  • 执行,到对应的地方将value读出来

  • 返回,将value通过TCP返回给客户端,如果value较大,则IO负荷会更重

其中解析执行是纯cpu/内存操作,而接收和返回主要是IO操作,首先我们先来看通信的过程。

网络IO的通信原理


同样,我也画了一幅图来描述网络数据的传输流程

首先,对于TCP通信来说,每个TCP Socket的内核中都有一个发送缓冲区和一个接收缓冲区

接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区。

read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。

进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端。

1576066931883

网卡中的缓冲区既不属于内核空间,也不属于用户空间。它属于硬件缓冲,允许网卡与操作系统之间有个缓冲;

内核缓冲区在内核空间,在内存中,用于内核程序,做为读自或写往硬件的数据缓冲区;

用户缓冲区在用户空间,在内存中,用于用户程序,做为读自或写往硬件的数据缓冲区

网卡芯片收到网络数据会以中断的方式通知CPU,我有数据了,存在我的硬件缓冲里了,来读我啊。

CPU收到这个中断信号后,会调用相应的驱动接口函数从网卡的硬件缓冲里把数据读到内核缓冲区,正常情况下会向上传递给TCP/IP模块一层一层的处理。

NIO多路复用机制


Redis的通信采用的是多路复用机制,什么是多路复用机制呢?

由于Redis是C语言实现,为了简化大家的理解,我们采用Java语言来描述这个过程。

在理解多路复用之前,我们先来了解一下BIO。

BIO模型


在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

public class BIOServerSocket {

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

static final int DEFAULT_PORT=8080;

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

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

//在服务器端,我们需要使用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();

}

}

}

}

}

演示方式,通过window的cmd窗口,使用telnet 192.168.1.102 8888 连接到Server端进行数据通信;也可以通过下面这样一个客户端程序来访问。

ReactorClient

public class ReactorClient {

private static Selector selector;

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

selector=Selector.open();

//创建一个连接通道连接指定的server

SocketChannel socketChannel= SocketChannel.open();

socketChannel.configureBlocking(false);

socketChannel.connect(new InetSocketAddress(“192.168.1.102”,8888));

socketChannel.register(selector, SelectionKey.OP_CONNECT);

while(true){

总结

阿里伤透我心,疯狂复习刷题,终于喜提offer 哈哈~好啦,不闲扯了

image

1、JAVA面试核心知识整理(PDF):包含JVMJAVA集合JAVA多线程并发,JAVA基础,Spring原理微服务,Netty与RPC,网络,日志,ZookeeperKafkaRabbitMQ,Hbase,MongoDB,Cassandra,设计模式负载均衡数据库一致性哈希JAVA算法数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算共30个章节。

image

2、Redis学习笔记及学习思维脑图

image

3、数据面试必备20题+数据库性能优化的21个最佳实践

image

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

ent

public class ReactorClient {

private static Selector selector;

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

selector=Selector.open();

//创建一个连接通道连接指定的server

SocketChannel socketChannel= SocketChannel.open();

socketChannel.configureBlocking(false);

socketChannel.connect(new InetSocketAddress(“192.168.1.102”,8888));

socketChannel.register(selector, SelectionKey.OP_CONNECT);

while(true){

总结

阿里伤透我心,疯狂复习刷题,终于喜提offer 哈哈~好啦,不闲扯了

[外链图片转存中…(img-IcuHRBNB-1713679613299)]

1、JAVA面试核心知识整理(PDF):包含JVMJAVA集合JAVA多线程并发,JAVA基础,Spring原理微服务,Netty与RPC,网络,日志,ZookeeperKafkaRabbitMQ,Hbase,MongoDB,Cassandra,设计模式负载均衡数据库一致性哈希JAVA算法数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算共30个章节。

[外链图片转存中…(img-ZTcRVicn-1713679613300)]

2、Redis学习笔记及学习思维脑图

[外链图片转存中…(img-ABf0xf6j-1713679613300)]

3、数据面试必备20题+数据库性能优化的21个最佳实践

[外链图片转存中…(img-V1ippOUg-1713679613301)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-fU5MSG8p-1713679613301)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值