Java socketIo crea un servicio de chat

Si desea crear un servicio de chat, pero no desea utilizar servicios de terceros, como Huanxin, Rongyun, etc., este artículo se puede utilizar como referencia. El artículo está construido con springboot2.2.x, porque parte del código involucra negocios y se ha eliminado, y el logotipo TODO debe cooperar con la implementación de su propia empresa. Si tiene problemas, puede hacer preguntas en el área de comentarios o chatear en privado.

1. Importar maven para introducir dependencias

<dependency>
    <groupId>com.corundumstudio.socketio</groupId>
    <artifactId>netty-socketio</artifactId>
    <version>1.7.18</version>
</dependency>

2. 配置 l

socketio:
  host: 192.168.0.120
  port: 8887
  # 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
  maxFramePayloadLength: 1048576
  # 设置http交互最大内容长度
  maxHttpContentLength: 1048576
  # socket连接数大小(如只监听一个端口boss线程组为1即可)
  bossCount: 1
  workCount: 100
  allowCustomRequests: true
  # 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间
  upgradeTimeout: 1000000
  # Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
  pingTimeout: 6000000
  # Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
  pingInterval: 25000

3. Cree una clase de configuración para socketIo

import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.Transport;
import com.corundumstudio.socketio.listener.ExceptionListener;
import io.netty.channel.ChannelHandlerContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SocketIOConfig {

    @Value("${socketio.host}")
    private String host;

    @Value("${socketio.port}")
    private Integer port;

    @Value("${socketio.bossCount}")
    private int bossCount;

    @Value("${socketio.workCount}")
    private int workCount;

    @Value("${socketio.allowCustomRequests}")
    private boolean allowCustomRequests;

    @Value("${socketio.upgradeTimeout}")
    private int upgradeTimeout;

    @Value("${socketio.pingTimeout}")
    private int pingTimeout;

    @Value("${socketio.pingInterval}")
    private int pingInterval;

    @Bean
    public SocketIOServer socketIOServer() {
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setTcpNoDelay(true);
        socketConfig.setSoLinger(0);
//        解决重启端口占用问题,但因为是docker部署好像没有发现这个问题
//        socketConfig.setReuseAddress(true);

        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        config.setSocketConfig(socketConfig);
//        config.setHostname(host);
        config.setPort(port);
        config.setBossThreads(bossCount);
        config.setWorkerThreads(workCount);
        config.setAllowCustomRequests(allowCustomRequests);
        config.setUpgradeTimeout(upgradeTimeout);
        config.setPingTimeout(pingTimeout);
        config.setPingInterval(pingInterval);
        config.setTransports(Transport.WEBSOCKET);
        config.setRandomSession(true);  //default is false
        //鉴权
        config.setAuthorizationListener(data -> {
            //socketio可以传参,可以获取到 链接后面的参数 ?username=1&pwd=2
            String username= data.getSingleUrlParam("username");
            String pwd= data.getSingleUrlParam("pwd");
            //这里可以用作链接时的权限判断,返回false就是不允许链接,
            return true;
        });

        // 异常
        config.setExceptionListener(new ExceptionListener() {
            @Override
            public void onEventException(Exception e, List<Object> args, SocketIOClient client) {
                e.printStackTrace();
            }

            @Override
            public void onDisconnectException(Exception e, SocketIOClient client) {
                e.printStackTrace();
            }

            @Override
            public void onConnectException(Exception e, SocketIOClient client) {
                e.printStackTrace();
            }

            @Override
            public void onPingException(Exception e, SocketIOClient client) {
                e.printStackTrace();
            }

            @Override
            public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                cause.printStackTrace();
                ctx.close();
                return false;
            }
        });
        return new SocketIOServer(config);
    }
}

4. Cree ISocketIoService como un servicio de interfaz para socketio

public interface ISocketIoService {
    /**
     * 启动服务
     */
    void start();

    /**
     * 停止服务
     */
    void stop();
}

5. Implementar SocketIoServiceImpl

package com.yianjia.im.socketio;

import com.alibaba.fastjson.JSON;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.yianjia.im.project.utils.JsonUtils;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PreDestroy;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
public class SocketIoServiceImpl implements ISocketIoService {

    private final static Logger logger = LoggerFactory.getLogger(SocketIoServiceImpl.class);

    /**
     * 存放已连接的客户端
     */
    private static final Map<String, SocketIOClient> accountClientMap = new ConcurrentHashMap<>();
    private static final Map<String, ImAccount> clientAccountMap = new ConcurrentHashMap<>();

    /**
     * 发送消息
     */
    private static final String SEND_MSG_EVENT = "send_msg_event";

    /**
     * 客户端接收消息
     */
    private static final String ON_RECEIVE_MSG = "on_receive_msg";

    /**
     * 未签收
     */
    private static final String UNSIGN_MSG_EVENT = "unsign_msg_event";

    @Autowired
    private SocketIOServer socketIOServer;

    /**
     * Spring IoC容器在销毁SocketIOServiceImpl Bean之前关闭,避免重启项目服务端口占用问题
     */
    @PreDestroy
    private void autoStop() {
        stop();
    }

    @Override
    public void start() {
        socketIOServer.start();
    }

    @Override
    public void stop() {
        if (socketIOServer != null) {
            socketIOServer.stop();
            socketIOServer = null;
        }
    }

    /**
     * 推送信息给指定客户端
     *
     * @param accountCode
     * @param msgContent
     * @param imMsg
     */
    public void pushMessageToAccount(String accountCode, String msgContent, 发送消息体 imMsg) {
        SocketIOClient client = accountClientMap.get(accountCode);
        if (client != null && client.isChannelOpen()) {
            client.sendEvent(ON_RECEIVE_MSG, msgContent);
            logger.info("发送消息:" + accountCode);
        }
    }

    /**
     * 获取客户端url中的鉴权参数
     *
     * @param client: 客户端
     */
    private Object getParamsByClient(SocketIOClient client) {
        // 获取客户端url参数
        String username = client.getHandshakeData().getSingleUrlParam("username");
        String pwd = client.getHandshakeData().getSingleUrlParam("pwd");
        //这里主要是返回用户信息 TODO
        return null;
    }

    /**
     * 获取连接的客户端ip地址
     *
     * @param client: 客户端
     * @return: java.lang.String
     */
    private String getIpByClient(SocketIOClient client) {
        String sa = client.getRemoteAddress().toString();
        if (client.getHandshakeData().getHttpHeaders().get("x-forwarded-for") != null) {
            return client.getHandshakeData().getHttpHeaders().get("x-forwarded-for");
        }
        return sa.substring(1, sa.indexOf(":"));
    }

    /**
     * 客户端链接
     *
     * @param client 客户端
     */
    @OnConnect
    private void connectListener(SocketIOClient client) {
        Object user = getParamsByClient(client);
        if (null == user) {
            client.disconnect();
            return;
        }
        logger.info("************ 客户端: " + getIpByClient(client) + " 已连接: " + user + " ************");

        // TODO 获取用户的唯一标识,从上面user里面拿user.getImCode();
        String userImCode = "";

        SocketIOClient socketIOClient = accountClientMap.get(userImCode);
        if (null != socketIOClient) {
            if (clientAccountMap.containsKey(socketIOClient.getSessionId().toString())) {
                clientAccountMap.remove(socketIOClient.getSessionId().toString());
            }
            if (socketIOClient.isChannelOpen()) {
                socketIOClient.sendEvent("disconnect", "断开连接");
                socketIOClient.disconnect();
            }
        }
        accountClientMap.put(userImCode, client);

        ImAccount imAccount = new ImAccount();
        imAccount.setImCode(userImCode);
        clientAccountMap.put(client.getSessionId().toString(), imAccount);
        logger.info("当前在线人数:" + accountClientMap.keySet().size());
        client.sendEvent("connected", "成功连接");
    }

    /**
     * 链接断开
     *
     * @param client 客户端
     */
    @OnDisconnect
    private void disconnectListener(SocketIOClient client) {
        logger.info("************ 客户端: " + getIpByClient(client) + " 已断开 ************");
        client.disconnect();
        clientAccountMap.remove(client.getSessionId().toString());
        // 这里没有删除 account,是因为存在账户后登陆,然后上一个链接才断线的情况
        logger.info("当前在线人数:" + clientAccountMap.keySet().size());
    }

    /**
     * 未签收消息监听
     *
     * @param client     客户端
     * @param data       数据
     * @param ackRequest 回调函数
     */
    @OnEvent(value = UNSIGN_MSG_EVENT)
    private void unreadConversationEventListener(SocketIOClient client, String data, AckRequest ackRequest) {
        ImAccount imAccount = clientAccountMap.get(client.getSessionId().toString());
        String imCode = imAccount.getImCode();
        if (StringUtils.isBlank(imCode)) {
            client.disconnect();
            return;
        }
        String clientIp = getIpByClient(client);
        logger.info(clientIp + " 客户端 未签收消息");

        /**
         * TODO 根据imCode去数据库里面查询这个账户没有签收的消息
         */
        List<消息db类> msgList = new ArrayList<>();

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List<消息db类> updateMsgList = msgList.stream().map(imMsg -> {
            发送消息体 sendChatMsg = new 发送消息体();
            // TODO 构建发送消息体,然后判断连接是否打开,打开的话,发过去

            if (client.isChannelOpen()) {
                client.sendEvent(ON_RECEIVE_MSG, JsonUtils.objectToJson(sendChatMsg));
                消息db类.setSign("已签收");
                return imMsg;
            }
            return null;
        }).filter(Objects::nonNull).collect(Collectors.toList());
        if (1 <= updateMsgList.size()) {
            // TODO 更新数据库的消息状态
            update(updateMsgList);
        }
        ackRequest.sendAckData("ok");
    }

    /**
     * 消息发送监听
     *
     * @param client     客户端
     * @param data       数据
     * @param ackRequest 回调函数
     */
    @OnEvent(value = SEND_MSG_EVENT)
    private void sendMsgEventListener(SocketIOClient client, String data, AckRequest ackRequest) {
        ImAccount imAccount = clientAccountMap.get(client.getSessionId().toString());
        String imCode = imAccount.getImCode();
        if (StringUtils.isBlank(imCode)) {
            client.disconnect();
            return;
        }
        String clientIp = getIpByClient(client);
        logger.info(clientIp + " 客户端消息:" + data);

        // TODO 将接受的消息字符串转成自己的对象
        发送消息体 chatMsg = JSON.parseObject(data, 发送消息体.class);

        // TODO 构建消息db类,并进行保存
        消息db类 imMsg = new 消息db类();
        save(imMsg);

        // TODO 构建发送消息体,然后判断连接是否打开,打开的话,发过去
        发送消息体 sendChatMsg = new 发送消息体();

        pushMessageToAccount(acceptAccountCode, JsonUtils.objectToJson(sendChatMsg), imMsg);

        ackRequest.sendAckData(data);
    }

    @Data
    class ImAccount {
        /**
         * imCode,标识用户的唯一码
         */
        private String imCode;
    }
}

6. Inicie el servicio socketIo

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class SocketIoServer implements ApplicationRunner {

    @Autowired
    private ISocketIoService socketIOService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        socketIOService.start();
    }
}

7. Registre la configuración en la aplicación

@Bean
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
    return new SpringAnnotationScanner(socketServer);
}

8. Referencia de configuración de Nginx

 

 

 

 

 

 

Supongo que te gusta

Origin blog.csdn.net/itjavaee/article/details/108996951
Recomendado
Clasificación