Actual Combat: Manually Build Java Crawlers——Based on JDK11 Native HttpClient (4)

Table of contents

Metropolis

Preparation

basic configuration

message specification

Netty package

server package

User Channel Encapsulation

message processing encapsulation

message client


In the first two articles, we have completed the encapsulation of the basic tools and the HttpClient tool. In fact, we can already start using it, but in order to achieve the effect of data visualization, I hope that every piece of information I crawl can let me know it immediately. It is effective, so I used the method of message push to tell me which information I caught in real time, so we need to package a real-time message tool. Here we use Netty as the message manager to start the package of Netty.

Metropolis _ _

Let's take a look at the characteristics of Netty first:

Netty is characterized by:

  • High concurrency: Netty is a network communication framework developed based on NIO (Nonblocking IO, non-blocking IO). Compared with BIO (Blocking I/O, blocking IO), its concurrency performance has been greatly improved.
  • Fast transmission: Netty's transmission relies on the zero-copy feature to minimize unnecessary memory copies and achieve more efficient transmission.
  • Well-encapsulated: Netty encapsulates many details of NIO operations and provides an easy-to-use calling interface.

Therefore, there are many tools on the market that are developed based on Netty for secondary packaging. Here we only make a basic package for Netty.

Let me talk about some points to pay attention to when encapsulating Netty:

1.Netty is an independent service, so it needs to be started independently

2. The client connects to the Netty server, and the client mark needs to be marked by ourselves

3. Netty's message forwarding is to send the message of A client to B client, so that A can send a message to B

4. When a Netty user connects to the server, there is a certain processing order, and there is also a processing order when disconnecting, so we can monitor the user's online and offline status

5. We do not start the Netty service independently, but hand it over to SpringBoot for management, that is, when the Springboot service starts, the Netty service is automatically started, and when the Springboot service is closed, the Netty service is closed first 

6. The message service is a long-term connection service. Unlike the Http request, the channel is automatically closed after the request ends. The long-term connection will maintain a long-term connection with the server. The user does not close the channel, so the Socket request must be used.

After understanding the above principles, let's start preparing for the Netty package.

Preparation

basic configuration

First of all, we write the configuration of Netty into application.properties to facilitate centralized management of basic configuration (here we have added it in the first article)

#Netty的自定义配置
netty.websocket.ip=0.0.0.0
netty.websocket.port=7251
netty.websocket.max-size=10240
netty.websocket.path=/channel

In this way, the connection of Netty means that any IP can access our server. The connection method is ws://127.0.0.1:7251/channel, and ws:// means that this request is for websocket service and long connection service.

In addition, we need to define some specifications to facilitate our processing of messages

message specification

The message format is standardized, which is convenient for us to unify the analysis of messages

Message.java

package com.vtarj.pythagoras.message.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import java.util.HashMap;

/**
 * @Author Vtarj
 * @Description 消息实体,定义消息规范
 * @Time 2022/3/10 16:19
 **/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Message {
    /**
     * 消息类型,1 个人消息,2 全体群发消息,0 系统响应消息
     */
    private Integer type;
    /**
     * 消息来源,一般为用户标识或系统标识
     */
    private String source;
    /**
     * 消息去处,一般为用户标识或群组标识
     */
    private String target;
    /**
     * 消息内容
     */
    private String message;
    /**
     * 消息扩展信息,可供用户自定义
     */
    private HashMap<String,Object> ext;
}

Message exception specification, which facilitates our tracking and feedback of exception messages

ErrorType.java

package com.vtarj.pythagoras.message.entity;

/**
 * @Author Vtarj
 * @Description 自定义错误类型
 * @Time 2022/3/11 11:16
 **/
public enum ErrorType {

    /**
     * 自定义错误
     */
    NO_STANDARD("消息格式不标准",1001),
    NOT_STANDARDIZED("消息内容不规范",1002),
    NO_TARGET("未找到消息接收目标",2001),
    NOT_ONLINE("消息接收人不在线",2002),
    BIND_USER_STANDARD("用户绑定失败,无效用户",9001);


    /**
     * 构造枚举类型
     */
    private final String value;
    public String getValue() {
        return value;
    }

    private final int key;
    public int getKey() {
        return key;
    }

    ErrorType(String s, int i) {
        this.value = s;
        this.key = i;
    }
}

For the error specification, we adopt the enumeration method, which is convenient for adding error content and managing error information

The preparatory work is completed, and then we start to package the message tool.

Netty package

First of all, what we want to encapsulate is the message server, how to start the Netty service, and the initial configuration of the Netty service when starting.

server package

NettyRunner.java

package com.vtarj.pythagoras.message.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

/**
 * @Author Vtarj
 * @Description Netty服务启动器
 * @Time 2022/3/10 14:10
 **/
@Component("Netty消息中心")
public class NettyRunner implements ApplicationRunner, ApplicationListener<ContextClosedEvent>, ApplicationContextAware {

    /**
     * 从配置文件中获取Netty配置
     */
    @Value("${netty.websocket.ip}")
    private String ip;
    @Value("${netty.websocket.port}")
    private int port;
    @Value("${netty.websocket.path}")
    private String path;
    @Value("${netty.websocket.max-size}")
    private long maxSize;

    //日志管理
    private static final Logger logger = LoggerFactory.getLogger(NettyRunner.class);

    //定义上下文服务
    private ApplicationContext applicationContext;
    //消息服务通道
    private Channel serverChannel;
    //主线程组,接收请求
    private EventLoopGroup serverGroup;
    //从线程组,处理主线程分配的IO操作
    private EventLoopGroup clientGroup;


    /**
     * 实现Netty服务启动器
     * @param args  Application运行参数
     */
    @Override
    public void run(ApplicationArguments args) {
        //创建主线程组,接收请求
        serverGroup = new NioEventLoopGroup();
        //创建从线程组,处理主线程分配的IO操作
        clientGroup = new NioEventLoopGroup();

        //创建Netty服务器,配置消息中心
        ServerBootstrap server = new ServerBootstrap();
        server.group(serverGroup,clientGroup);
        server.channel(NioServerSocketChannel.class);
        server.localAddress(new InetSocketAddress(this.ip,this.port));
        server.childHandler(new ChannelInitializer<SocketChannel>() {
            //初始化客户端连接通道
            @Override
            protected void initChannel(SocketChannel socketChannel) {
                //配置消息过滤器
                ChannelPipeline pi = socketChannel.pipeline();
                //支持Http解码器,HttpRequestDecoder和HttpResponseEncoder的一个组合,针对http协议进行编解码
                pi.addLast(new HttpServerCodec());
                //支持大数据流,将大数据流分块发送客户端,防止大文件发送内存溢出
                pi.addLast(new ChunkedWriteHandler());
                //支持Http聚合器,将HttpMessage和HttpContents聚合到一个完成的 FullHttpRequest或FullHttpResponse中,具体是FullHttpRequest对象还是FullHttpResponse对象取决于是请求还是响应,需要放到HttpServerCodec这个处理器后面
                pi.addLast(new HttpObjectAggregator(65536));
                //支持入站事件控制器
                pi.addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception {
                        if(msg instanceof FullHttpRequest){
                            FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
                            String uri = fullHttpRequest.uri();
                            //判断请求链接非WebSocket指定的端点地址,直接响应404给客户端并关闭消息监听
                            //正常消息则继续处理
                            if(!uri.equals(path)){
                                ctx.channel().writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.NOT_FOUND)).addListener(ChannelFutureListener.CLOSE);
                                return;
                            }
                        }
                        super.channelRead(ctx,msg);
                    }
                });
                //支持WebSocket数据压缩扩展
                pi.addLast(new WebSocketServerCompressionHandler());
                //设置WebSocket向外暴露的站点信息,当启动数据压缩扩展时,第三个参数必须为true
                pi.addLast(new WebSocketServerProtocolHandler(path,null,true,maxSize));
                //控制反转,自定义消息处理机制,将消息交由ChatHandler处理
                pi.addLast(applicationContext.getBean(ChatHandler.class));
            }
        });

        //启动消息中心
        try {
            //服务器绑定监听端口,开始接收连接
            this.serverChannel = server.bind().sync().channel();
            logger.info("Netty消息中心服务启动,ip={},port={}", this.ip, this.port);

        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * 将Netty启动器交由Springboot管理
     * @param applicationContext    Application上下文信息
     * @throws BeansException   Beans加载异常
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * Springboot服务停止监听
     * @param event 服务器关闭事件触发
     */
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        //关闭主线程信道
        if(this.serverGroup != null){
            try {
                this.serverGroup.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //关闭从线程信道
        if(this.clientGroup != null){
            try {
                this.clientGroup.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //关闭消息服务通道
        if(this.serverChannel != null){
            this.serverChannel.close();
        }
        logger.info("Netty消息中心服务关闭!");
    }
}

After the message service is started, the next step is to establish a message channel. If the client wants to send a message to the server, there must be a channel. How to manage the channel becomes the next thing we need to consider (here we use the user to mark the channel).

User Channel Encapsulation

UserChannel.java

package com.vtarj.pythagoras.message.netty;

import io.netty.channel.Channel;
import org.springframework.stereotype.Service;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author Vtarj
 * @Description 用户通道管理器
 * @Time 2022/3/10 15:54
 **/
@Service
public class UserChannel {

    //定义全局用户通道管理器
    private static final ConcurrentHashMap<String, Channel> USER_CHANNEL_MAP = new ConcurrentHashMap<>();

    /**
     * 给用户通道管理器中添加用户渠道信息
     * @param userKey   用户标识
     * @param channel   通信渠道
     */
    public static void add(String userKey,Channel channel){
        USER_CHANNEL_MAP.put(userKey,channel);
    }

    /**
     * 从用户渠道管理器中删除指定用户渠道信息
     * @param userKey   待删除的用户标识
     */
    public static void remove(String userKey){
        USER_CHANNEL_MAP.remove(userKey);
    }

    /**
     * 根据通道ID移除用户渠道信息
     * @param channelId 通道ID
     */
    public static void removeByChannelId(String channelId){
        if(isNotNull(channelId)){
            for (String key:
                 USER_CHANNEL_MAP.keySet()) {
                Channel channel = USER_CHANNEL_MAP.get(key);
                if(channelId.equals(channel.id().asLongText())){
                    remove(key);
                    break;
                }
            }
        }
    }

    /**
     * 根据用户标识获取用户的渠道
     * @param userKey   用户标识
     * @return  返回消息通道
     */
    public static Channel get(String userKey){
        if(isNotNull(userKey)){
            return USER_CHANNEL_MAP.get(userKey);
        }
        return null;
    }

    /**
     * 判断字符串是否为空字符串
     * @param str   待验证的字符串
     * @return  字符串为空判断结果
     */
    private static boolean isNotNull(String str){
        return str != null && !str.trim().isEmpty();
    }

}

Now that there are services and users, how to let users enter the server through the channel and let the server record user information? Here we need to design the user interaction method.

message processing encapsulation

ChatHandler.java

package com.vtarj.pythagoras.message.netty;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.vtarj.pythagoras.message.entity.ErrorType;
import com.vtarj.pythagoras.message.entity.Message;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.HashMap;


/**
 * @Author Vtarj
 * @Description 消息处理中心
 * @Time 2022/3/10 14:38
 **/
@ChannelHandler.Sharable
@Component
public class ChatHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    //日志管理
    private static final Logger logger = LoggerFactory.getLogger(ChatHandler.class);

    /**
     * 定义客户端群组,管理所有Channel连接
     * GlobalEventExecutor.INSTANCE是全局单例事件执行器
     */
    private static final ChannelGroup CLIENTS = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 定义在线人数
     */
    public static int online;
    /**
     * 1 发送个人,2 发送群体,0 系统消息,不转发
     */
    private static final int SYSTEM_CHANNEL = 0;
    private static final int TO_PERSON = 1;
    private static final int TO_GROUP = 2;

    //定义系统用户
    private static final String SYSTEM_USER = "admin";

    /**
     * 消息信道建立
     * @param ctx   通道信息
     * @throws Exception   信道建立异常信息
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        logger.info("【连接】 通信地址:{},时间:{}",ctx.channel().remoteAddress(),System.currentTimeMillis());
    }

    /**
     * 消息通道关闭
     * @param ctx   通道信息
     * @throws Exception    信道关闭异常信息
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //移除信道
        super.channelInactive(ctx);
        logger.info("【断开】 通信地址:{},时间:{}",ctx.channel().remoteAddress(),System.currentTimeMillis());
    }

    /**
     * 客户端连接建立机制(连接顺序):
     * handlerAdded() -> channelRegistered() -> channelActive() -> channelRead() -> channelReadComplete()
     * @param ctx   通道信息
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx){
        CLIENTS.add(ctx.channel());
        online = CLIENTS.size();
    }

    /**
     * 客户端关闭连接机制(断开顺序):
     * channelInactive() -> channelUnregistered() -> handlerRemoved()
     * @param ctx   通道信息
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx){
        CLIENTS.remove(ctx.channel());
        online = CLIENTS.size();
        //移除用户通道
        UserChannel.removeByChannelId(ctx.channel().id().asLongText());
    }

    /**
     * 创建用户绑定通道
     * @param ctx   用户连接通道
     * @param info   包含用户信息的Socket消息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame info) {
        //文本消息处理
        if (info instanceof TextWebSocketFrame){
            TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) info;

            Message message = decrypt(textWebSocketFrame.text());
            if(message != null){
                //绑定用户,激活用户通道(用户在未发送消息前通道不记录,表示该通道处于休眠状态,用户通道变更后自动更新通道)
                UserChannel.add(message.getSource(),ctx.channel());
                //转发消息
                sendTextMessage(message);
            }else{
                ctx.channel().writeAndFlush(new TextWebSocketFrame(ErrorType.BIND_USER_STANDARD.getValue())).addListener(ChannelFutureListener.CLOSE);
            }
        } else {
            //不接受文本以外的数据帧类型,通道创建失败
            ctx.channel().writeAndFlush(WebSocketCloseStatus.INVALID_MESSAGE_TYPE).addListener(ChannelFutureListener.CLOSE);
        }
    }


    /**
     * 发送文本消息
     * @param m 待发送的消息
     */
    public static void sendTextMessage(Message m) {
        if(m.getType() == TO_GROUP){
            sendTextMessageToAll(m);
        }else if(m.getType() == TO_PERSON){
            sendTextMessageToPerson(m);
        }else{
            if(m.getType() != SYSTEM_CHANNEL){
                sendError(m,ErrorType.NO_STANDARD);
            }
        }
    }

    /**
     * 向指定用户发送消息
     * @param m 待发送的消息
     */
    public static void sendTextMessageToPerson(Message m){
        if(m.getTarget().isEmpty()) {
            logger.info("{} 【消息发送失败】: {}【消息来源:{}】",System.currentTimeMillis(),ErrorType.NO_TARGET.getValue(),m.getSource());
            sendError(m, ErrorType.NO_TARGET);
            return;
        }
        if(m.getSource().equals(m.getTarget())  || m.getTarget().equals(SYSTEM_USER)){
            logger.info("{} 【消息发送失败】: {}【消息来源:{}】【消息目标:{}】",System.currentTimeMillis(),"消息发送目标异常",m.getSource(),m.getTarget());
            return;
        }
        Channel channel = UserChannel.get(m.getTarget());
        if (channel != null){
            channel.writeAndFlush(new TextWebSocketFrame(encryption(m)));
        } else {
            logger.info("{} 【消息发送失败】: {}【消息来源:{}】【消息目标:{}】",System.currentTimeMillis(),ErrorType.NOT_ONLINE.getValue(),m.getSource(),m.getTarget());
            sendError(m, ErrorType.NOT_ONLINE);
        }
    }

    /**
     * 向全体在线用户发送消息(含休眠用户)
     * @param m 待发送的消息
     */
    public static void sendTextMessageToAll(Message m){
        for (Channel channel:
                CLIENTS) {
            //排除消息发送人
            Channel channel1 = UserChannel.get(m.getSource());
            if(channel1 != null && !channel1.id().asLongText().equals(channel.id().asLongText())){
                channel.writeAndFlush(new TextWebSocketFrame(encryption(m)));
            }
        }
    }

    /**
     * 系统响应错误信息
     * @param m 用于反馈客户端错误消息
     */
    public static void sendError(Message m,ErrorType e){
        if(m.getTarget().equals(SYSTEM_USER)){
            System.out.println("系统消息无法送达!");
            return;
        }
        Message r = new Message();
        r.setSource(SYSTEM_USER);
        r.setTarget(m.getSource());
        r.setType(SYSTEM_CHANNEL);
        r.setMessage(e.getValue());
        HashMap<String,Object> ext = new HashMap<>();
        ext.put("resTime",System.currentTimeMillis());
        ext.put("code",e.getKey());
        ext.put("data",m);
        r.setExt(ext);
        sendTextMessageToPerson(r);
    }

    /**
     * 消息内容转为Json字符串
     * @param message   消息内容
     * @return  返回Json字符串
     */
    private static String encryption(Message message){
        ObjectMapper mapper = new ObjectMapper();
        String result = null;
        try{
            result = mapper.writeValueAsString(message);
        } catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 消息字符串转消息内容
     * @param context   消息字符串
     * @return  返回字符串转换的Message对象
     */
    private static Message decrypt(String context){
        Message message = null;
        if(!context.isEmpty()){
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
            try {
                message = mapper.readValue(context,Message.class);
            } catch (JsonProcessingException e){
                e.printStackTrace();
            }
        }
        return message;
    }
}

 Here we define an admin user as a system user, which is used for the system to send messages to the client (S—>A or S—>B), rather than the user to user mode (A—>S—>B).

In addition, the server will not recognize the user tag by itself, the server will only recognize its own CLIENTS, so we store the user tag in the CLIENTS, so that we can identify the Channel (channel) in the CLIENTS through the user.

The principle of message sending is very simple. After the user establishes a channel with the server, the user only needs to put the message into the channel, and the message can reach the server. Similarly, the server puts the message into the channel, and the user can receive the message. Because it is a long connection, the channel will remain open until either the user or the server closes it by itself. For offline messages such as QQ, the message is first temporarily stored in the server, and then the channel is re-established after the user goes online, and then the message is sent to the channel, so as to achieve the purpose of sending the message offline. The offline service is not specifically introduced here. .

When the user establishes a connection with the server, it needs to go through five stages (handlerAdded() -> channelRegistered() -> channelActive() -> channelRead() -> channelReadComplete()), which is equivalent to the client connecting once, and the server actually needs to process 5 times, so we only need to grab a certain time and mark it. Here we usually listen to channelActive to indicate that the channel has been established (the message channel is closed for the same reason).

In this encapsulation, there will be a difference between general messages and personal messages. As we have said above, personal messages are sent to designated channels, and general messages are sent to all channels except the system itself. In fact, groups can be derived from this. News, organization news, etc., here is not too much divergence.

In addition, we only encapsulate text messages, pictures, files and other message support, if you are interested, please explore by yourself.

The basic encapsulation of message channel management is completed. What we need to do now is to encapsulate a quick message sending interface to avoid the need to sort out a lot of relationships when other business calls.

NettyHelper.java

package com.vtarj.pythagoras.message;

import com.vtarj.pythagoras.message.entity.Message;
import com.vtarj.pythagoras.message.netty.ChatHandler;

/**
 * @Author Vtarj
 * @Description Netty Client helper,提供第三方消息接口
 * @Time 2022/3/10 14:08
 **/
public class NettyHelper {

    /**
     * 客户端发送消息
     * @param from  消息来源
     * @param to    消息发送至,发送全体消息时无需指定该参数
     * @param type  消息类型,1 个人,2 全体
     * @param content   消息内容
     */
    public static void send(String from, String to, int type, String content){
        Message message = new Message();
        message.setMessage(content);
        message.setType(type);
        message.setSource(from);
        message.setTarget(to);
        ChatHandler.sendTextMessage(message);
    }
}

The encapsulation of the message server is completed, and then someone asks, how does the client connect to the server to establish a long connection? Haha, in fact, we have already said above that the channel can be established through ws://127.0.0.1:7251/channel, so whichever client the client uses, just write the corresponding request.

message client

Here we write a JS example for your reference. After all, we just said that we want to implement monitoring on the web.

message.js

/**
 * 定义消息适配器,规范消息模型
 * @user 用户标识
 * @send 发送消息
 * @receive 接收消息
 */
let MsgAdapter = {
    "user":"",
    "send":function (target,message){},
    "receive":function (data){}
}

$(function(){
    //判断浏览器是否支持WebSocket
    if(window.WebSocket){
        /**
         * 创建全局WS服务
         * @type {WebSocket}
         */
        const websocket = new WebSocket('ws://127.0.0.1:7251/channel');

        /**
         * 创建消息通道,并绑定用户
         */
        websocket.onopen = () => {
            if(MsgAdapter.user){
                websocket.send(JSON.stringify({
                    "source" : MsgAdapter.user,
                    "type" : 0,
                    "target" : "",
                    "message" : "Hello Server!"
                }));
            }else {
                console.log("您还未指定用户标识,消息通道无法建立!");
            }
        }

        /**
         * 给指定用户发送消息
         * @param t 待接收的人
         * @param m 待发送的消息
         */
        MsgAdapter.send = (t,m) => {
            if(!t){
                console.log("未指定消息接收人,消息发送失败!");
                return;
            }
            websocket.send(JSON.stringify({
                "source" : MsgAdapter.user,
                "type" : 1,
                "target" : t,
                "message" : m
            }));
        }

        /**
         * 接收消息
         * @param e
         */
        websocket.onmessage = (e) => {
            MsgAdapter.receive(e.data)
        }

        /**
         * 消息通道被关闭
         * @param e
         */
        websocket.onclose = (e) => {
            console.log("消息通道连接已断开:",e);
        }

        /**
         * 消息中心连接异常
         * @param e
         */
        websocket.onerror = (e) => {
            console.log("消息中心连接异常:",e);
        }

    }else{
        console.log("您的浏览器不支持WebSocket服务,及时消息服务无法启用");
    }
})

Here we rely on jquery. After all, we use other people's $ symbols, so everyone loads dependencies by themselves. We define a MsgAdapter to manage messages. Send messages through MsgAdapter.send and receive messages through MsgAdapter.receive.

So far, the encapsulation of the message service Netty has been completed, and the next thing is ready, let's start crawling!

To be continued~~~

Previous: Actual Combat: Building a Java Crawler by Hand - Based on JDK11 Native HttpClient (3)

Next: Actual Combat: Building a Java Crawler by Hand - Based on JDK11 Native HttpClient (5)

Guess you like

Origin blog.csdn.net/Asgard_Hu/article/details/124603963