[netty] How does java act as a websocket client to initiate a request to the server

Article Directory

Yes, this article introduces how to use java as a client to initiate a websocket request.
Bloggers don’t make headlines, and they don’t know how to tell the difference between the server and the client. Write a headline and scribble articles.

foreword

Why use java as websocket client? Although the websocket protocol is intended to be a communication protocol between the web and the server, what if one day our supplier or Party A only provides the websocket interface?
If the front-end is directly connected and then the data is passed to the back-end, then what if data is lost in the steps from the front-end to the back-end warehousing? In short, it is relatively reliable to put data processing on the backend. We can use netty to realize the websocket client function

There are two points worth noting about long links. One is the heartbeat mechanism and the other is the reconnection mechanism
. If the heartbeat packet is not sent, the connection may be disconnected after a while;
Open, as a client, you need to be able to reconnect

the code

The blogger is not particularly proficient in long links, but what code can be used officially, what code can’t be used in production is just a demo written for fun, and it can be distinguished at a glance. The code is mainly for reference, and the writing is very good. There is room for code [email protected]:yimiancheng/netty-study.gitoptimization Rarely, thread pool bloggers recommend creating new ThreadPoolExecutor() to avoid OOM problems. In addition, it should be quasi-production environment-level code.

maven dependencies

          <dependency>
               <groupId>io.netty</groupId>
               <artifactId>netty-all</artifactId>
               <version>4.1.35.Final</version>
           </dependency>

The websocket client handle class mainly handles received messages, events, etc.
Event triggering will enter the userEventTriggered method ,
establishing a connection will enter the channelActive method,
receiving messages will enter the channelRead0 method,
and if an exception occurs, it
will enter the exceptionCaught method. Write this method, it is no problem to reconnect in the channelInactive method)


import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.SocketClient;
import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.websocket.SendMsg;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * WebSocketClientFrameHandler
 *
 * @date 2019/8/20 16:42
 */
public class WebSocketClientFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    
    
    private static final Logger LOG = LoggerFactory.getLogger(WebSocketClientFrameHandler.class);
    private SocketClient socketClient;
    private ChannelPromise channelPromise;

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    
    
        LOG.info("客户端接收到事件 " + (evt.getClass()) + " | " + evt.toString());

        if(WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE.equals(evt)) {
    
    
            LOG.info(ctx.channel().id().asShortText() + " 握手完成!");
            socketClient.CHANNEL_IS_READY.set(true);
            channelPromise.setSuccess();
            // SendMsg.send(ctx.channel(),"客户端握手完成消息 -》服务器时间: " + System.currentTimeMillis());
        }
        else if(evt instanceof IdleStateEvent){
    
    
            //ctx.channel().writeAndFlush(new PingWebSocketFrame());
            IdleStateEvent evtIdle = (IdleStateEvent) evt;
            switch(evtIdle.state()) {
    
    
                case WRITER_IDLE:
                    // SendMsg.send(ctx.channel(),"客户端 ping 消息 -》服务器时间: " + System.currentTimeMillis());
                    ctx.channel().writeAndFlush(new PingWebSocketFrame());
                case READER_IDLE:
                    // SendMsg.send(ctx.channel(),"客户端 ping 消息 -》服务器时间: " + System.currentTimeMillis());
                    ctx.channel().writeAndFlush(new PingWebSocketFrame());
                default:
                    break;
            }
        }

        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        LOG.info("打开连接 handlerAdded SUCCESS. | name = " +channel.id().asShortText());
        super.handlerAdded(ctx);
        channelPromise = ctx.newPromise();
    }

    /**
     * Channel 已经被注册到了EventLoop
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        LOG.info("注册成功 channelRegistered SUCCESS. | name = " +channel.id().asShortText());
        super.channelRegistered(ctx);
    }

    /**
     * Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        LOG.info("活动状态 channelActive SUCCESS. | name = " +channel.id().asShortText());
        super.channelActive(ctx);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, WebSocketFrame webSocketFrame) throws
        Exception {
    
    
        if(webSocketFrame instanceof TextWebSocketFrame) {
    
    
            // String message = textWebSocketFrame.content().toString(Charset.forName("utf-8"));
            String message = ((TextWebSocketFrame) webSocketFrame).text();
            LOG.info("客户端接收到消息:" + message);
        }
        else {
    
    
            LOG.info("接收到消息类型" + (webSocketFrame.getClass().getName()));
        }

        SendMsg.sendPong(channelHandlerContext.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    
    
        LOG.error("消息处理失败: " + cause.getMessage(), cause);
        ctx.close();
    }

    /**
     * Channel 已经被创建,但还未注册到EventLoop
     * 连接断开
     */
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        LOG.info("连接断开 channelUnregistered SUCCESS. | name = " +channel.id().asShortText());
        super.channelUnregistered(ctx);
        channelPromise = null;
    }

    public SocketClient getSocketClient() {
    
    
        return socketClient;
    }

    public void setSocketClient(SocketClient socketClient) {
    
    
        this.socketClient = socketClient;
    }

    public ChannelPromise getChannelPromise() {
    
    
        return channelPromise;
    }
}


Heartbeat task class


import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.SocketClient;
import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.websocket.SendMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.TimerTask;

/**
 * HeartBeatTimerTask
 *
 * @date 2019/9/2 16:24
 */
public class HeartBeatTimerTask extends TimerTask {
    
    
    private static final Logger LOG = LoggerFactory.getLogger(ReconnectTimerTask.class);
    private SocketClient socketClient;

    public HeartBeatTimerTask(SocketClient socketClient) {
    
    
        this.socketClient = socketClient;
    }

    @Override
    public void run() {
    
    
        if(socketClient != null && socketClient.isValid()) {
    
    
            SendMsg.send(socketClient.getChannel(), "客户端心跳消息 => " + System.currentTimeMillis());
        }
    }
}


Reconnect task class

import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.SocketClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.TimerTask;

/**
 * ReconnectTimerTask
 *
 * @date 2019/9/2 16:03
 */
public class ReconnectTimerTask extends TimerTask {
    
    
    private static final Logger LOG = LoggerFactory.getLogger(ReconnectTimerTask.class);
    private SocketClient socketClient;

    public ReconnectTimerTask(SocketClient socketClient) {
    
    
        this.socketClient = socketClient;
    }

    @Override
    public void run() {
    
    
        if(socketClient != null && !socketClient.isValid()) {
    
    
            LOG.info("=== 客户端重连 " + System.currentTimeMillis());
            socketClient.connect();
        }
    }
}

Customize the thread factory class, mainly to rename the thread for easy maintenance and debugging

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class NamedThreadFactory implements ThreadFactory {
    
    
    static final AtomicInteger poolNumber = new AtomicInteger(1);
    final AtomicInteger threadNumber = new AtomicInteger(1);
    final ThreadGroup group;
    final String prefix;
    final boolean isDaemon;
    final int priority;

    public NamedThreadFactory() {
    
    
        this("pool");
    }

    public NamedThreadFactory(String prefix) {
    
    
        this(prefix, false, Thread.NORM_PRIORITY);
    }

    public NamedThreadFactory(String prefix, boolean isDaemon, int priority) {
    
    
        SecurityManager sm = System.getSecurityManager();
        this.group = (sm != null) ? sm.getThreadGroup() : Thread.currentThread().getThreadGroup();
        this.prefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
        this.isDaemon = isDaemon;
        this.priority = priority;
    }

    public Thread newThread(Runnable r) {
    
    
        Thread thread = new Thread(group, r, prefix + threadNumber.getAndIncrement(), 0);
        thread.setDaemon(isDaemon);
        thread.setPriority(priority);
        return thread;
    }
}

Message monitoring class, mainly used to send messages (including heartbeat packets) whether they are successful


import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class CustomerChannelFutureListener implements ChannelFutureListener {
    
    
    private static final Logger LOG = LoggerFactory.getLogger(CustomerChannelFutureListener.class);

        public void operationComplete(ChannelFuture channelFuture) throws Exception {
    
    
            // LOG.info(JSON.toJSONString(channelFuture));
            if(channelFuture.isDone() && channelFuture.isSuccess()){
    
    
               // LOG.info("send success.");
            }
            else {
    
    
                channelFuture.channel().close();
                LOG.info("send error. cause = " + channelFuture.cause());
                channelFuture.cause().printStackTrace();
            }
        }
    }

Simple encapsulated message sending class


import com.alibaba.fastjson.JSON;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * SendMsg
 *
 * @date 2019/8/30 18:02
 */
public class SendMsg {
    
    
    private static final Logger LOG = LoggerFactory.getLogger(SendMsg.class);
    public static ConcurrentHashMap<String, Channel> ALL_CHANNEL = new ConcurrentHashMap<String, Channel>();

    public static void startSendMsg() {
    
    
        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    
                while(true) {
    
    
                    sendMsgTest();
                }
            }
        });

        thread.start();
    }

    public static void sendMsgTest() {
    
    
        try {
    
    
            Map<String, Channel> map = Collections.unmodifiableMap(ALL_CHANNEL);
            LOG.info("map size = " + map.size());
            if(MapUtils.isEmpty(map)) {
    
    
                Thread.sleep(2000);
                return;
            }

            for(Map.Entry<String, Channel> entry : map.entrySet()) {
    
    
                LOG.info("------------- key = " + entry.getKey());
                send(entry.getValue(), "服务端发送消息 " + entry.getKey() + " | " + System.currentTimeMillis());
            }

            Thread.sleep(10000);
        }
        catch(Exception ex) {
    
    
            ex.printStackTrace();
        }
    }

    public static void put(Channel channel) {
    
    
        ALL_CHANNEL.put(channel.id().asShortText(), channel);
    }

    public static void remove(Channel channel) {
    
    
        ALL_CHANNEL.remove(channel.id().asShortText());
    }

    public static void send(Channel channel, Object msg) {
    
    
        final String textMsg = JSON.toJSONString(msg);

        if(channel != null && channel.isActive()) {
    
    
            TextWebSocketFrame frame = new TextWebSocketFrame(textMsg);
            channel.writeAndFlush(frame)
                .addListener(new CustomerChannelFutureListener());
        }
        else {
    
    
            LOG.error("消息发送失败! textMsg = " + textMsg);
        }
    }

    public static void sendPing(Channel channel) {
    
    
        if(channel != null && channel.isActive()) {
    
    
            channel.writeAndFlush(new PingWebSocketFrame())
                .addListener(new CustomerChannelFutureListener());
        }
        else {
    
    
            LOG.error("消息发送失败! ping");
        }
    }

    public static void sendPong(Channel channel) {
    
    
        if(channel != null && channel.isActive()) {
    
    
            channel.writeAndFlush(new PongWebSocketFrame())
                .addListener(new CustomerChannelFutureListener());
        }
        else {
    
    
            LOG.error("消息发送失败! pong");
        }
    }

}


channel initialization


import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.handler.WebSocketClientFrameHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.timeout.IdleStateHandler;

/**
 * ClientChannelInitializer
 *
 * @date 2019/8/31 16:05
 */
public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    
    
    private WebSocketClientHandshaker webSocketClientHandshaker;
    private WebSocketClientFrameHandler webSocketFrameHandler;

    public ClientChannelInitializer(WebSocketClientHandshaker webSocketClientHandshaker, WebSocketClientFrameHandler
        webSocketFrameHandler) {
    
    
        this.webSocketClientHandshaker = webSocketClientHandshaker;
        this.webSocketFrameHandler = webSocketFrameHandler;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
    
    
        ChannelPipeline pipeline = socketChannel.pipeline();

        pipeline.addLast(new HttpClientCodec());//Http协议编码解码器
        pipeline.addLast(new HttpObjectAggregator(65536));//聚合 HttpRequest
        pipeline.addLast(new IdleStateHandler(5, 10, 0));
        //会处理ping pong close消息
        pipeline.addLast(new WebSocketClientProtocolHandler(webSocketClientHandshaker,true));
        pipeline.addLast(webSocketFrameHandler);
    }
}

Client main class: add workgroup, start websocket client, establish connection, etc.

import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.handler.WebSocketClientFrameHandler;
import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.task.HeartBeatTimerTask;
import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.task.ReconnectTimerTask;
import com.qiuhuanhen.springroot.interfaces.websocket.websocketclient.thread.NamedThreadFactory;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * SocketClient
 *
 * @date 2019/8/20 17:53
 */
public class SocketClient {
    
    
    private static final Logger LOG = LoggerFactory.getLogger(SocketClient.class);

    public static String CLIENT_VERSION = "client_version";
    private static final int DEFAULT_PORT = 80;
    /**
     * 长链重连间隔时间,单位s
     */
    public static long RECONNECT_INTERVAL = 10;
    /**
     * 长链心跳时间,单位s
     */
    public static long FETCH_PERIOD = 30;

    public static String host = "127.0.0.1";
    public static int port = 8585;

    private Channel channel = null;
    private NioEventLoopGroup nioEventLoopGroup;

    public AtomicBoolean CHANNEL_IS_READY = new AtomicBoolean(false);

    private ScheduledExecutorService RECONNECT_TIMER;
    private ScheduledExecutorService HEARTBEAT_TIMER;
    static final String URL = System.getProperty("url", "ws://127.0.0.1:8585/boot/imserver/1111");
    URI uri = new URI(URL);

    static {
    
    
        // RECONNECT_TIMER = Executors.newSingleThreadScheduledExecutor();
    }


    public SocketClient(URI uri) throws URISyntaxException {
    
    
        this.uri = uri;
    }

    private void start() {
    
    
        Bootstrap boot = new Bootstrap();
        nioEventLoopGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());

        try {
    
    
            HttpHeaders httpHeaders = new DefaultHttpHeaders();
            httpHeaders.add(CLIENT_VERSION, 1);

            WebSocketClientHandshaker webSocketClientHandshaker = WebSocketClientHandshakerFactory.newHandshaker(uri,
                WebSocketVersion.V13, null, false, httpHeaders);

            boot.group(nioEventLoopGroup)
                .option(ChannelOption.TCP_NODELAY, true)
                .channel(NioSocketChannel.class);

            WebSocketClientFrameHandler webSocketFrameHandler = new WebSocketClientFrameHandler();
            webSocketFrameHandler.setSocketClient(this);

            ClientChannelInitializer clientChannelInitializer =
                new ClientChannelInitializer(webSocketClientHandshaker, webSocketFrameHandler);

            boot.handler(new LoggingHandler(LogLevel.INFO));
            boot.handler(clientChannelInitializer);

            port = (uri.getPort() == -1) ? DEFAULT_PORT : uri.getPort();
            host = uri.getHost();
            channel = boot.connect(host, port).sync().channel();
            LOG.info("SocketClient has started. CHANNEL_IS_READY = " + CHANNEL_IS_READY.get());
            webSocketFrameHandler.getChannelPromise().sync();
            LOG.info("SocketClient has started full. CHANNEL_IS_READY = " + CHANNEL_IS_READY.get());
        }
        catch(Exception ex) {
    
    
            ex.printStackTrace();
            LOG.error("connect error. uri " + uri.toString());
        }
        finally {
    
    
            // nioEventLoopGroup.shutdownGracefully();
        }
    }

    /**
     * 客户端连接服务端
     */
    public void connect() {
    
    
        stop();
        start();
        startReconnect();
        doHeartBeat();
    }

    /**
     * 开启线程-断开重连
     */
    public void startReconnect() {
    
    
        RECONNECT_TIMER = new ScheduledThreadPoolExecutor(1,
            new NamedThreadFactory("reconnect-schedule-pool", Boolean.TRUE, Thread.NORM_PRIORITY));

        // https://www.jianshu.com/p/502f9952c09b
        RECONNECT_TIMER.scheduleAtFixedRate(new ReconnectTimerTask(this),
            RECONNECT_INTERVAL * 1000L, RECONNECT_INTERVAL * 1000L, TimeUnit.MILLISECONDS);
    }

    /**
     * 心跳
     */
    private void doHeartBeat() {
    
    
        HEARTBEAT_TIMER = new ScheduledThreadPoolExecutor(1,
            new NamedThreadFactory("heartbeat-schedule-pool", Boolean.TRUE, Thread.NORM_PRIORITY));

        // https://www.jianshu.com/p/502f9952c09b
        HEARTBEAT_TIMER.scheduleAtFixedRate(new HeartBeatTimerTask(this),
            FETCH_PERIOD * 1000L, FETCH_PERIOD * 1000L, TimeUnit.MILLISECONDS);
    }

    /**
     * 客户端停止
     */
    public void stop() {
    
    
        try {
    
    
            if(nioEventLoopGroup != null) {
    
    
                nioEventLoopGroup.shutdownGracefully();
            }

            if(channel != null) {
    
    
                channel.close();
            }

            if(RECONNECT_TIMER != null) {
    
    
                RECONNECT_TIMER.shutdown();
                RECONNECT_TIMER = null;
            }

            if(HEARTBEAT_TIMER != null) {
    
    
                HEARTBEAT_TIMER.shutdown();
                HEARTBEAT_TIMER = null;
            }
        }
        catch(Exception ex) {
    
    
            //do nothing.
        }
    }


    public boolean isValid() {
    
    
        if (channel != null && channel.isActive()) {
    
    
            return true;
        } else {
    
    
            return false;
        }
    }

    public Channel getChannel() {
    
    
        return channel;
    }

    public static void main(String[] args) {
    
    
        try {
    
    
            new SocketClient(new URI(URL)).connect();
        } catch (URISyntaxException e) {
    
    
            e.printStackTrace();
        }
    }
}


Screenshot of successful reconnection:
insert image description here

Guess you like

Origin blog.csdn.net/qq_36268103/article/details/129534105