本篇文章主要讲解Client(客户端)如何连接到DDPush,并向DDPush发送消息(主要是心跳包和确认信息),和如何接收APPServer推送给DDPush的消息,本篇文章分析官方推荐的UDP工作模式。
UDP模式主要涉及到以下几个重要的类:
1、UdpConnector 监听端口的UDP数据包
2、Receiver 接受终端消息
3、Sender 向终端发送消息
4、Messenger 从Receiver的消息队列中取出消息, 再从内存中查找对应的状态机以及需要发送给终端的消息, 最后加入到Sender的消息队列并由Sender发出去
一、UdpConnector
关键方法start()
public void start() throws Exception{ if(antenna != null){ throw new Exception("antenna is not null, may have run before"); } antenna = DatagramChannel.open(); antenna.socket().bind(new InetSocketAddress(port)); System.out.println("udp connector port:"+port); //non-blocking antenna.configureBlocking(false); antenna.socket().setReceiveBufferSize(1024*1024*PropertyUtil.getPropertyInt("CLIENT_UDP_BUFFER_RECEIVE")); antenna.socket().setSendBufferSize(1024*1024*PropertyUtil.getPropertyInt("CLIENT_UDP_BUFFER_SEND")); System.out.println("udp connector recv buffer size:"+antenna.socket().getReceiveBufferSize()); System.out.println("udp connector send buffer size:"+antenna.socket().getSendBufferSize()); this.receiver = new Receiver(antenna); this.receiver.init(); this.sender = new Sender(antenna); this.sender.init(); this.senderThread = new Thread(sender,"AsynUdpConnector-sender"); this.receiverThread = new Thread(receiver,"AsynUdpConnector-receiver"); this.receiverThread.start(); this.senderThread.start(); }第6行:绑定一个端口端口,监听此端口的数据包
第16行:receiver是一个runnable对象,用来接受Client发来的消息
第19行:sender是一个runnable对象,用来向Client推送消息
第21-24行:分别启动receiver和sender
二、Receiver
第一步:run()方法
public void run(){ while(!this.stoped){ try{ //synchronized(enQueSignal){ processMessage(); // if(mq.isEmpty() == true){ // enQueSignal.wait(); // } //} }catch(Exception e){ e.printStackTrace(); }catch(Throwable t){ t.printStackTrace(); } } }这是一个while循环,用来不断的接收消息,主要看第5行的processMessage()方法
protected void processMessage() throws Exception{ address = null; buffer.clear(); try{ address = this.channel.receive(buffer); }catch(SocketTimeoutException timeout){ } if(address == null){ try{ Thread.sleep(1); }catch(Exception e){ } return; } buffer.flip(); byte[] swap = new byte[buffer.limit() - buffer.position()]; System.arraycopy(buffer.array(), buffer.position(), swap, 0, swap.length); ClientMessage m = new ClientMessage(address,swap); enqueue(m); //System.out.println(DateTimeUtil.getCurDateTime()+" r:"+StringUtil.convert(m.getData())+" from:"+m.getSocketAddress().toString()); }第5行:接收数据填充到buffer,因为DatagramChannel设置成非阻塞了,所以此方法不管有无数据都会立即返回,所以第9行会做一个判断
第19-20:将buffer缓冲区的数据拷贝到swap数据
第22行:将byte[]数组和Client的address信息封装成ClientMessage对象
第24行:将ClientMessage对象加入到消息队列,等待处理(Messenger会从此队列取消息)
三、Sender
第一步:run()方法
public void run(){ while(!this.stoped){ try{ synchronized(enQueSignal){ while(mq.isEmpty() == true && stoped == false){ try{ enQueSignal.wait(1); }catch(InterruptedException e){ } //System.out.println("sender wake up"); } processMessage(); } }catch(Exception e){ e.printStackTrace(); }catch(Throwable t){ t.printStackTrace(); } } }这是一个while循环,用来不断的发送消息,主要看第13行的processMessage()方法
第二步:processMessage()方法
protected void processMessage() throws Exception{ buffer.clear(); ServerMessage pendingMessage = dequeue(); if(pendingMessage == null){ //Thread.yield(); return; } buffer.put(pendingMessage.getData()); buffer.flip(); channel.send(buffer, pendingMessage.getSocketAddress()); //System.out.println(DateTimeUtil.getCurDateTime()+" s:"+StringUtil.convert(pendingMessage.getData())+" to :"+pendingMessage.getSocketAddress().toString()); }第3行:从消息发送队列取出一条消息,ServerMessage对象主要包括消息内容和Client地址信息两个属性
第8行:将消息内容放到缓冲区
第10行:将消息内容发送给Client
那么问题来了,Receiver中加入队列的ClientMessage怎么转为ServerMessage并加入到Sender的消息发送队列的呢?这就涉及另外一个重要的角色Messenger
四、Messenger
这也是一个实现Runnable接口的对象,所以首先找到run()方法
第一步:run()方法
@Override public void run() { this.started = true; while(stoped == false){ try{ procMessage(); }catch(Exception e){ e.printStackTrace(); }catch(Throwable t){ t.printStackTrace(); } } }这是一个while循环,用来不断的发送消息,主要看第7行的procMessage()方法
第二步:procMessage()方法
private void procMessage() throws Exception{ ClientMessage m = this.obtainMessage(); if(m == null){ try{ Thread.sleep(5); }catch(Exception e){ ; } return; } // 对终端发布消息 this.deliverMessage(m); }第2行:从队列取出一条Client发过来的消息,ClientMessage是不是很眼熟?我们看下this.obtainMessage()这个方法 private ClientMessage obtainMessage() throws Exception{
什么?它又调用了receiver的receiver()方法,好吧,绕了半天,原来最终是从receiver的消息队列中取得消息的。
第12行:处理Client发来的消息,那么我们具体看下deliverMessage(m)这个方法
第三步、deliverMessage(ClientMessage m)方法
private void deliverMessage(ClientMessage m) throws Exception{ //System.out.println(this.hostThread.getName()+" receive:"+StringUtil.convert(m.getData())); //System.out.println(m.getSocketAddress().getClass().getName()); String uuid = m.getUuidHexString(); //ClientStatMachine csm = NodeStatus.getInstance().getClientStat(uuid); ClientStatMachine csm = nodeStat.getClientStat(uuid); // 查找内存中的状态机 if(csm == null){// csm = ClientStatMachine.newByClientTick(m); // 创建状态机 if(csm == null){ return; } nodeStat.putClientStat(uuid, csm); } // 查找是否有消息发送给终端 ArrayList<ServerMessage> smList = csm.onClientMessage(m); if(smList == null){ return; } for(int i = 0; i < smList.size(); i++){ ServerMessage sm = smList.get(i); if(sm.getSocketAddress() == null)continue; this.connector.send(sm); } }第4行:是一个标准的UUID,用来唯一标识每一个用户
第6行:根据UUID去内存中查找有没有状态机csm,可以理解为一个在线用户对象
第7-13行:内存中没有该用户则创建一个并保存
第15行:根据Client发送过来的消息,判断需不需要回复、是否有离线的消息需要发给此用户,消息封装为ServerMessage并加入到集合,因为可能会有多条消息需要下发
第22行:将ServerMessage推送给客户端,看到这里,我们不禁有疑问,Sender不是用来给Client推送消息的吗?我们看下this.connector.send(sm);这行代码执行了什么,它是调用UdpConnector对象的send(ServerMessage message)方法,让我们看下这个方法
public boolean send(ServerMessage message) throws Exception { return sender.send(message); }它又调用了Sender对象的send(ServerMessage message)方法,继续跟踪下去
public boolean send(ServerMessage message){ return enqueue(message); }看到这里,我们应该能明白了,这个方法的作用是将需要推送的ServerMessage对象加入到Sender的消息队列,Sender的run()方法会从队列去取消息再推送给客户端。
到此,我们应该明白了UDP模式Client与DDPush的交互流程。好了,本篇文章的讲解也就到此结束了,如有问题,欢迎大家指正!