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

mac2025-07-27  1

文章参考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

最新回复(0)