浅谈BIO到手写Redis客户端

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服务器中

  1. 我们首先导入jedis的依赖

    <dependencies>
    	<dependency>
    		<groupId>redis.clients</groupId>
    		<artifactId>jedis</artifactId>
    		<version>3.0.1</version>
    	</dependency>
    </dependencies>
    
  2. 书写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
    
  3. 有了上述的知识后,大概知道要发送内容如下

    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
    
  4. 有了上述的知识我们可以手写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如下所示:

    在这里插入图片描述

    运行后的结果如下所示:

    在这里插入图片描述

    可以看到我们的设置值都设置进去了。

    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值