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
如下所示:
运行后的结果如下所示:
可以看到我们的设置值都设置进去了。