java socketIo 搭建 聊天服务

如果你想搭建一个聊天服务,但是又不想用第三方服务,比如:环信,融云等其他,本篇文章可作为参考。文章采用springboot2.2.x构建,因为部分代码涉及业务,已移除,标识TODO都需要配合你自身的业务实现,如果遇到问题可以在评论区提问,或者私聊我也可以

1.导入maven引入依赖

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

2.配置yml

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.创建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.创建 ISocketIoService 用作socketio的接口服务

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

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

5. 实现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.启动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. 在Application里面注册配置

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

8.Nginx配置参考

 

 

 

 

 

 

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值