其中Connection对象包含的信息如下:
@Slf4j @Data public class Connection { /** * 当前的socket连接实例 */ private Socket socket; /** * 当前连接线程 */ private ConnectionThread connectionThread; /** * 当前连接是否登陆 */ private boolean isLogin; /** * 存储当前的user信息 */ private String userId; /** * 创建时间 */ private Date createTime; /** * 最后一次更新时间,用于判断心跳 */ private Date lastOnTime; }主要关注其中的lastOnTime字段,每次服务端接收到标识是心跳数据,会更新当前的lastOnTime字段,代码如下:
if (functionCode.equals(FunctionCodeEnum.HEART.getValue())) { //心跳类型 connection.setLastOnTime(new Date()); //发送同样的心跳数据给客户端 ServerSendDto dto = new ServerSendDto(); dto.setFunctionCode(FunctionCodeEnum.HEART.getValue()); connection.println(JSONObject.toJSONString(dto)); }额外会有一个监测进程,以一定频率来监测上述维护的map中的每一个Connection对象,如果当前时间与lastOnTime的时间间隔超过自定义的长度,则自动将其对应的socket连接关闭,代码如下:
Date now = new Date(); Date lastOnTime = connectionThread.getConnection().getLastOnTime(); long heartDuration = now.getTime() - lastOnTime.getTime(); if (heartDuration > SocketConstant.HEART_RATE) { //心跳超时,关闭当前线程 log.error("心跳超时"); connectionThread.stopRunning(); }在上面代码中,服务端收到标识是心跳数据的时候,除了更新该socket对应的lastOnTime,还会同样同样心跳类型的数据给客户端,客户端收到标识是心跳数据的时候也会更新自己的lastOnTime字段,同时也有一个心跳监测线程在监测当前的socket连接心跳是否超时
客户端身份获知、强制身份验证 1. 实现思路通过代码socket = serverSocket.accept()获得的一个socket连接我们仅仅只能知道其客户端的ip以及端口号,并不能获知这个socket连接对应的到底是哪一个客户端,因此必须得先获得客户端的身份并且验证通过其身份才能让其正常连接。
具体的实现思路是:
自定义一个登陆处理接口,当server端受到标识是用户登陆的时候(此时会携带用户信息或者token,此处简化为用户id),调用用户的登陆验证,验证通过的话则将该socket连接与用户信息绑定,设置其为已登录,并且封装对应的对象放入前面提的客户端map中,由此可获得具体用户对应的哪一个socket连接。
为了实现socket连接的强制验证,在监测线程中,也会判断当前用户多长时间内没有实现登录态,若超时则认为该socket连接为非法连接,主动关闭该socket连接。
2. 代码实现自定义登陆处理接口,这边简单以userId来判断是否允许登陆:
public interface LoginHandler { /** * client登陆的处理函数 * * @param userId 用户id * * @return 是否验证通过 */ boolean canLogin(String userId); }收到客户端发来的数据时候的处理:
if (functionCode.equals(FunctionCodeEnum.LOGIN.getValue())) { //登陆,身份验证 String userId = receiveDto.getUserId(); if (socketServer.getLoginHandler().canLogin(userId)) { //设置用户对象已登录状态 connection.setLogin(true); connection.setUserId(userId); if (socketServer.getExistSocketMap().containsKey(userId)) { //存在已登录的用户,发送登出指令并主动关闭该socket Connection existConnection = socketServer.getExistSocketMap().get(userId); ServerSendDto dto = new ServerSendDto(); dto.setStatusCode(999); dto.setFunctionCode(FunctionCodeEnum.MESSAGE.getValue()); dto.setErrorMessage("force logout"); existConnection.println(JSONObject.toJSONString(dto)); existConnection.getConnectionThread().stopRunning(); log.error("用户被客户端重入踢出,userId:{}", userId); } //添加到已登录map中 socketServer.getExistSocketMap().put(userId, connection); }监测线程判断用户是否完成身份验证:
if (!connectionThread.getConnection().isLogin()) { //还没有用户登陆成功 Date createTime = connectionThread.getConnection().getCreateTime(); long loginDuration = now.getTime() - createTime.getTime(); if (loginDuration > SocketConstant.LOGIN_DELAY) { //身份验证超时 log.error("身份验证超时"); connectionThread.stopRunning(); } } socket异常处理与垃圾线程回收 1. 实现思路socket在读取数据或者发送数据的时候会出现各种异常,比如客户端的socket已断开连接(正常断开或物理连接断开等),但是服务端还在发送数据或者还在接受数据的过程中,此时socket会抛出相关异常,对于该异常的处理需要将自身的socket连接关闭,避免资源的浪费,同时由于是多线程方案,还需将该socket对应的线程正常清理。
2. 代码实现