学习使用socket编程,并自己实现一个简单的redis客户端

文章参考1:https://blog.csdn.net/weixin_39634961/article/details/80236161
文章参考2:https://blog.csdn.net/weixin_39569611/article/details/81879266
文章参考3:https://blog.csdn.net/a78270528/article/details/80318571

学习使用socket编程,并自己实现一个简单的redis客户端

1.socket大致介绍

socket编程是一门技术,它主要是在网络通信中经常用到.

既然是一门技术,由于现在是面向对象的编程,一些计算机行业的大神通过抽象的理念,在现实中通过反复的理论或者实际的推导,提出了抽象的一些通信协议,基于tcp/ip协议,提出大致的构想,一些泛型的程序大牛在这个协议的基础上,将这些抽象化的理念接口化,针对协议提出的每个理念,专门的编写制定的接口,与其协议一一对应,形成了现在的socket标准规范,然后将其接口封装成可以调用的接口,供开发者使用.

2.TCP/IP协议

要理解socket必须的得理解tcp/ip,它们之间好比送信的线路和驿站的作用,比如要建议送信驿站,必须得了解送信的各个细节。
什么是TCP/IP协议:https://blog.csdn.net/petterp/article/details/102779131

OSI模型简图:
在这里插入图片描述
在这里插入图片描述
网络中的七层协议为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。那么介绍一下在网络七层协议中传输数据时的工作原理是:
在数据的实际传输中,发送方将数据送到自己的应用层,加上该层的控制信息后传给表示层;表示层如法炮制,再将数据加上自己的标识传给会话层;以此类推,每一层都在收到的数据上加上本层的控制信息并传给下一层;最后到达物理层时,数据通过实际的物理媒体传到接收方。接收端则执行与发送端相反的操作,由下往上,将逐层标识去掉,重新还原成最初的数据。由此可见,数据通讯双方在对等层必须采用相同的协议,定义同一种数据标识格式,这样才可能保证数据的正确传输。

总体来说,OSI模型是从底层往上层发展出来的。
在这里插入图片描述
这个模型推出的最开始,是是因为美国人有两台机器之间进行通信的需求。

需求1:-> 物理层Physical(以二进制数据形式在物理媒体上传输数据)
科学家要解决的第一个问题是,两个硬件之间怎么通信。具体就是一台发些比特流,然后另一台能收到。于是,科学家发明了物理层:

主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。

需求2:-> 数据链路层Data Link(传输有地址的帧以及错误检测功能 )
现在通过电线我能发数据流了,但是,我还希望通过无线电波,通过其它介质来传输。然后我还要保证传输过去的比特流是正确的,要有纠错功能。

于是,发明了数据链路层:
定义了如何让格式化数据以进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。

需求3:-> 网络层Network (为数据包选择路由)
如果我有多台计算机,怎么找到我要发的那台?或者,A要给F发信息,中间要经过B,C,D,E,但是中间还有好多节点如K.J.Z.Y。我怎么选择最佳路径?这就是路由要做的事。

于是,发明了网络层:
即路由器,交换机那些具有寻址功能的设备所实现的功能。这一层定义的是IP地址,通过IP地址寻址。所以产生了IP协议。

需求4:-> 传输层Transport (提供端对端的接口协议,TCP/OCP等)
现在我能发正确的发比特流数据到另一台计算机了,但是当我发大量数据时候,可能需要好长时间,例如一个视频格式的,网络会中断好多次(事实上,即使有了物理层和数据链路层,网络还是经常中断,只是中断的时间是毫秒级别的)。

那么,我还须要保证传输大量文件时的准确性。于是,我要对发出去的数据进行封装。就像发快递一样,一个个地发。

例如TCP,是用于发大量数据的,我发了1万个包出去,另一台电脑就要告诉我是否接受到了1万个包,如果缺了3个包,就告诉我是第1001,234,8888个包丢了,那我再发一次。这样,就能保证对方把这个视频完整接收了。

例如UDP,是用于发送少量数据的。我发20个包出去,一般不会丢包,所以,我不管你收到多少个。在多人互动游戏,也经常用UDP协议,因为一般都是简单的信息,而且有广播的需求。如果用TCP,效率就很低,因为它会不停地告诉主机我收到了20个包,或者我收到了18个包,再发我两个!如果同时有1万台计算机都这样做,那么用TCP反而会降低效率,还不如用UDP,主机发出去就算了,丢几个包你就卡一下,算了,下次再发包你再更新。

TCP协议是会绑定IP和端口的协议,下面会介绍IP协议。

需求5:-> 会话层Session(解除与建立与别的接口的联系)
现在我们已经保证给正确的计算机,发送正确的封装过后的信息了。但是用户级别的体验好不好?难道我每次都要调用TCP去打包,然后调用IP协议去找路由,自己去发?当然不行,所以我们要建立一个自动收发包,自动寻址的功能。

于是,发明了会话层。会话层的作用就是建立和管理应用程序之间的通信。

需求6:-> 表示层Presentation(数据格式化,代码转换,数据加密)
现在我能保证应用程序自动收发包和寻址了。但是我要用Linux给window发包,两个系统语法不一致,就像安装包一样,exe是不能在linux下用的,shell在window下也是不能直接运行的。于是需要表示层(presentation),帮我们解决不同系统之间的通信语法问题。

需求7:-> 应用层Application(文件传输,电子邮件,文件服务,虚拟终端)
OK,传输的数据根据应用层的协议进行服务

详图:
在这里插入图片描述

3.那什么是socket

这不是一个协议,而是一个通信模型。其实它最初是伯克利加州分校软件研究所,简称BSD发明的,主要用来一台电脑的两个进程间通信,然后把它用到了两台电脑的进程间通信。所以,可以把它简单理解为进程间通信,不是什么高级的东西。主要做的事情不就是:

A发包:发请求包给某个已经绑定的端口(所以我们经常会访问这样的地址182.13.15.16:1235,1235就是端口);收到B的允许;然后正式发送;发送完了,告诉B要断开链接;收到断开允许,马上断开,然后发送已经断开信息给B。

B收包:绑定端口和IP;然后在这个端口监听;接收到A的请求,发允许给A,并做好接收准备,主要就是清理缓存等待接收新数据;然后正式接收;接受到断开请求,允许断开;确认断开后,继续监听其它请求。

可见,Socket其实就是I/O操作。Socket并不仅限于网络通信。在网络通信中,它涵盖了网络层、传输层、会话层、表示层、应用层——其实这都不需要记,因为Socket通信时候用到了IP和端口,仅这两个就表明了它用到了网络层和传输层;而且它无视多台电脑通信的系统差别,所以它涉及了表示层;一般Socket都是基于一个应用程序的,所以会涉及到会话层和应用层。
在这里插入图片描述

4.使用java实现一下socket

在这里插入图片描述

4.1 实现一个最简单的你(client)发我(service)收
/**
 * 实现了单向的通信,客户端给服务端发消息
 */
public class SocketOne {

    /*=============================服务端========================*/
    static class Service {
        public static void main(String[] args) throws IOException {
            int port = 8888;
            ServerSocket serverSocket = new ServerSocket(port);
            //该服务会一直阻塞等待连接,当有client连接才会真正返回操作连接的操作对象
            System.out.println("该服务会一直阻塞等待连接");
            Socket socket = serverSocket.accept();
            //当有了连接就取出连接中的数据
            InputStream inputStream = socket.getInputStream();
            byte[] b = new byte[1024];
            int len = 0;
            StringBuilder sb = new StringBuilder();
            //当等于-1就说明结束了
            while ((len = inputStream.read(b))!= -1) {
                //这里要注意:最好指定编码统一,并且注意String导入的包
                sb.append(new String(b, 0, len, "utf-8"));
            }
            //输出客户端发过来的消息
            System.out.println(sb.toString());
            //关闭众多的流
            inputStream.close();
            socket.close();
            serverSocket.close();
        }
    }
    /*========================服务端结束==================================*/

    /*=======================客户端开始===================================*/
    static class Client {
        public static void main(String[] args) throws IOException {
            String hostname = "127.0.0.1";
            int port = 8888;
            //创建一个客户端的socket
            Socket socket = new Socket(hostname, port);
            //输出内容
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("第一个socket程序".getBytes("utf-8"));
            //关闭连接
            outputStream.close();
            socket.close();
        }
    }
    /*=======================客户端结束==========================*/
}
4.2实现一下双向通信

上边最基础的通信中,服务端不能给回复,那怎么实现一个你发我收我回复的模型呢?

/**
 * 实现简单的双向通讯,一应一答
 */
public class SocketTwo {

    /*==================服务端开始=====================*/
    //与之前的不同之处在于,当读取完客户端的消息后,打开输出流,将指定消息发送回客户端
    static class Server {
        public static void main(String[] args) throws IOException {
            int port = 8888;
            ServerSocket serverSocket = new ServerSocket(port);

            System.out.println("-----服务端一直等待连接");
            Socket socket = serverSocket.accept();
            //当收到请求后
            InputStream inputStream = socket.getInputStream();
            byte[] b = new byte[1024];
            int len = 0;
            StringBuilder sb = new StringBuilder();
            while ((len = inputStream.read(b)) != -1) {
                sb.append(new String(b, 0, len, "utf-8"));
            }

            System.out.println("---服务端接收到客户端的信息---"+sb.toString());
            //客户端返回消息
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("服务端收到了,ok".getBytes("utf-8"));
            //关闭
            outputStream.close();
            inputStream.close();
            socket.close();
            serverSocket.close();
        }
    }
    /*==================服务端结束===========================*/

    /*====================客户端开始==========================*/
    //与之前不同:  在发送完消息时,调用关闭输出通道方法,然后打开输出流,等候服务端的消息。(不用做确认,tcp还是很可靠的)
    static class Client {
        public static void main(String[] args) throws IOException {
            String host = "127.0.0.1";
            int port = 8888;
            Socket socket = new Socket(host, port);
            //先给服务端发消息
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("服务端收到我的消息了吗,请回复".getBytes("utf-8"));

            //关闭输出的通道,如果不关闭,服务端将不会知道什么时候停止接收消息,将会一直接收消息
            //会导致持续连接,而不输出消息.关闭之后就需要创建新的socket连接了.
            //对于频繁的联系是很不合适的,so,一般需要指定一个约定的关闭符号或者指定消息的长度.后边会使用到
            socket.shutdownOutput();

            //接收服务端的回复
            InputStream inputStream = socket.getInputStream();
            byte[] b = new byte[1024];
            int len;

            StringBuilder sb = new StringBuilder();
            while ((len = inputStream.read(b)) != -1) {
                sb.append(new String(b, 0, len, "utf-8"));
            }
            System.out.println("--客户端收到服务端的回复---" + sb.toString());
            //关闭资源
            inputStream.close();
            outputStream.close();
            socket.close();
        }
    }
    /*==================客户端结束=====================*/
}
4.3使用指定长度告知服务端发送结束

本代码是参考的其他人的代码,其中有些自己的理解.如有不当,还请指出.

/**
 * 使用指定长度判断client---->server的信息是否发送完成
 */
public class SocketThree {

    /*====================服务端开始======================*/
    static class server {
        public static void main(String[] args) throws IOException {
            int port = 8888;
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("-----server一直阻塞等待client---");
            Socket socket = serverSocket.accept();

            InputStream inputStream = socket.getInputStream();
            while (true) {
                //n条消息就循环n+1次
                StringBuilder sb = new StringBuilder();
                //先获取头部第一个字节的标志,判断消息是否到最后了
                //例子同下边的客户端,当信息长度是1268时,first=4,second=244
                //这些值就对应了我的理解.在消息不超过256时,这里的first都是0.
                int first = inputStream.read();
                System.out.println("--first--"+first);

                if (-1==first) {
                    break;
                }
                //获取第二个字节,剩余的不满256的长度
                int second = inputStream.read();
                //组合回信息的总长度
                int len = (first << 8)+second;
                byte[] b = new byte[len];
                inputStream.read(b);
                sb.append(new String(b, "utf-8"));
                System.out.println("---收到的消息--"+sb);
            }

            inputStream.close();
            socket.close();
            serverSocket.close();
        }
    }
    /*===================服务端结束=======================*/

    /*====================客户端开始======================*/
    static class client {
        public static void main(String[] args) throws IOException {
            String host = "127.0.0.1";
            int port = 8888;
            Socket socket = new Socket(host, port);
            OutputStream outputStream = socket.getOutputStream();
            String meg = "可以使用一个超长的信息,测试为什么要先位移8位发送长度";
            byte[] bytes = meg.getBytes("utf-8");

            /*................................自己的理解..............................................
            用两个字节来表示消息的长度,最大容纳的字节数是256*256,用utf-8可以容纳2万个汉字.一个汉字是3个字节(utf-8);
            当不足256个字符的时候,就是一位表示,当超过256个时,一位就无法表示了,就需要两位.同理超过了256*256就需要3位;
            当传输的数字(表示长度的数字)超过256时,会被分解,所以需要使用两位表示;如果长度是1268如何分解???
            1268/256=4余244,就会被分成5个字节(5份01010..),所以需要分解传送.最后服务端组合起来,244+4*256=1268.
            */
            outputStream.write(bytes.length >> 8);
            outputStream.write(bytes.length);
            outputStream.write(bytes);
            outputStream.flush();

            //发送第二条消息
            meg = "这是第二条消息";
            byte[] bytes2 = meg.getBytes("utf-8");
            outputStream.write(bytes2.length >> 8);
            outputStream.write(bytes2.length);
            outputStream.write(bytes2);
            outputStream.flush();
            //关闭
            outputStream.close();
            socket.close();
        }
    }
    /*===================客户端结束=======================*/
}
4.4优化一下服务端

此时服务端只能处理一次请求,然后下一次又要重新开始,这是很麻烦的.有没有什么办法可以解决?
1.使用死循环监控,这是一种方式,但是当请求较大怎么办.
2.在死循环的基础上,添加线程池,每来一个请求就开启一个线程.

/**
 * 服务端使用线程池,长期开启,不关闭.应对较多的请求.
 */
public class SocketFour {
    /*=====================服务端开始============================*/
    static class Server {
        public static void main(String[] args) throws IOException {
            //创建一个线程池
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            int port = 8888;
            
            /*ServerSocket有以下3个属性。
             SO_TIMEOUT:表示等待客户连接的超时时间milliseconds。一般不设置,会持续等待。
             SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。一般不设置。
             SO_RCVBUF:表示接收数据的缓冲区的大小。一般不设置,用系统默认就可以了。 */
             
            ServerSocket serverSocket = new ServerSocket(port);
            //设置超时时间,时间超了,就会停止改服务
            //java.net.SocketTimeoutException: Accept timed out
            serverSocket.setSoTimeout(10000);
            
            //写一个死循环,避免停止
            for (; ; ) {
                //只有有了请求才往下,在这里阻塞
                Socket socket = serverSocket.accept();
                //每次来一个请求就用一个线程去处理
                executorService.execute(()->{
                    try {
                        InputStream inputStream = socket.getInputStream();
                        StringBuilder sb = new StringBuilder();
                        int len;
                        byte[] b = new byte[1024];
                        while ((len = inputStream.read(b)) != -1) {
                            sb.append(new String(b, 0, len, "utf-8"));
                        }
                        System.out.println("---收到的消息是---"+sb);
                        inputStream.close();
                        socket.close();
                        System.out.println("===等待下一个消息");
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });

            }
        }
    }
    /*====================服务端结束=============================*/
    
    /*====================客户端开始=============================*/
    static class Client {
        public static void main(String[] args) throws IOException {
            //可以写个多线程测试
            String host = "127.0.0.1";
            int port = 8888;
            Socket socket = new Socket(host, port);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("这是一个消息".getBytes("utf-8"));
            outputStream.close();
            socket.close();
        }
    }
    /*=====================客户端结束============================*/
}
4.5如何发送一个对象呢

使用ObjectOutputStream/ObjectIutputStream来封装一下字节流.
注意:要发送的实体类必须实现java的serializable接口.

/**
 * 客户端向服务端发送对象
 */
public class SocketFive {

    /*===========================实体类开始===============================*/
    static class User implements Serializable{
        private static final long serialVersionUID = 5321738719161973699L;
        private String name;
        private Integer age;

        public User(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    /*============================实体类结束==============================*/

    /*=============================服务端开始========================*/
    static class Service {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            int port = 8888;
            ServerSocket serverSocket = new ServerSocket(port);

            System.out.println("该服务会一直阻塞等待连接");
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            User user =(User) objectInputStream.readObject();

            System.out.println(user.toString());

            inputStream.close();
            socket.close();
            serverSocket.close();
        }
    }
    /*========================服务端结束==================================*/

    /*=======================客户端开始===================================*/
    static class Client {
        public static void main(String[] args) throws IOException {
            String hostname = "127.0.0.1";
            int port = 8888;
            Socket socket = new Socket(hostname, port);
            OutputStream outputStream = socket.getOutputStream();
            User user = new User("测试传输对象", 100);
            //对象必须实现序列化
            //java.io.NotSerializableException: domain.User
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(user);

            outputStream.close();
            objectOutputStream.close();
            socket.close();
        }
    }
    /*=======================客户端结束==========================*/
}
4.6实现一下简单的redis客户端

redis有一个自己的序列化的协议RESP.应当遵守此协议编程客户端.
REdis Serialization Protocol,这里给出官方的文档链接。
RESP主要有这么几种类型:

  • 简单字符串,开头是 “+”
  • 错误信息,开头是 “-“
  • 数字,开头是 “:”
  • 大字符串(一般是二进制),开头是 “$”
  • 数组,开头是 “*”
/**
 * 使用socket编写一个简单的redis客户端
 * 这里写一个客户端即可,服务端是redis软件
 */
public class SocketRedis {

    private String host;
    private int port;
    private Socket socket;

    public SocketRedis(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void set(String key, String value) {
        //按照redis要求的规则构建命令,都要以\r\n结尾
        StringBuilder outputsb = new StringBuilder();
        // *3意思是当前的命令中包含3个内容,就应该有3个$num
        outputsb.append("*3").append("\r\n")
                //$3 表示下一个内容长度是3
                .append("$3").append("\r\n")
                //内容
                .append("set").append("\r\n")
                //$4  表示下一个有4个长度
                .append("$").append(key.length()).append("\r\n")
                .append(key).append("\r\n")
                //同理  写入value的长度和内容
                .append("$").append(value.length()).append("\r\n")
                .append(value).append("\r\n");

        try {
            //创建客户端
            socket = new Socket(host, port);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(outputsb.toString().getBytes());
            socket.shutdownOutput();
            //接收回应
            InputStream inputStream = socket.getInputStream();
            StringBuilder inputsb = new StringBuilder();
            byte[] b = new byte[1024];
            int len;
            while ((len = inputStream.read(b)) != -1) {
                inputsb.append(new String(b, 0, len));
            }
            System.out.println(inputsb);
            inputStream.close();
            outputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public String get(String key) {
        //按照redis要求的规则构建命令,都要以\r\n结尾
        StringBuilder outputsb = new StringBuilder();
        // *3意思是当前的命令中包含3个内容,就应该有3个$num
        outputsb.append("*2").append("\r\n")
                //$3 表示下一个内容长度是3
                .append("$3").append("\r\n")
                //内容
                .append("get").append("\r\n")
                //$4  表示下一个有4个长度
                .append("$").append(key.length()).append("\r\n")
                .append(key).append("\r\n");
        //声明返回的字节数组
        byte[] b=null;
        try {
            //创建客户端
            socket = new Socket(host, port);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(outputsb.toString().getBytes());
            socket.shutdownOutput();
            //接收回应
            InputStream inputStream = socket.getInputStream();
            while (true) {
                //获取第一个字节,是$
                int first = inputStream.read();
                if (-1==first) {
                    break;
                }
                //获取第二个字节,是当前的返回值的长度
                int second = inputStream.read();

                b= new byte[second];
                inputStream.read(b);
            }
            inputStream.close();
            outputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new String(b);
    }

    public static void main(String[] args) {
        SocketRedis socketRedis = new SocketRedis("127.0.0.1", 6379);
        //socketRedis.set("num", "11 22 33 44");
        String name = socketRedis.get("name");
        System.out.println(name);
    }
}

运行set之后就能看到结果
在这里插入图片描述

其他

华为的100个网络基础知识:https://blog.csdn.net/devcloud/article/details/101199255

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值