1 RPC功能简介
- 远程过程调用,可以让程序员像调用本地方法一样调用远程方法
- 本地只有接口Car、Fly,服务端有其实现类MyCar和MyFly,客户端通过动态代理生成该接口的代理类,这个代理类底层需要将相关信息通过IO传递到远程服务端,最终服务端调用MyCar和MyFly的方法,并将结果通过IO返回给客户端
2 协议作用
- 所谓使用哪种协议,就是指将数据以该协议要求的格式,发送出去,将要发送的内容包装成指定协议要求的格式,就叫网络封包
- 使用RPC协议传递数据,就好像小火车(协议Myheader)拉货(数据MyContent)
- 我们既可以用自定义的RPC协议,也可以用Http协议作为载体传输数据
- Dubbo中的传输协议,可以是自定义RPC协议,也可以是Http协议
- Provider可能是通过Tomcat对外暴露服务,也可能是通过自定义的RPC框架对外暴露服务
- 当前主机能支持的并发量,主要取决于IO上的损耗,尤其在读取时间,所以尽量使用更小的数据包,而小的数据包取决于协议上的减轻以及更好的压缩算法,压缩算法消耗cpu,但cpu处理速度比IO快,通常不会达到瓶颈
3 有状态协议和无状态协议
- 自定义有状态协议:在协议头中添加requestID属性表示状态,因为协议有状态,所以客户端能分辨出返回的response是哪次request产生的
- 因此可以多个客户端复用一个链接(Socket)
- 客户端向服务器发送请求后,其他客户端就可以使用该链接继续向服务器发送请求,而不必等服务端返回数据
- 无状态协议:Http协议无状态,所以客户端无法分辨出返回的response是哪次request产生的
- 因此我们通常浏览器发送请求后,都会阻塞一直等到Tomcat服务器返回,这个阻塞是浏览器实现的,当浏览器开启多个窗口访问一个服务器时,实际上需要建立多个链接,否则新的请求会被老的请求阻塞
- 虽然我们使用Netty编写客户端时,可以编写为不阻塞到服务端返回,而是发送完毕后将该链接给其他客户端使用,但这样做,会导致我们无法将response和对应的request结果关联起来,即客户端A调用远程方法可能会得到客户端B调用远程方法的结果
- 虽然Http协议本身无状态,但可以同时对客户端和服务端进行特殊处理,例如在其内加上requestID,这样多个客户端就可以复用一个链接了
- 但实际上,通常服务端已经提供好了服务,我们没法去修改,比如服务端是一个Tomcat,且回复的内容中,并没有requestID,我们又没法修改服务端代码,所以对于微服务这种,我们向服务器发送请求后,如果服务器没有及时响应,客户端应该一直等待,直至服务器响应或者超时
4 粘包和拆包
- 客户端写入和服务端读取,是两台机器发生的操作,我们并不能控制客户端每发送一次数据包,服务端就接收一次数据包,因此服务端读取到的数据包,很可能是客户端半次发送的或多次发送的
- 粘包:服务端一次读取到了客户端发送的多个数据包
- 拆包:服务端一次只读取到了客户端发送的一个数据包的一部分
5 IO Threads
- 客户端java程序写入数据时,其实是先将数据从用户空间复制到内核空间的Send-Q,之后内核将这部分数据通过网卡传输到服务端的网卡中,随后服务端网卡产生中断,将网卡中数据复制到内核空间的Recv-Q,如果数据到达服务端网卡速度过快,操作系统会关闭中断,直接干预数据DMA拷贝,即轮循将网卡中数据放入内核,服务端java程序读取数据程序时,其实是将Recv-Q中数据拷贝到用户空间
- 如果服务端读取数据速度较慢,导致服务端的Recv-Q被填满,那么发送方会阻塞,一直等到接收方接收完毕,这一般被称为网卡被打满了
- 因此对于程序员设计的程序,应该使用IO Threads,即启动多个线程处理IO,并将IO线程和业务逻辑线程分开,防止业务逻辑较慢导致IO线程被阻塞,从而保证更快地搬运数据到用户空间
6 手写RPC代码
6.1 测试类
-
MyRPCTest
package com.bjmashibing.system.rpcdemo; import com.bjmashibing.system.rpcdemo.proxy.MyProxy; import com.bjmashibing.system.rpcdemo.rpc.Dispatcher; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import com.bjmashibing.system.rpcdemo.rpc.transport.MyHttpRpcHandler; import com.bjmashibing.system.rpcdemo.rpc.transport.ServerDecode; import com.bjmashibing.system.rpcdemo.rpc.transport.ServerRequestHandler; import com.bjmashibing.system.rpcdemo.service.*; import com.bjmashibing.system.rpcdemo.util.SerDerUtil; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.*; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.URLConnection; import java.util.concurrent.atomic.AtomicInteger; public class MyRPCTest { //服务端:Netty实现 @Test public void startServer() { MyCar car = new MyCar(); MyFly fly = new MyFly(); //由于传来的MyContent中,放的是接口名,直接通过反射,并不知道应该创建该接口的哪个实现类的实例,所以需要使用Dispatcher存放接口名到具体对象的映射 Dispatcher dis = Dispatcher.getDis(); dis.register(Car.class.getName(), car); dis.register(Fly.class.getName(), fly); //创建20个线程,对应20个selector,但一个client只能绑定到一个selector上,所以如果只有10个client,那么只能绑定到其中10个线程上,那么NioEventLoopGroup中就会剩余10个线程,不处理IO NioEventLoopGroup boss = new NioEventLoopGroup(20); NioEventLoopGroup worker = boss; ServerBootstrap sbs = new ServerBootstrap(); ChannelFuture bind = sbs.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { System.out.println("server accept cliet port: " + ch.remoteAddress().getPort()); ChannelPipeline p = ch.pipeline(); //当使用自定义的RPC协议时,需要通过如下两个handler对客户端传递来的数据进行解码和业务处理 // p.addLast(new ServerDecode()); // p.addLast(new ServerRequestHandler(dis)); //HttpServerCodec、HttpObjectAggregator:Netty内置的handler实现,对读取的数据进行编解码,解决拆包、粘包等问题,通过这两个handler将数据包转为FullHttpRequest,他们之后的handler中msg就是FullHttpRequest了 //其实Tomcat做的就是HttpServerCodec和HttpObjectAggregator的功能,对数据包编解码后将数据包转为Request对象,然后将Request对象传到一个handler类中,这个handler类可以决定将request传给具体Servlet中执行 p.addLast(new HttpServerCodec()); p.addLast(new HttpObjectAggregator(1024 * 512)); //自己定义业务逻辑 p.addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //经过HttpServerCodec和HttpObjectAggregator,此处的msg就是FullHttpRequest对象 FullHttpRequest request = (FullHttpRequest) msg; System.out.println(request.toString()); //得到客户端请求体中的字符序列,其实就是序列化后的MyContent对象 ByteBuf content = request.content(); byte[] data = new byte[content.readableBytes()]; content.readBytes(data); ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(data)); MyContent myContent = (MyContent) oin.readObject(); String serviceName = myContent.getName(); String method = myContent.getMethodName(); //从服务端获取一个注册好的该服务的具体对象 Object c = dis.get(serviceName); Class<?> clazz = c.getClass(); Object res = null; try { Method m = clazz.getMethod(method, myContent.getParameterTypes()); res = m.invoke(c, myContent.getArgs()); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } MyContent resContent = new MyContent(); resContent.setRes(res); byte[] contentByte = SerDerUtil.ser(resContent); //使用Http协议 DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK, Unpooled.copiedBuffer(contentByte)); //当客户端使用Netty方式建立链接,而不使用URLConnection时,需要设置传输出去的数据长度,不然客户端无法解析 response.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentByte.length); //response相当于自定义RPC协议中的header+body ctx.writeAndFlush(response); } }); } }).bind(new InetSocketAddress("localhost", 9090)); try { bind.sync().channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } } //服务端:Jetty实现 //使用Jetty可以创建基于Http协议的服务器,类似Tomcat,中间件如果想内置服务器功能,通常使用Jetty实现 //使用Jetty需要在pom文件中引入jetty-server和jetty-webapp @Test public void startHttpServer() { MyCar car = new MyCar(); MyFly fly = new MyFly(); Dispatcher dis = Dispatcher.getDis(); dis.register(Car.class.getName(), car); dis.register(Fly.class.getName(), fly); //通过Jetty创建服务 Server server = new Server(new InetSocketAddress("localhost", 9090)); ServletContextHandler handler = new ServletContextHandler(server, "/"); server.setHandler(handler); //只要URI为"/*",就会进入MyHttpRpcHandler这个Servlet,addServlet类似web.xml的功能 handler.addServlet(MyHttpRpcHandler.class, "/*"); try { //启动服务 server.start(); //阻塞,防止代码直接退出 server.join(); } catch (Exception e) { e.printStackTrace(); } } //客户端:多线程进行RPC调用 @Test public void get() { AtomicInteger num = new AtomicInteger(0); int size = 50; Thread[] threads = new Thread[size]; for (int i = 0; i < size; i++) { threads[i] = new Thread(() -> { Car car = MyProxy.proxyGet(Car.class);//动态代理实现 //是真的要去触发 RPC调用吗? String arg = "hello" + num.incrementAndGet(); String res = car.ooxx(arg); System.out.println("client over msg: " + res + " src arg: " + arg); }); } for (Thread thread : threads) { thread.start(); } try { System.in.read(); } catch (IOException e) { e.printStackTrace(); } } //客户端:单线程进行RPC调用 @Test public void testRPC() { Car car = MyProxy.proxyGet(Car.class); Persion zhangsan = car.oxox("zhangsan", 16); System.out.println(zhangsan); } //客户端:调用本地对象的方法 @Test public void testRpcLocal() { new Thread(() -> { startServer(); }).start(); System.out.println("server started......"); Car car = MyProxy.proxyGet(Car.class); Persion zhangsan = car.oxox("zhangsan", 16); System.out.println(zhangsan); } }
6.2 proxy层
-
MyProxy
package com.bjmashibing.system.rpcdemo.proxy; import com.bjmashibing.system.rpcdemo.rpc.Dispatcher; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import com.bjmashibing.system.rpcdemo.rpc.transport.ClientFactory; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.concurrent.CompletableFuture; public class MyProxy { public static <T> T proxyGet(Class<T> interfaceInfo) { ClassLoader loader = interfaceInfo.getClassLoader(); Class<?>[] interfaces = {interfaceInfo}; Dispatcher dis = Dispatcher.getDis(); //Proxy.newProxyInstance需要传入3个参数 //1. loader:使用哪个类加载器对代理类进行加载 //2. interfaces:代理类需要实现哪些接口 //3. InvocationHandler:当调用代理对象的方法,最终就相当于调用InvocationHandler的invoke方法 return (T) Proxy.newProxyInstance(loader, interfaces, new InvocationHandler() { //proxy:代理对象本身,method:这个代理对象本次调用的方法,args:本次调用方法传递的参数 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object res = null; //如果本地向Dispatcher注册了Car、Fly的实例,就不必调用远程对象得到结果了 Object o = dis.get(interfaceInfo.getName()); if (o == null) { String name = interfaceInfo.getName(); String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); //MyContent是要传输的具体数据,Myhead是协议头,MyContent为协议体,只传输MyContent是不行的,因为协议中有MyContent的长度,不然服务端无法知道数据包是否传完,以及截取多长作为MyContent MyContent content = new MyContent(); content.setArgs(args); content.setName(name); content.setMethodName(methodName); content.setParameterTypes(parameterTypes); //此处并没有实现通过ZK完成服务的注册发现功能,即可能有多个服务端提供服务,可以任选其一完成远程调用,本例中选择固定服务,localhost 9090 CompletableFuture resF = ClientFactory.transport(content); //CompletableFuture:jdk提供的一个类,其对象的get方法会阻塞,直到调用该对象的complete方法,向该对象中放入返回值。此时get方法解除阻塞,并返回你放入的那个返回值 res = resF.get(); } else { System.out.println("当前为本地方法调用..."); Class<?> clazz = o.getClass(); try { Method m = clazz.getMethod(method.getName(), method.getParameterTypes()); res = m.invoke(o, args); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return res; } }); } }
6.3 rpc.protocol层
-
MyContent:表示协议体
package com.bjmashibing.system.rpcdemo.rpc.protocol; import java.io.Serializable; //必须可序列化,因为需要通过网络传输 public class MyContent implements Serializable { String name; String methodName; Class<?>[] parameterTypes; Object[] args; //返回的数据 Object res; public Object getRes() { return res; } public void setRes(Object res) { this.res = res; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } public Class<?>[] getParameterTypes() { return parameterTypes; } public void setParameterTypes(Class<?>[] parameterTypes) { this.parameterTypes = parameterTypes; } public Object[] getArgs() { return args; } public void setArgs(Object[] args) { this.args = args; } }
-
Myheader:表示协议头
package com.bjmashibing.system.rpcdemo.rpc.protocol; import java.io.Serializable; import java.util.UUID; //必须可序列化,因为需要通过网络传输 public class Myheader implements Serializable { int flag; //用于标记不同请求,表示当前协议是一个有状态的协议 long requestID; //MyContent数据包的长度 long dataLen; public static Myheader createHeader(byte[] msg){ Myheader header = new Myheader(); int size = msg.length; //int占4字节,1字节8bit,1个16进制数占4bit,即0x14占1字节,所以0x14141414正好4字节 int f = 0x14141414; //防止requestID重复,所以使用UUID生成requestID long requestID = Math.abs(UUID.randomUUID().getLeastSignificantBits()); header.setFlag(f); header.setDataLen(size); header.setRequestID(requestID); return header; } public int getFlag() { return flag; } public void setFlag(int flag) { this.flag = flag; } public long getRequestID() { return requestID; } public void setRequestID(long requestID) { this.requestID = requestID; } public long getDataLen() { return dataLen; } public void setDataLen(long dataLen) { this.dataLen = dataLen; } }
6.4 rpc.transport层
-
ClientFactory
package com.bjmashibing.system.rpcdemo.rpc.transport; import com.bjmashibing.system.rpcdemo.util.SerDerUtil; import com.bjmashibing.system.rpcdemo.rpc.ResponseMappingCallback; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import com.bjmashibing.system.rpcdemo.rpc.protocol.Myheader; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.*; import java.io.*; import java.net.*; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; public class ClientFactory { int poolSize = 5; NioEventLoopGroup clientWorker; Random rand = new Random(); private ClientFactory() { } private static final ClientFactory factory; static { factory = new ClientFactory(); } public static ClientFactory getFactory() { return factory; } public static CompletableFuture<Object> transport(MyContent content) { // String type = "rpc"; String type = "http"; CompletableFuture<Object> res = new CompletableFuture<>(); //调用通过自定义RPC协议暴露服务的Provider if (type.equals("rpc")) { byte[] msgBody = SerDerUtil.ser(content); Myheader header = Myheader.createHeader(msgBody); byte[] msgHeader = SerDerUtil.ser(header); // System.out.println("main:::"+ msgHeader.length); NioSocketChannel clientChannel = factory.getClient(new InetSocketAddress("localhost", 9090)); ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(msgHeader.length + msgBody.length); long id = header.getRequestID(); //添加回调 ResponseMappingCallback.addCallBack(id, res); byteBuf.writeBytes(msgHeader); byteBuf.writeBytes(msgBody); //注意,不能使用channelFuture.sync()来完成阻塞,因为该阻塞只能阻塞到发送成功,而不能一直阻塞到服务端返回结果,所以需要使用CompletableFuture的get方法保证阻塞,如果不保证阻塞,那可能没等服务端返回数据,客户端就执行完毕,那么返回的结果就不正确了 ChannelFuture channelFuture = clientChannel.writeAndFlush(byteBuf); } //调用通过http协议暴露服务的Provider else { //1. 使用jdk提供的URL类建立链接,该类包含了Http的编解码、建立Socket链接等功能 //通过Http协议建立链接,由于Http无状态,所以为每个客户端分配一个链接,而没法使用一个链接 // urlTS(content,res); //2. 使用Netty实现 //每个客户端,都会new Bootstrap,然后调用Bootstrap对象的connet方法建立链接,所以是每个客户端新建一个链接 nettyTS(content, res); } return res; } private static void nettyTS(MyContent content, CompletableFuture<Object> res) { //应该定义到外面 NioEventLoopGroup group = new NioEventLoopGroup(1); Bootstrap bs = new Bootstrap(); Bootstrap client = bs.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); //编解码 p.addLast(new HttpClientCodec()) .addLast(new HttpObjectAggregator(1024 * 512)) //处理服务端返回的数据对应的FullHttpResponse .addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { FullHttpResponse response = (FullHttpResponse) msg; System.out.println(response.toString()); ByteBuf resContent = response.content(); byte[] data = new byte[resContent.readableBytes()]; resContent.readBytes(data); ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(data)); MyContent o = (MyContent) oin.readObject(); res.complete(o.getRes()); } }); } }); try { ChannelFuture syncFuture = client.connect("localhost", 9090).sync(); Channel clientChannel = syncFuture.channel(); byte[] data = SerDerUtil.ser(content); DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.POST, "/", Unpooled.copiedBuffer(data) ); //设置长度,否则服务端解析会报错 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, data.length); //客户端使用Http协议向服务端发送数据 clientChannel.writeAndFlush(request).sync(); } catch (InterruptedException e) { e.printStackTrace(); } } private static void urlTS(MyContent content, CompletableFuture<Object> res) { Object obj = null; try { URL url = new URL("http://localhost:9090/"); HttpURLConnection hc = (HttpURLConnection) url.openConnection(); hc.setRequestMethod("POST"); //告诉URL底层通信时,自己的数据包中需要发送请求体 hc.setDoOutput(true); hc.setDoInput(true); //获得IO,注意这个IO是BIO OutputStream out = hc.getOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(out); //此处不真正发送 oout.writeObject(content); //真正发送,并阻塞等待返回数据 if (hc.getResponseCode() == 200) { //拿到返回的输入流 InputStream in = hc.getInputStream(); ObjectInputStream oin = new ObjectInputStream(in); MyContent myContent = (MyContent) oin.readObject(); obj = myContent.getRes(); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } //放入远程返回的结果,并解除阻塞 res.complete(obj); } //为不同ip的Provider,准备不同的链接池,即每个Provider可以建立多个链接 ConcurrentHashMap<InetSocketAddress, ClientPool> outboxs = new ConcurrentHashMap<>(); public NioSocketChannel getClient(InetSocketAddress address) { ClientPool clientPool = outboxs.get(address); //多线程时需要加入,双重判断保证clientPool一定为null才会新建 if (clientPool == null) { synchronized (outboxs) { if (clientPool == null) { outboxs.putIfAbsent(address, new ClientPool(poolSize)); clientPool = outboxs.get(address); } } } //从链接池中随机选择一个链接 int i = rand.nextInt(poolSize); //如果链接已经被初始化,且链接是active状态,那么可以直接返回使用 if (clientPool.clients[i] != null && clientPool.clients[i].isActive()) { return clientPool.clients[i]; } else { //锁住clientPool.lock[i]这个对象,直到代码执行完成,才解锁,不同线程进来,如果也想锁这个对象,会失败 synchronized (clientPool.lock[i]) { //也是双重判断防止多线程下出现问题 if (clientPool.clients[i] == null || !clientPool.clients[i].isActive()) //使用Netty创建链接 clientPool.clients[i] = create(address); } } return clientPool.clients[i]; } private NioSocketChannel create(InetSocketAddress address) { //基于Netty创建链接 clientWorker = new NioEventLoopGroup(1); Bootstrap bs = new Bootstrap(); ChannelFuture connect = bs.group(clientWorker) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); //接收到数据后的处理 //ServerDecode:用于将read到的内容,转换成Packmsg放入集合 //ClientResponses:用于依次处理集合中的Packmsg p.addLast(new ServerDecode()); p.addLast(new ClientResponses()); } }).connect(address); try { NioSocketChannel client = (NioSocketChannel) connect.sync().channel(); return client; } catch (InterruptedException e) { e.printStackTrace(); } return null; } }
-
ClientPool
package com.bjmashibing.system.rpcdemo.rpc.transport; import io.netty.channel.socket.nio.NioSocketChannel; public class ClientPool{ NioSocketChannel[] clients; Object[] lock; ClientPool(int size){ //初始化,链接还是null clients = new NioSocketChannel[size]; //锁,同一时间不能有两个客户端创建对应索引的链接 lock = new Object[size]; for(int i = 0;i< size;i++){ lock[i] = new Object(); } } }
-
ClientResponses
package com.bjmashibing.system.rpcdemo.rpc.transport; import com.bjmashibing.system.rpcdemo.util.Packmsg; import com.bjmashibing.system.rpcdemo.rpc.ResponseMappingCallback; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; public class ClientResponses extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //由于先使用了解码器,所以msg此时类型为Packmsg Packmsg responsepkg = (Packmsg) msg; //执行回调,解除阻塞 ResponseMappingCallback.runCallBack(responsepkg); } }
-
MyHttpRpcHandler
package com.bjmashibing.system.rpcdemo.rpc.transport; import com.bjmashibing.system.rpcdemo.rpc.Dispatcher; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class MyHttpRpcHandler extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletInputStream in = req.getInputStream(); ObjectInputStream oin = new ObjectInputStream(in); try { MyContent myContent = (MyContent) oin.readObject(); String serviceName = myContent.getName(); String method = myContent.getMethodName(); Object c = Dispatcher.getDis().get(serviceName); Class<?> clazz = c.getClass(); Object res = null; try { Method m = clazz.getMethod(method, myContent.getParameterTypes()); res = m.invoke(c, myContent.getArgs()); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } MyContent resContent = new MyContent(); resContent.setRes(res); if (res == null) { System.out.println("完蛋了。。。"); } // byte[] contentByte = SerDerUtil.ser(resContent); ServletOutputStream out = resp.getOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(out); oout.writeObject(resContent); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
-
ServerDecode
package com.bjmashibing.system.rpcdemo.rpc.transport; import com.bjmashibing.system.rpcdemo.util.Packmsg; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import com.bjmashibing.system.rpcdemo.rpc.protocol.Myheader; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.util.List; public class ServerDecode extends ByteToMessageDecoder { //ByteToMessageDecoder类中,在channelRead中,替你解决了拆包的问题,其内逻辑为:将上一次剩余的ByteBuf拼接到本次的ByteBuf上,然后调用decode代码,最后将decode中剩余的ByteBuf,留存起来,用于拼接到下一次的ByteBuf上 @Override protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception { //Myheader为122位长,大于等于122表示ByteBuf中一定包含一个完整的Myheader,该长度取决于你序列化的Myhead对象,当Myhead处于不同包下时,该值可能不同,所以开始时,应该先将Myhead长度打印,来确定该值到底应为多少 //此处需要使用while处理,因为本次ByteBuf中,可能包含多个Myhead+MyContent,且只有当ByteBuf中包含一整个Myhead+MyContent时,才应该移动ByteBuf中的读指针,否则应将ByteBuf记录下来和下一次的ByteBuf合并 while (buf.readableBytes() >= 122) { byte[] bytes = new byte[122]; //获得Myheader,这样就能通过header.getDataLen得到MyContent的长度 //此处不移动ByteBuf的读指针 buf.getBytes(buf.readerIndex(), bytes); ByteArrayInputStream in = new ByteArrayInputStream(bytes); ObjectInputStream oin = new ObjectInputStream(in); Myheader header = (Myheader) oin.readObject(); //如果ByteBuf-Myheader长度,还超过MyContent的长度,说明ByteBuf中包含一个完整的Myheader+MyContent if (buf.readableBytes() - 122 >= header.getDataLen()) { //此时就可以将指针移动到MyContent开始的位置 buf.readBytes(122); byte[] data = new byte[(int) header.getDataLen()]; buf.readBytes(data); ByteArrayInputStream din = new ByteArrayInputStream(data); ObjectInputStream doin = new ObjectInputStream(din); //flag为0x14141414表示在处理客户端发来的数据,为0x14141424表示正处理服务端发来的数据,对于当前代码,处理客户端或服务端发来的数据并没有区别 if (header.getFlag() == 0x14141414) { MyContent content = (MyContent) doin.readObject(); //Packmsg:对Myhead+MyContent的进一步封装,方便后续放入out集合 out.add(new Packmsg(header, content)); } else if (header.getFlag() == 0x14141424) { MyContent content = (MyContent) doin.readObject(); out.add(new Packmsg(header, content)); } } else { break; } } } }
-
ServerRequestHandler
package com.bjmashibing.system.rpcdemo.rpc.transport; import com.bjmashibing.system.rpcdemo.util.Packmsg; import com.bjmashibing.system.rpcdemo.util.SerDerUtil; import com.bjmashibing.system.rpcdemo.rpc.Dispatcher; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import com.bjmashibing.system.rpcdemo.rpc.protocol.Myheader; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; //服务端收到数据后,调用Dispatcher中对象的指定方法,并返回给客户端 public class ServerRequestHandler extends ChannelInboundHandlerAdapter { Dispatcher dis; public ServerRequestHandler(Dispatcher dis) { this.dis = dis; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //先调用了ServerDecode这个handler,所以此处的msg已经变为了放入ServerDecode中的out集合中的Packmsg Packmsg requestPkg = (Packmsg) msg; //此处可以有三种方式处理业务逻辑 //1. 直接在当前线程,当前方法中编写业务逻辑:但由于业务逻辑和IO在一个线程中处理,所以业务逻辑阻塞,会导致IO也被阻塞 //2. 自己创建线程池,并从中选取一个线程处理业务逻辑:会导致没有充分利用NioEventLoopGroup中的线程,可能NioEventLoopGroup中还有线程空闲,但自己又建立了一些线程处理业务逻辑 //3. 使用Netty自己的eventloop中的线程来处理业务逻辑 //a. 写法一:ctx.executor().execute:这相当于将处理业务逻辑的代码,放入了当前线程的任务队列,之后其实还是当前线程处理该逻辑,也就是说,如果该业务逻辑阻塞,还是会导致IO阻塞 //b. 写法二:ctx.executor().parent().next().execute:从NioEventLoopGroup线程组中,选取下一个线程对业务逻辑进行处理,其实就是将业务逻辑放到其他并不常使用的IO线程中执行,而不在当前IO线程中执行,假设NioEventLoopGroup中有20个线程,但Client只有1个,由于1个Client只能绑定到一个Selector,所以只能用到NioEventLoopGroup中的一个线程,此时使用写法二,就能充分地利用剩下不处理IO的那19个线程,对业务逻辑进行处理 ctx.executor().execute(new Runnable() { // ctx.executor().parent().next().execute(new Runnable() { @Override public void run() { String serviceName = requestPkg.getContent().getName(); String method = requestPkg.getContent().getMethodName(); Object c = dis.get(serviceName); Class<?> clazz = c.getClass(); Object res = null; try { Method m = clazz.getMethod(method, requestPkg.getContent().getParameterTypes()); res = m.invoke(c, requestPkg.getContent().getArgs()); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } // String execThreadName = Thread.currentThread().getName(); MyContent content = new MyContent(); // String s = "io thread: " + ioThreadName + " exec thread: " + execThreadName + " from args:" + requestPkg.content.getArgs()[0]; content.setRes(res); byte[] contentByte = SerDerUtil.ser(content); Myheader resHeader = new Myheader(); //返回的requestID和接收的requestID应该一样 resHeader.setRequestID(requestPkg.getHeader().getRequestID()); //返回的头是0x14141424,不再是0x14141414 resHeader.setFlag(0x14141424); resHeader.setDataLen(contentByte.length); byte[] headerByte = SerDerUtil.ser(resHeader); ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(headerByte.length + contentByte.length); byteBuf.writeBytes(headerByte); byteBuf.writeBytes(contentByte); ctx.writeAndFlush(byteBuf); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("client close"); } }
6.5 rpc层
-
Dispatcher
package com.bjmashibing.system.rpcdemo.rpc; import java.util.concurrent.ConcurrentHashMap; public class Dispatcher { private static Dispatcher dis = null; static { dis = new Dispatcher(); } public static Dispatcher getDis() { return dis; } private Dispatcher() { } public static ConcurrentHashMap<String, Object> invokeMap = new ConcurrentHashMap<>(); public void register(String k, Object obj) { invokeMap.put(k, obj); } public Object get(String k) { return invokeMap.get(k); } }
-
ResponseMappingCallback
package com.bjmashibing.system.rpcdemo.rpc; import com.bjmashibing.system.rpcdemo.util.Packmsg; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; public class ResponseMappingCallback { static ConcurrentHashMap<Long, CompletableFuture> mapping = new ConcurrentHashMap<>(); public static void addCallBack(long requestID, CompletableFuture cb) { mapping.putIfAbsent(requestID, cb); } //为指定的requestID对应的客户端线程提供返回值,让其不再阻塞 public static void runCallBack(Packmsg msg) { CompletableFuture cf = mapping.get(msg.getHeader().getRequestID()); cf.complete(msg.getContent().getRes()); removeCB(msg.getHeader().getRequestID()); } private static void removeCB(long requestID) { mapping.remove(requestID); } }
6.6 service层
-
Car
package com.bjmashibing.system.rpcdemo.service; public interface Car{ public String ooxx(String msg); public Persion oxox(String name,Integer age); }
-
Fly
package com.bjmashibing.system.rpcdemo.service; public interface Fly{ void xxoo(String msg); }
-
MyCar
package com.bjmashibing.system.rpcdemo.service; public class MyCar implements Car { @Override public String ooxx(String msg) { return "server res "+ msg; } @Override public Persion oxox(String name, Integer age) { Persion p = new Persion(); p.setName(name); p.setAge(age); return p; } }
-
MyFly
package com.bjmashibing.system.rpcdemo.service; public class MyFly implements Fly { @Override public void xxoo(String msg) { System.out.println("server,get client arg:" + msg); } }
-
Person
package com.bjmashibing.system.rpcdemo.service; import java.io.Serializable; public class Persion implements Serializable { String name; Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return name + " " + age; } }
6.7 util层
-
Packmsg:封装数据包
package com.bjmashibing.system.rpcdemo.util; import com.bjmashibing.system.rpcdemo.rpc.protocol.MyContent; import com.bjmashibing.system.rpcdemo.rpc.protocol.Myheader; public class Packmsg { Myheader header; MyContent content; public Myheader getHeader() { return header; } public void setHeader(Myheader header) { this.header = header; } public MyContent getContent() { return content; } public void setContent(MyContent content) { this.content = content; } public Packmsg(Myheader header, MyContent content) { this.header = header; this.content = content; } }
-
SerDerUtil:将对象序列化后转为字节数组
package com.bjmashibing.system.rpcdemo.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class SerDerUtil { //只创建一个ByteArrayOutputStream对象,可以重复利用,也因此ser方法必须加同步进行处理 static ByteArrayOutputStream out = new ByteArrayOutputStream(); public synchronized static byte[] ser(Object msg) { out.reset(); ObjectOutputStream oout = null; byte[] msgBody = null; try { oout = new ObjectOutputStream(out); oout.writeObject(msg); msgBody = out.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return msgBody; } }
7 EventLoopGroup
- EventLoopGroup相当于Selector+线程池
- 一个EventLoop对应一个线程,该线程中的处理分为3步
- select
- processSelectedKeys:此处使用当前线程,依次执行pipeline中的handler,但handler中,可以直接在当前线程处理,也可以将要处理的内容放入当前线程或其他线程的任务队列中,等处理任务队列那步再进行处理
- 处理任务队列
- 如果我们建立的NioEventLoopGroup有20个线程,但只有10个client,由于1个client最多注册到一个selector,因此最多会使用10个线程处理IO,此时我们可以将handler中要处理的内容,放入其他线程的任务队列中,由那些不处理IO的线程处理业务逻辑,这样就能充分的利用系统资源
8 ServerBootstrap
- 当使用同一个ServerBootstrap对象可以bind多个ip端口,那么两个端口用的是一套handler逻辑,只不过丰富了暴露的端口号而已
- 当使用不同ServerBootstrap对象时bind多个ip端口,那么两个端口号用的是各自的handler逻辑。且两个ServerBootstrap对象可以使用不同的boss和worker,只不过比较浪费资源而已
9 TODO和FIXME注释
- 当在IDE中使用//TODO或//FIXME开头进行注释时,IDE中会高亮显示此注释,且在右边栏中标记为蓝色小条,这样就方便后续你找到这个注释所在的位置
- 当前先不进行实现,后续再对其进行实现的内容,使用//TODO注释
- 代码错误,但准备之后再进行修复的内容,使用//FIXME注释
10 mmap
- 网卡是字符设备,硬盘是块设备,只有块设备可以做mmap映射,而字符设备不行,即Socket无法做mmap映射,所以从内核搬运到用户空间这步无法省略
11 selectedKeys中调用remove
- select:从内核将有事件的fd集合放入jvm直接空间(内核),每次会清空重放
- selectedKeys:从内核放入jvm堆中,并不是每次返回一个新集合,而是将新的select得到的内容放入,历史的不清除
- 所以每次需要对处理完的SelectionKey,从selectedKeys集合中删除