Nacos源码系列之UDP(三)

一、UDP

在这里插入图片描述

User Datagram Protocol 用户数据协议,属于传输层的一种协议。无需建立连接就可以发送封装的 IP 数据包,不对传送数据包进行可靠性保证,即无法得知报文是否安全完整到达。

优势:
具有较好的实时性,工作效率较高;
网络开销小。

劣势:
无法保证可靠性、顺序性等

二、Java实现UDP

public class Send {

    public static void main(String[] args) {
        byte[] data="hello world".getBytes();
        try {
            DatagramPacket datagramPacket=new DatagramPacket(data,data.length,
                    InetAddress.getByName("127.0.0.1"),6000);
            DatagramSocket socket=new DatagramSocket();
            socket.send(datagramPacket);
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class Receive {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket=new DatagramSocket(6000);
        byte[] data=new byte[1024];
        DatagramPacket datagramPacket=new DatagramPacket(data,data.length);
        socket.receive(datagramPacket);
        System.out.println(new String(data,0,datagramPacket.getLength())+" ip:"
                +datagramPacket.getAddress().getHostAddress()+" port:"+datagramPacket.getPort());
        socket.close();

    }
}

测试:

注意:先执行receive,然后等待send消息发送。

三、nacos中的主要UDP通信

在Nacos Naming中,使用http/udp的推送模型。由于UDP无ack,所以client要轮询查询保证数据一致性。

在这里插入图片描述
Nacos Naming的UDP通知可以分为以下几个主要流程。
在这里插入图片描述

1、Client上报ip+port(http)

Nacos Client在服务发现时,会通过NamingHttpClientProxy调用Nacos Naming的/instance/list接口,传入ip、udpPort。该接口有v1、v2版本。这里以v1为例。位置:com.alibaba.nacos.naming.controllers.InstanceController。



 //InstanceController
 
 @GetMapping("/list")
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
    public Object list(HttpServletRequest request) throws Exception {
    ...
       String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
       int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
    ...
    Subscriber subscriber = new Subscriber(clientIP + ":" + udpPort, agent, app, clientIP, namespaceId, serviceName,
                udpPort, clusters);
        return getInstanceOperator().listInstance(namespaceId, serviceName, subscriber, clusters, healthyOnly);
    
    }
    
    
  public ServiceInfo listInstance(String namespaceId, String serviceName, Subscriber subscriber, String cluster,
            boolean healthOnly) throws Exception {
            
   ...
   if (subscriber.getPort() > 0 && pushService.canEnablePush(subscriber.getAgent())) {
                subscriberServiceV1.addClient(namespaceId, serviceName, cluster, subscriber.getAgent(),
                        new InetSocketAddress(clientIP, subscriber.getPort()), pushDataSource, StringUtils.EMPTY,
                        StringUtils.EMPTY);
                cacheMillis = switchDomain.getPushCacheMillis(serviceName);
            }
   ...
}


    public void addClient(String namespaceId, String serviceName, String clusters, String agent,
            InetSocketAddress socketAddr, DataSource dataSource, String tenant, String app) {
        
        PushClient client = new PushClient(namespaceId, serviceName, clusters, agent, socketAddr, dataSource, tenant,
                app);
        addClient(client);
    }
    
    
    
 private final ConcurrentMap<String, ConcurrentMap<String, PushClient>> clientMap = new ConcurrentHashMap<>();

    
     public void addClient(PushClient client) {
        // client is stored by key 'serviceName' because notify event is driven by serviceName change
        String serviceKey = UtilsAndCommons.assembleFullServiceName(client.getNamespaceId(), client.getServiceName());
        ConcurrentMap<String, PushClient> clients = clientMap.get(serviceKey);
        if (clients == null) {
            clientMap.putIfAbsent(serviceKey, new ConcurrentHashMap<>(1024));
            clients = clientMap.get(serviceKey);
        }
        
        PushClient oldClient = clients.get(client.toString());
        if (oldClient != null) {
            oldClient.refresh();
        } else {
            PushClient res = clients.putIfAbsent(client.toString(), client);
            if (res != null) {
                Loggers.PUSH.warn("client: {} already associated with key {}", res.getAddrStr(), res);
            }
            Loggers.PUSH.debug("client: {} added for serviceName: {}", client.getAddrStr(), client.getServiceName());
        }
    }
   

2、Naming通知服务实例变化

事件发布:

当服务实例发生变化后,会添加task。
位置:com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl.Notifier。
notifier.addTask(key, DataOperation.CHANGE);

最终会进入到
com.alibaba.nacos.naming.core.Service#onChange




public void onChange(String key, Instances value) throws Exception {
 ...
     updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));
...
}

public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
...

        getPushService().serviceChanged(this);
...
  }

重点来了!最后会来到UdpPushService的serviceChanged()。UdpPushService实现了ApplicationListener,Spring提供的发布订阅模型实现,最终调用publishEvent()将事件发布出去。

public class UdpPushService implements ApplicationContextAware, ApplicationListener<ServiceChangeEvent> {

    public void serviceChanged(Service service) {
        this.applicationContext.publishEvent(new ServiceChangeEvent(this, service));
    }
  }
事件订阅:

com.alibaba.nacos.naming.push.UdpPushService

 @Override
    public void onApplicationEvent(ServiceChangeEvent event) {
        // If upgrade to 2.0.X, do not push for v1.
        if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
            return;
        }
        Service service = event.getService();
        String serviceName = service.getName();
        String namespaceId = service.getNamespaceId();
        //merge some change events to reduce the push frequency:
        if (futureMap.containsKey(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName))) {
            return;
        }
        Future future = GlobalExecutor.scheduleUdpSender(() -> {
            try {
                Loggers.PUSH.info(serviceName + " is changed, add it to push queue.");
                ConcurrentMap<String, PushClient> clients = subscriberServiceV1.getClientMap()
                        .get(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
                if (MapUtils.isEmpty(clients)) {
                    return;
                }
                
                Map<String, Object> cache = new HashMap<>(16);
                long lastRefTime = System.nanoTime();
                for (PushClient client : clients.values()) {
                    if (client.zombie()) {
                        Loggers.PUSH.debug("client is zombie: " + client);
                        clients.remove(client.toString());
                        Loggers.PUSH.debug("client is zombie: " + client);
                        continue;
                    }
                    
                    AckEntry ackEntry;
                    Loggers.PUSH.debug("push serviceName: {} to client: {}", serviceName, client);
                    String key = getPushCacheKey(serviceName, client.getIp(), client.getAgent());
                    byte[] compressData = null;
                    Map<String, Object> data = null;
                    if (switchDomain.getDefaultPushCacheMillis() >= 20000 && cache.containsKey(key)) {
                        org.javatuples.Pair pair = (org.javatuples.Pair) cache.get(key);
                        compressData = (byte[]) (pair.getValue0());
                        data = (Map<String, Object>) pair.getValue1();
                        
                        Loggers.PUSH.debug("[PUSH-CACHE] cache hit: {}:{}", serviceName, client.getAddrStr());
                    }
                    
                    if (compressData != null) {
                        ackEntry = prepareAckEntry(client, compressData, data, lastRefTime);
                    } else {
                        ackEntry = prepareAckEntry(client, prepareHostsData(client), lastRefTime);
                        if (ackEntry != null) {
                            cache.put(key,
                                    new org.javatuples.Pair<>(ackEntry.getOrigin().getData(), ackEntry.getData()));
                        }
                    }
                    
                    Loggers.PUSH.info("serviceName: {} changed, schedule push for: {}, agent: {}, key: {}",
                            client.getServiceName(), client.getAddrStr(), client.getAgent(),
                            (ackEntry == null ? null : ackEntry.getKey()));
                    
                    udpPush(ackEntry);
                }
            } catch (Exception e) {
                Loggers.PUSH.error("[NACOS-PUSH] failed to push serviceName: {} to client, error: {}", serviceName, e);
                
            } finally {
                futureMap.remove(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
            }
            
        }, 1000, TimeUnit.MILLISECONDS);
        
        futureMap.put(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName), future);
        
    }
    
     
     //push方法
    private static AckEntry udpPush(AckEntry ackEntry) {
        if (ackEntry == null) {
            Loggers.PUSH.error("[NACOS-PUSH] ackEntry is null.");
            return null;
        }
        
        if (ackEntry.getRetryTimes() > Constants.UDP_MAX_RETRY_TIMES) {
            Loggers.PUSH.warn("max re-push times reached, retry times {}, key: {}", ackEntry.getRetryTimes(),
                    ackEntry.getKey());
            ackMap.remove(ackEntry.getKey());
            udpSendTimeMap.remove(ackEntry.getKey());
            MetricsMonitor.incrementFailPush();
            return ackEntry;
        }
        
        try {
            if (!ackMap.containsKey(ackEntry.getKey())) {
                MetricsMonitor.incrementPush();
            }
            ackMap.put(ackEntry.getKey(), ackEntry);
            udpSendTimeMap.put(ackEntry.getKey(), System.currentTimeMillis());
            
            Loggers.PUSH.info("send udp packet: " + ackEntry.getKey());
            udpSocket.send(ackEntry.getOrigin());
            
            ackEntry.increaseRetryTime();
            
            GlobalExecutor.scheduleRetransmitter(new Retransmitter(ackEntry),
                    TimeUnit.NANOSECONDS.toMillis(Constants.ACK_TIMEOUT_NANOS), TimeUnit.MILLISECONDS);
            
            return ackEntry;
        } catch (Exception e) {
            Loggers.PUSH.error("[NACOS-PUSH] failed to push data: {} to client: {}, error: {}", ackEntry.getData(),
                    ackEntry.getOrigin().getAddress().getHostAddress(), e);
            ackMap.remove(ackEntry.getKey());
            udpSendTimeMap.remove(ackEntry.getKey());
            MetricsMonitor.incrementFailPush();
            
            return null;
        }
    }

3、Client接收服务实例变化,并发送ack

Client的接收可以从单元测试入手,位置:com.alibaba.nacos.client.naming.core.PushReceiverTest

@Test 
public void testTestRunWithDump() throws InterruptedException, IOException {
        ServiceInfoHolder holder = Mockito.mock(ServiceInfoHolder.class);
        final PushReceiver pushReceiver = new PushReceiver(holder);
        final ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                pushReceiver.run();
            }
        });
        TimeUnit.MILLISECONDS.sleep(10);
        
        PushReceiver.PushPacket pack1 = new PushReceiver.PushPacket();
        pack1.type = "dump";
        pack1.data = "pack1";
        pack1.lastRefTime = 1;
        final String res1 = udpClientRun(pack1, pushReceiver);
        Assert.assertEquals("{\"type\": \"dump-ack\", \"lastRefTime\": \"1\", \"data\":\"{}\"}", res1);
        verify(holder, times(1)).getServiceInfoMap();
        
    }

private String udpClientRun(PushReceiver.PushPacket pack, PushReceiver pushReceiver) throws IOException {
    final int udpPort = pushReceiver.getUdpPort();
    String json = JacksonUtils.toJson(pack);
    final byte[] bytes = IoUtils.tryCompress(json, "UTF-8");
    final DatagramSocket datagramSocket = new DatagramSocket();
    datagramSocket.send(new DatagramPacket(bytes, bytes.length, InetAddress.getByName("localhost"), udpPort));
    byte[] buffer = new byte[20480];
    final DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length);
    datagramSocket.receive(datagramPacket);
    final byte[] data = datagramPacket.getData();
    String res = new String(data, StandardCharsets.UTF_8);
    return res.trim();
}

上面涉及到一个核心处理类PushReceiver。

//init
   public PushReceiver(ServiceInfoHolder serviceInfoHolder) {
        try {
            this.serviceInfoHolder = serviceInfoHolder;
            String udpPort = getPushReceiverUdpPort();
            if (StringUtils.isEmpty(udpPort)) {
                this.udpSocket = new DatagramSocket();
            } else {
                //如果有udpPort,创建ip+port的socket
                this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort)));
            }
            this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setDaemon(true);
                    thread.setName("com.alibaba.nacos.naming.push.receiver");
                    return thread;
                }
            });
            
            this.executorService.execute(this);
        } catch (Exception e) {
            NAMING_LOGGER.error("[NA] init udp socket failed", e);
        }
    }

//run()
    @Override
    public void run() {
        while (!closed) {
            try {
                
                // byte[] is initialized with 0 full filled by default
                byte[] buffer = new byte[UDP_MSS];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                
                //socket接收数据包
                udpSocket.receive(packet);
                
                String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
                NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
                //转换成entity
                PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
                String ack;
                //根据type组装ack消息
                if (PUSH_PACKAGE_TYPE_DOM.equals(pushPacket.type) || PUSH_PACKAGE_TYPE_SERVICE.equals(pushPacket.type)) {
                    serviceInfoHolder.processServiceInfo(pushPacket.data);
                    
                    // send ack to server
                    ack = "{\"type\": \"push-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":"
                            + "\"\"}";
                } else if (PUSH_PACKAGE_TYPE_DUMP.equals(pushPacket.type)) {
                    // dump data to server
                    ack = "{\"type\": \"dump-ack\"" + ", \"lastRefTime\": \"" + pushPacket.lastRefTime + "\", \"data\":"
                            + "\"" + StringUtils.escapeJavaScript(JacksonUtils.toJson(serviceInfoHolder.getServiceInfoMap()))
                            + "\"}";
                } else {
                    // do nothing send ack only
                    ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
                            + "\", \"data\":" + "\"\"}";
                }
                
                //将ack发送出去
                udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,
                        packet.getSocketAddress()));
            } catch (Exception e) {
                if (closed) {
                    return;
                }
                NAMING_LOGGER.error("[NA] error while receiving push data", e);
            }
        }
    }

而在实际项目中,客户端通过NamingHttpClientProxy的构造方法,初始化PushReceiver。

   public NamingHttpClientProxy(String namespaceId, SecurityProxy securityProxy, ServerListManager serverListManager,
            Properties properties, ServiceInfoHolder serviceInfoHolder) {
        super(securityProxy, properties);
        this.serverListManager = serverListManager;
        this.setServerPort(DEFAULT_SERVER_PORT);
        this.namespaceId = namespaceId;
        this.beatReactor = new BeatReactor(this, properties);
        this.pushReceiver = new PushReceiver(serviceInfoHolder);
        this.maxRetry = ConvertUtils.toInt(properties.getProperty(PropertyKeyConst.NAMING_REQUEST_DOMAIN_RETRY_COUNT,
                String.valueOf(UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT)));
    }

4、Naming接收ack

初始化了udpsocket和处理线程,在Receiver中接收udp数据包。

    static {
        try {
            udpSocket = new DatagramSocket();
            
            Receiver receiver = new Receiver();
            
            Thread inThread = new Thread(receiver);
            inThread.setDaemon(true);
            inThread.setName("com.alibaba.nacos.naming.push.receiver");
            inThread.start();
            
        } catch (SocketException e) {
            Loggers.SRV_LOG.error("[NACOS-PUSH] failed to init push service");
        }
    }

  public static class Receiver implements Runnable {
        
        @Override
        public void run() {
            while (true) {
                byte[] buffer = new byte[1024 * 64];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                
                try {
                    udpSocket.receive(packet);
                    
                    String json = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8).trim();
                    AckPacket ackPacket = JacksonUtils.toObj(json, AckPacket.class);
                    
                    InetSocketAddress socketAddress = (InetSocketAddress) packet.getSocketAddress();
                    String ip = socketAddress.getAddress().getHostAddress();
                    int port = socketAddress.getPort();
                    
                    if (System.nanoTime() - ackPacket.lastRefTime > Constants.ACK_TIMEOUT_NANOS) {
                        Loggers.PUSH.warn("ack takes too long from {} ack json: {}", packet.getSocketAddress(), json);
                    }
                    
                    String ackKey = AckEntry.getAckKey(ip, port, ackPacket.lastRefTime);
                    AckEntry ackEntry = ackMap.remove(ackKey);
                    if (ackEntry == null) {
                        throw new IllegalStateException(
                                "unable to find ackEntry for key: " + ackKey + ", ack json: " + json);
                    }
                    
                    long pushCost = System.currentTimeMillis() - udpSendTimeMap.get(ackKey);
                    
                    Loggers.PUSH
                            .info("received ack: {} from: {}:{}, cost: {} ms, unacked: {}, total push: {}", json, ip,
                                    port, pushCost, ackMap.size(), MetricsMonitor.getTotalPushMonitor().get());
                    
                    MetricsMonitor.incrementPushCost(pushCost);
                    
                    udpSendTimeMap.remove(ackKey);
                    
                } catch (Throwable e) {
                    Loggers.PUSH.error("[NACOS-PUSH] error while receiving ack data", e);
                }
            }
        }
        
    }
//ack entity
public class AckEntry {
    
    public AckEntry(String key, DatagramPacket packet) {
        this.key = key;
        this.origin = packet;
    }
    
    private String key;
    //传递数据包
    private DatagramPacket origin;
    //重试次数
    private AtomicInteger retryTimes = new AtomicInteger(0);
    
    private Map<String, Object> data;
    
}

四、小结

  • 对比TCP、UDP,各自有适合的场景。对不同的业务场景需要,选择响应的传输方式。
  • nacos naming:UDP+补偿机制。UDP虽然没有ack机制,但可以在接收消息后再次发出send来实现消息确认。
  • 解耦:发布-订阅模式,socket的send、receive。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值