IO的模型有三种,BIO(同步阻塞式IO),NIO(同步非阻塞式IO),AIO(异步非阻塞式IO),今天我们来谈谈BIO。
Java BIO:在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket
,然后在客户端启动Socket
来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
BIO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。具体如下图:
上面谈到同步、异步、阻塞、非阻塞又是什么意思呢?以食堂打饭为例。
- 同步:正常调用。你自己去食堂拿饭卡亲自去打饭(使用同步IO时,Java自己处理IO读写)
- 异步:基于回调。委托一小弟拿饭卡到食堂打饭,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(饭卡),OS需要支持异步IO操作API)
- 阻塞:没有开启新的线程。食堂排队打饭,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)
- 非阻塞:付好钱,取个号,然后坐在椅子上做其它事,等号广播会通知你取饭,没到号你就不能取,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能取(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
我们下面来利用Java的BIO来实现一个时间服务器。书写的代码如下:
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//客户端代码
public class BioClient {
public static void main(String[] args) {
Socket socket = null;
OutputStream outputStream = null;
try {
//连接服务器端
socket = new Socket("127.0.0.1", 9999);
//开启一个线程处理从服务端发送来
new Thread(new BioClientHandler(socket)).start();
//获取对应的输出流
outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要发送的消息:");
while (true) {
String s = scanner.nextLine();
if (s.trim().equals("by")) {
break;
}
outputStream.write(s.getBytes());
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
//客户端的处理器
public class BioClientHandler implements Runnable {
private Socket socket;
public BioClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
int count = 0;
byte[] bytes = new byte[1024];
while ((count = inputStream.read(bytes)) != -1) {
System.out.println("\n收到服务器的消息:" + new String(bytes, 0, count, StandardCharsets.UTF_8));
System.out.println("请输入要发送的消息:");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
//服务器端代码
public class BioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(9999);
TimeServerHandlerExecutorPool executorPool = new TimeServerHandlerExecutorPool();
while (true) {
Socket socket = serverSocket.accept();
System.out.println("客户端" + socket.getRemoteSocketAddress().toString() + "来连接了");
executorPool.execute(new BioServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
import java.util.concurrent.*;
public class TimeServerHandlerExecutorPool implements Executor {
private ExecutorService executorService;
/**
* @param corePoolSize 核心线程数量
* @param maximumPoolSize 线程创建最大数量
* @param keepAliveTime 当创建到了线程池最大数量时 多长时间线程没有处理任务,则线程销毁
* @param unit keepAliveTime时间单位
* @param workQueue 此线程池使用什么队列
*/
public TimeServerHandlerExecutorPool(int maxPoolSize, int queueSize) {
this.executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
/**
* @param corePoolSize 核心线程数量
* @param maximumPoolSize 线程创建最大数量
* @param keepAliveTime 当创建到了线程池最大数量时 多长时间线程没有处理任务,则线程销毁
* @param unit keepAliveTime时间单位
* @param workQueue 此线程池使用什么队列
*/
public TimeServerHandlerExecutorPool(int corePoolSize, int maxPoolSize, int queueSize) {
this.executorService = new ThreadPoolExecutor(corePoolSize,
maxPoolSize, 120L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(queueSize));
}
@Override
public void execute(Runnable command) {
executorService.execute(command);
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
public class BioServerHandler implements Runnable {
private Socket socket;
public BioServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
int count = 0;
String content = null;
byte[] bytes = new byte[1024];
while ((count = inputStream.read(bytes)) != -1) {
String line = new String(bytes, 0, count, StandardCharsets.UTF_8);
System.out.println(line);
content = line.trim().equalsIgnoreCase("SJ") ? new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()) : "你发的是啥?";
outputStream.write(content.getBytes());
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
然后先运行BioServer
,然后在运行BioClient
,输入SJ
,然后运行结果如下:
可以看到我们程序并没有结束,一直在阻塞的接受,这就是BIO(同步阻塞式IO)
接下来我们要模拟的就是Redis
的客户端,首先我们通过Jedis
来连接我们自己写的服务器,看发送的是什么,然后手写Redis
的客户端模拟发送的数据,最后再看看是否真的将我们的数据存入到Redis
服务器中
-
我们首先导入
jedis
的依赖<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.0.1</version> </dependency> </dependencies>
-
书写
Jedis
客户端,我们利用的是自己书写的BIO服务器,主要看下发送的是什么命令import redis.clients.jedis.Jedis; public class JedisTest { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1", 9999); jedis.set("ys", "haha"); jedis.incr("yy"); jedis.get("ys"); } }
运行的结果如下:
可以看到发送的内容如上图所示,但是这些又是什么东西呢?于是上网搜,原来
Redis客户端
和Redis服务
端通信的协议使RESP协议
具体的中文官方的解释:Resp协议自己对命令的通俗的解释
*3 //*表示接下来有几条命令,3表示有三组命令,以$分隔 $3 //$表示接下来的操作的字符是长度,3表示长度是3 SET $2 ys $4 haha
-
有了上述的知识后,大概知道要发送内容如下
jedis.set("ys", "haha"); jedis.incr("yy"); jedis.get("ys");
转换后的内容如下
*3 $3 SET $2 ys $4 haha *2 $3 GET $2 ys *2 $4 INCR $2 yy
-
有了上述的知识我们可以手写
Redis的客户端
了package myRedis; //定义好命令的参数的类 public class Resp { public static final String STAR = "*"; public static final String CRLF = "\r\n"; public static final String DOLLAR = "$"; public static enum command{ SET,GET,INCR } }
package myRedis; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class RedisSocket { private Socket socket; private InputStream inputStream; private OutputStream outputStream; //对应的构造函数,传入IP地址,端口号 public RedisSocket(String ip, int prot) { if (!isCon()) { try { socket = new Socket(ip, prot); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); } catch (IOException e) { e.printStackTrace(); } } } //发送命令 public void send(String string) { System.out.println(string); try { outputStream.write(string.getBytes()); } catch (IOException e) { e.printStackTrace(); } } //读取返回的结果 public String read() { byte[] b = new byte[1024]; int count = 0; try { count = inputStream.read(b); } catch (IOException e) { e.printStackTrace(); } return new String(b, 0, count); } //判断是否断开连接 private boolean isCon() { return socket != null && socket.isClosed() && socket.isConnected(); } //关闭连接 public void close() { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
package myRedis; public class RedisClient { private RedisSocket socket; public RedisClient(String ip, int prot) { this.socket = new RedisSocket(ip, prot); } //对应的set命令 public String set(String key, String value) { socket.send(RedisUtil(Resp.command.SET, key.getBytes(), value.getBytes())); return socket.read(); } public void close() { socket.close(); } //对应的get命令 public String get(String key) { socket.send(RedisUtil(Resp.command.GET, key.getBytes())); return socket.read(); } //对应的incr命令 public String incr(String key) { socket.send(RedisUtil(Resp.command.INCR, key.getBytes())); return socket.read(); } //将传进来的参数转成Redis的命令 private String RedisUtil(Resp.command command, byte[]... bytes) { StringBuilder sb = new StringBuilder(); sb.append(Resp.STAR).append(1 + bytes.length).append(Resp.CRLF); sb.append(Resp.DOLLAR).append(command.toString().getBytes().length).append(Resp.CRLF); sb.append(command.toString()).append(Resp.CRLF); for (byte[] aByte : bytes) { sb.append(Resp.DOLLAR).append(aByte.length).append(Resp.CRLF); sb.append(new String(aByte)).append(Resp.CRLF); } return sb.toString(); } public static void main(String[] args) { RedisClient redisClient = new RedisClient("192.168.182.133", 6379); System.out.println(redisClient.set("ys", "haha")); System.out.println(redisClient.get("ys")); System.out.println(redisClient.incr("yy")); redisClient.close(); } }
运行上面的代码,运行之前的
Redis
如下所示:运行后的结果如下所示:
可以看到我们的设置值都设置进去了。