├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── panda │ ├── DemoApplication.java │ ├── SocketClientTest.java │ ├── config │ ├── SocketServerConfig.java │ ├── SystemExceptionHandler.java │ └── ThreadPoolConfigurer.java │ ├── controller │ └── socket │ │ ├── SocketClientController.java │ │ └── SocketServerController.java │ ├── core │ ├── ErrorCode.java │ ├── ErrorInfoEntity.java │ ├── ResponseEntity.java │ └── ServiceException.java │ ├── model │ ├── ClientParamVo.java │ ├── ClientSocket.java │ └── ServerParamVo.java │ ├── service │ ├── SocketClientService.java │ └── impl │ │ └── SocketClientServiceImpl.java │ └── utils │ └── socket │ ├── client │ └── SocketClient.java │ ├── constants │ └── SocketConstant.java │ ├── dto │ ├── ClientSendDto.java │ ├── ServerReceiveDto.java │ └── ServerSendDto.java │ ├── enums │ └── FunctionCodeEnum.java │ ├── handler │ ├── LoginHandler.java │ └── MessageHandler.java │ └── server │ ├── Connection.java │ ├── ConnectionThread.java │ ├── ListeningThread.java │ └── SocketServer.java └── resources ├── Socket测试.postman_collection.json ├── application.yml └── socket测试.postman_environment.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Maven template 3 | target/ 4 | pom.xml.tag 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | pom.xml.next 8 | release.properties 9 | dependency-reduced-pom.xml 10 | buildNumber.properties 11 | .mvn/timing.properties 12 | .mvn/wrapper/maven-wrapper.jar 13 | 14 | .idea/ 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 知乎文章地址:https://zhuanlan.zhihu.com/p/56135195 2 | 3 | 采用了BIO的多线程方案,实现了 4 | - [x] 自定义简单协议 5 | - [x] 心跳机制 6 | - [x] socket客户端身份强制验证 7 | - [x] socket客户端断线获知 8 | - [x] 暴露了一些接口,可通过接口简单实现客户端与服务端的socket交互 9 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.panda 8 | springboot-socket-demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.0.3.RELEASE 15 | 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-test 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 31 | 32 | com.alibaba 33 | fastjson 34 | 1.2.36 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/com/panda/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.panda; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | /** 8 | * @author 丁许 9 | * @date 2019-01-23 13:35 10 | */ 11 | @SpringBootApplication 12 | @Slf4j 13 | public class DemoApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(DemoApplication.class, args); 17 | log.info(" ヾ(◍°∇°◍)ノ゙ DemoApplication ヾ(◍°∇°◍)ノ゙\n" 18 | + " ____ _ ______ _ ______ \n" 19 | + " / ___'_ __ _ _(_)_ __ __ _ |_ _ \\ / |_|_ _ `. \n" 20 | + " \\___ | '_ | '_| | '_ \\/ _` | | |_) | .--DemoApplication. .--. `| |-' | | `. \\ .--. \n" 21 | + " ___)| |_)| | | | | || (_| | | __'. / .'`\\ \\/ .'`\\ \\| | | | | |/ .'`\\ \\ \n" 22 | + " |____| .__|_| |_|_| |_\\__, |_ | |__) || \\__. || \\__. || |, _| |_.' /| \\__. | \n" 23 | + " ====|_|===============|___/ |_______/ '.__.' '.__.' \\__/|______.' '.__.' "); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/panda/SocketClientTest.java: -------------------------------------------------------------------------------- 1 | package com.panda; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.utils.socket.client.SocketClient; 5 | import com.panda.utils.socket.dto.ClientSendDto; 6 | import com.panda.utils.socket.enums.FunctionCodeEnum; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.net.InetAddress; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.ScheduledExecutorService; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * @author 丁许 17 | * @date 2019-01-25 14:34 18 | */ 19 | @Slf4j 20 | public class SocketClientTest { 21 | 22 | public static void main(String[] args) { 23 | ExecutorService clientService = Executors.newCachedThreadPool(); 24 | String userId = "dingxu"; 25 | for (int i = 0; i < 1000; i++) { 26 | int index = i; 27 | clientService.execute(() -> { 28 | try { 29 | SocketClient client; 30 | client = new SocketClient(InetAddress.getByName("127.0.0.1"), 60000); 31 | //登陆 32 | ClientSendDto dto = new ClientSendDto(); 33 | dto.setFunctionCode(FunctionCodeEnum.LOGIN.getValue()); 34 | dto.setUserId(userId + index); 35 | client.println(JSONObject.toJSONString(dto)); 36 | ScheduledExecutorService clientHeartExecutor = Executors.newSingleThreadScheduledExecutor( 37 | r -> new Thread(r, "socket_client+heart_" + r.hashCode())); 38 | clientHeartExecutor.scheduleWithFixedDelay(() -> { 39 | try { 40 | ClientSendDto heartDto = new ClientSendDto(); 41 | heartDto.setFunctionCode(FunctionCodeEnum.HEART.getValue()); 42 | client.println(JSONObject.toJSONString(heartDto)); 43 | } catch (Exception e) { 44 | log.error("客户端异常,userId:{},exception:{}", userId, e.getMessage()); 45 | client.close(); 46 | } 47 | }, 0, 5, TimeUnit.SECONDS); 48 | while (true){ 49 | 50 | } 51 | } catch (Exception e) { 52 | log.error(e.getMessage()); 53 | } 54 | 55 | }); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/panda/config/SocketServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.panda.config; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.utils.socket.server.SocketServer; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * @author 丁许 11 | * @date 2019-01-24 22:24 12 | */ 13 | @Configuration 14 | @Slf4j 15 | public class SocketServerConfig { 16 | 17 | @Bean 18 | public SocketServer socketServer() { 19 | SocketServer socketServer = new SocketServer(60000); 20 | socketServer.setLoginHandler(userId -> { 21 | log.info("处理socket用户身份验证,userId:{}", userId); 22 | //用户名中包含了dingxu则允许登陆 23 | return userId.contains("dingxu"); 24 | 25 | }); 26 | socketServer.setMessageHandler((connection, receiveDto) -> log 27 | .info("处理socket消息,userId:{},receiveDto:{}", connection.getUserId(), 28 | JSONObject.toJSONString(receiveDto))); 29 | socketServer.start(); 30 | return socketServer; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/panda/config/SystemExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.panda.config; 2 | 3 | import com.panda.core.ResponseEntity; 4 | import com.panda.core.ServiceException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.propertyeditors.CustomDateEditor; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.HttpRequestMethodNotSupportedException; 10 | import org.springframework.web.bind.MissingServletRequestParameterException; 11 | import org.springframework.web.bind.WebDataBinder; 12 | import org.springframework.web.bind.annotation.*; 13 | import org.springframework.web.context.request.WebRequest; 14 | 15 | import java.text.DateFormat; 16 | import java.text.SimpleDateFormat; 17 | import java.util.Date; 18 | 19 | /** 20 | * @ClassName:SystemExceptionHandler 21 | * @Description:TODO 22 | * @author:huangyongfa 23 | * @date:2017年08月08日 24 | */ 25 | @ControllerAdvice 26 | public class SystemExceptionHandler { 27 | 28 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 29 | 30 | /** 31 | * 解决Form请求无法转换日期问题 32 | * 33 | * @param binder 绑定器 34 | * @param request 请求体 35 | */ 36 | @InitBinder 37 | private void initBinder(WebDataBinder binder, WebRequest request) { 38 | // 转换日期 39 | DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 40 | // CustomDateEditor为自定义日期编辑器 41 | binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); 42 | } 43 | 44 | /** 45 | * 未捕获的业务异常处理 46 | * 47 | * @param exception 48 | * 49 | * @return 50 | * 51 | * @throws 52 | * @see 53 | */ 54 | @ExceptionHandler(ServiceException.class) 55 | @ResponseStatus(value = HttpStatus.OK) 56 | public @ResponseBody 57 | ResponseEntity handleServcerException(ServiceException exception) { 58 | logger.error("system service exception handler,exception code:{},msg:{}", exception.getErrorCode(), 59 | exception.getMessage()); 60 | return new ResponseEntity().ERROR(exception); 61 | } 62 | 63 | 64 | @ExceptionHandler(org.springframework.web.servlet.NoHandlerFoundException.class) 65 | @ResponseStatus(value = HttpStatus.OK) 66 | @ResponseBody 67 | public ResponseEntity noHandlerFoundException(org.springframework.web.servlet.NoHandlerFoundException e) { 68 | logger.error(e.getMessage(), e); 69 | return new ResponseEntity().ERROR(new ServiceException("没有找到该页面")); 70 | } 71 | 72 | 73 | /** 74 | * 处理@requestParam不存在的异常 75 | * 76 | * @param exception 异常 77 | * 78 | * @return 返回responseEntity 79 | */ 80 | @ExceptionHandler(MissingServletRequestParameterException.class) 81 | @ResponseStatus(value = HttpStatus.OK) 82 | @ResponseBody 83 | public ResponseEntity handleMissingServletRequestParameterException( 84 | MissingServletRequestParameterException exception) { 85 | logger.error("system runtime exception handler:", exception); 86 | return new ResponseEntity().ERROR(new ServiceException(exception.getMessage(), exception)); 87 | } 88 | 89 | /** 90 | * 处理请求类型不一致的异常 91 | * 92 | * @param exception 异常 93 | * 94 | * @return 返回responseEntity 95 | */ 96 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 97 | @ResponseStatus(value = HttpStatus.OK) 98 | @ResponseBody 99 | public ResponseEntity handleHttpRequestMethodNotSupportedException( 100 | HttpRequestMethodNotSupportedException exception) { 101 | logger.error("system runtime exception handler:", exception); 102 | return new ResponseEntity().ERROR(new ServiceException(exception.getMessage(), exception)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/panda/config/ThreadPoolConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.panda.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 6 | 7 | import java.util.concurrent.ThreadPoolExecutor; 8 | 9 | /** 10 | * 线程池配置 11 | * 12 | * @author J.y 13 | */ 14 | @Configuration 15 | public class ThreadPoolConfigurer { 16 | 17 | @Bean(name = "clientTaskPool") 18 | public ThreadPoolTaskExecutor clientTaskPool() { 19 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 20 | executor.setCorePoolSize(5); 21 | executor.setKeepAliveSeconds(60); 22 | executor.setMaxPoolSize(Integer.MAX_VALUE); 23 | executor.setThreadNamePrefix("clientTaskPool"); 24 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 25 | executor.initialize(); 26 | return executor; 27 | } 28 | 29 | @Bean(name = "clientMessageTaskPool") 30 | public ThreadPoolTaskExecutor clientMessageTaskPool() { 31 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 32 | executor.setCorePoolSize(5); 33 | executor.setKeepAliveSeconds(60); 34 | executor.setMaxPoolSize(Integer.MAX_VALUE); 35 | executor.setThreadNamePrefix("clientMessageTaskPool"); 36 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 37 | executor.initialize(); 38 | return executor; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/panda/controller/socket/SocketClientController.java: -------------------------------------------------------------------------------- 1 | package com.panda.controller.socket; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.core.ResponseEntity; 5 | import com.panda.core.ServiceException; 6 | import com.panda.model.ClientParamVo; 7 | import com.panda.service.SocketClientService; 8 | import com.panda.service.impl.SocketClientServiceImpl; 9 | import com.panda.utils.socket.client.SocketClient; 10 | import com.panda.utils.socket.dto.ClientSendDto; 11 | import com.panda.utils.socket.enums.FunctionCodeEnum; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 14 | import org.springframework.util.StringUtils; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import javax.annotation.Resource; 18 | import java.util.Set; 19 | 20 | /** 21 | * @author 丁许 22 | * @date 2019-01-25 9:46 23 | */ 24 | @RestController 25 | @RequestMapping("/socket-client") 26 | @Slf4j 27 | public class SocketClientController { 28 | 29 | @Resource(name = "clientTaskPool") 30 | private ThreadPoolTaskExecutor clientExecutor; 31 | 32 | @Resource 33 | private SocketClientService socketClientService; 34 | 35 | /** 36 | * @param paramVo 用户id 37 | * 38 | * @return 是否操作成功 39 | */ 40 | @PostMapping("/start") 41 | public ResponseEntity startClient(@RequestBody ClientParamVo paramVo) { 42 | String userId = paramVo.getUserId(); 43 | socketClientService.startOneClient(userId); 44 | return ResponseEntity.success(); 45 | } 46 | 47 | /** 48 | * 关闭客户端 49 | * 50 | * @param paramVo userId 51 | * 52 | * @return 是否操作成功 53 | */ 54 | @PostMapping("/close") 55 | public ResponseEntity closeClient(@RequestBody ClientParamVo paramVo) { 56 | String userId = paramVo.getUserId(); 57 | socketClientService.closeOneClient(userId); 58 | return ResponseEntity.success(); 59 | } 60 | 61 | @GetMapping("/get-users") 62 | public ResponseEntity> getUsers() { 63 | return ResponseEntity.success(SocketClientServiceImpl.existSocketClientMap.keySet()); 64 | } 65 | 66 | @PostMapping("/send-message") 67 | public ResponseEntity sendMessage(@RequestBody ClientParamVo paramVo) { 68 | if (StringUtils.isEmpty(paramVo.getUserId()) || StringUtils.isEmpty(paramVo.getMessage())) { 69 | throw new ServiceException("参数不全"); 70 | } 71 | if (!SocketClientServiceImpl.existSocketClientMap.containsKey(paramVo.getUserId())) { 72 | throw new ServiceException("并没有客户端连接"); 73 | } 74 | SocketClient client = SocketClientServiceImpl.existSocketClientMap.get(paramVo.getUserId()).getSocketClient(); 75 | ClientSendDto dto = new ClientSendDto(); 76 | dto.setFunctionCode(FunctionCodeEnum.MESSAGE.getValue()); 77 | dto.setUserId(paramVo.getUserId()); 78 | dto.setMessage(paramVo.getMessage()); 79 | client.println(JSONObject.toJSONString(dto)); 80 | return ResponseEntity.success(); 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/panda/controller/socket/SocketServerController.java: -------------------------------------------------------------------------------- 1 | package com.panda.controller.socket; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.core.ResponseEntity; 5 | import com.panda.core.ServiceException; 6 | import com.panda.model.ServerParamVo; 7 | import com.panda.utils.socket.dto.ServerSendDto; 8 | import com.panda.utils.socket.enums.FunctionCodeEnum; 9 | import com.panda.utils.socket.server.Connection; 10 | import com.panda.utils.socket.server.SocketServer; 11 | import org.springframework.util.StringUtils; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.annotation.Resource; 15 | import java.util.concurrent.ConcurrentMap; 16 | 17 | /** 18 | * @author 丁许 19 | * @date 2019-01-25 8:54 20 | */ 21 | @RestController 22 | @RequestMapping("/socket-server") 23 | public class SocketServerController { 24 | 25 | @Resource 26 | private SocketServer socketServer; 27 | 28 | @GetMapping("/get-users") 29 | public ResponseEntity getLoginUsers() { 30 | ConcurrentMap userMaps = socketServer.getExistSocketMap(); 31 | JSONObject result=new JSONObject(); 32 | result.put("total",userMaps.keySet().size()); 33 | result.put("dataList",userMaps.keySet()); 34 | return ResponseEntity.success(result); 35 | } 36 | 37 | @PostMapping("/send-message") 38 | public ResponseEntity sendMessage(@RequestBody ServerParamVo paramVo) { 39 | 40 | if (StringUtils.isEmpty(paramVo.getUserId()) || StringUtils.isEmpty(paramVo.getMessage())) { 41 | throw new ServiceException("参数不全"); 42 | } 43 | if (!socketServer.getExistSocketMap().containsKey(paramVo.getUserId())) { 44 | throw new ServiceException("并没有客户端连接"); 45 | } 46 | Connection connection = socketServer.getExistSocketMap().get(paramVo.getUserId()); 47 | ServerSendDto dto = new ServerSendDto(); 48 | dto.setFunctionCode(FunctionCodeEnum.MESSAGE.getValue()); 49 | dto.setStatusCode(20000); 50 | dto.setMessage(paramVo.getMessage()); 51 | connection.println(JSONObject.toJSONString(dto)); 52 | return ResponseEntity.success(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/panda/core/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.panda.core; 2 | 3 | /** 4 | * 系统错误码,规范系统异常的构造 5 | * 6 | * @ClassName:ErrorCode 7 | * @Description:TODO 8 | * @author:huangyongfa 9 | * @date:2017年08月08日 10 | */ 11 | public enum ErrorCode { 12 | 13 | /** 14 | * 404找不到出错 15 | */ 16 | ERROR404(404), 17 | 18 | /** 19 | * 会话超时,请重新登陆 20 | */ 21 | ESYS0001(50014), 22 | 23 | /** 24 | * 用户鉴权失败 25 | */ 26 | ESYS9998(50008), 27 | 28 | /** 29 | * 系统内部异常 30 | */ 31 | ESYS9999(40001), 32 | 33 | /** 34 | * 用户重复登录 35 | */ 36 | EUOP0001(50012), 37 | 38 | /** 39 | * 参数错误 40 | */ 41 | ESYS10000(40002), 42 | 43 | /** 44 | * 操作成功! 45 | */ 46 | IPER0001(20000); 47 | 48 | private Integer value; 49 | 50 | private ErrorCode(Integer value) { 51 | this.value = value; 52 | } 53 | 54 | public Integer getValue() { 55 | return value; 56 | } 57 | 58 | public void setValue(Integer value) { 59 | this.value = value; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/panda/core/ErrorInfoEntity.java: -------------------------------------------------------------------------------- 1 | package com.panda.core; 2 | 3 | /** 4 | * Created With User-Center 5 | * 6 | * @author ChenHao 7 | * @date 2018/7/5 8 | * Target 9 | */ 10 | public interface ErrorInfoEntity { 11 | 12 | /** 13 | * 获取错误信息 14 | * 15 | * @return 错误信息 16 | */ 17 | String getErrorMsg(); 18 | 19 | /** 20 | * 获取错误码 21 | * 22 | * @return 错误码 23 | */ 24 | Integer getErrorCode(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/panda/core/ResponseEntity.java: -------------------------------------------------------------------------------- 1 | package com.panda.core; 2 | 3 | /** 4 | * 对请求详细的结果进行重新封装,规范接口返回值 5 | * 6 | * @param 7 | * 8 | * @ClassName:ResponseEntity 9 | * @author:huangyongfa 10 | * @date:2017年08月08日 11 | */ 12 | public class ResponseEntity { 13 | 14 | private Integer code; 15 | 16 | private String message; 17 | 18 | private T data; 19 | 20 | /** 21 | * 静态构造方法 success时调用 22 | * 23 | * @param 24 | * 25 | * @return 26 | */ 27 | public static ResponseEntity success() { 28 | return new ResponseEntity().OK(); 29 | } 30 | 31 | /** 32 | * 静态构造方法 success时调用 33 | * 34 | * @param data 35 | * @param 36 | * 37 | * @return 38 | */ 39 | public static ResponseEntity success(T data) { 40 | return new ResponseEntity().OK(data); 41 | } 42 | 43 | /** 44 | * 静态构造方法 success时调用 45 | * 46 | * @param data 47 | * @param message 48 | * @param 49 | * 50 | * @return 51 | */ 52 | public static ResponseEntity success(T data, String message) { 53 | return new ResponseEntity().OK(data, message); 54 | } 55 | 56 | /** 57 | * 通过错误信息实体异常构造方法 58 | * 59 | * @param errorInfoEntity 错误信息实体类 60 | * 61 | * @return 异常信息的返回实例 62 | */ 63 | public static ResponseEntity fail(ErrorInfoEntity errorInfoEntity) { 64 | return new ResponseEntity().ERROR(new ServiceException(errorInfoEntity)); 65 | } 66 | 67 | /** 68 | * 异常构造方法 69 | * 70 | * @param serviceException 71 | * 72 | * @return 73 | */ 74 | public static ResponseEntity fail(ServiceException serviceException) { 75 | return new ResponseEntity().ERROR(serviceException); 76 | } 77 | 78 | /** 79 | * 异常构造方法 80 | * 81 | * @param serviceException 82 | * 83 | * @return 84 | */ 85 | public static ResponseEntity fail(T data, ServiceException serviceException) { 86 | return new ResponseEntity().ERROR(data, serviceException); 87 | } 88 | 89 | public Integer getCode() { 90 | return code; 91 | } 92 | 93 | /** 94 | * 私有化,请使用统一构造方法 95 | * 96 | * @param code 97 | */ 98 | @Deprecated 99 | public void setCode(Integer code) { 100 | this.code = code; 101 | } 102 | 103 | public String getMessage() { 104 | return message; 105 | } 106 | 107 | /** 108 | * 私有化,请使用统一构造方法 109 | * 110 | * @param message 111 | */ 112 | @Deprecated 113 | public void setMessage(String message) { 114 | this.message = message; 115 | } 116 | 117 | public T getData() { 118 | return data; 119 | } 120 | 121 | /** 122 | * 私有化,请使用统一构造方法 123 | * 124 | * @param data 125 | */ 126 | @Deprecated 127 | public void setData(T data) { 128 | this.data = data; 129 | } 130 | 131 | /** 132 | * 正常业务返回值 133 | * 134 | * @return 135 | */ 136 | @Deprecated 137 | public ResponseEntity OK() { 138 | return OK(null); 139 | } 140 | 141 | /** 142 | * 正常业务返回值 143 | * 144 | * @param data 145 | * 146 | * @return 147 | */ 148 | @Deprecated 149 | public ResponseEntity OK(T data) { 150 | this.setCode(ErrorCode.IPER0001.getValue()); 151 | if (null != data) { 152 | this.setData(data); 153 | } 154 | return this; 155 | } 156 | 157 | /** 158 | * 正常业务返回值,带message 159 | * 160 | * @param data 161 | * @param message 162 | * 163 | * @return 164 | */ 165 | @Deprecated 166 | public ResponseEntity OK(T data, String message) { 167 | this.setCode(ErrorCode.IPER0001.getValue()); 168 | if (null != data) { 169 | this.setData(data); 170 | } 171 | if (null != message) { 172 | this.setMessage(message); 173 | } 174 | return this; 175 | } 176 | 177 | /** 178 | * 异常构造方法 179 | * 180 | * @param serviceException 181 | * 182 | * @return 183 | */ 184 | @Deprecated 185 | public ResponseEntity ERROR(ServiceException serviceException) { 186 | this.setMessage(serviceException.getMessage()); 187 | this.setCode(serviceException.getErrorCode()); 188 | return this; 189 | } 190 | 191 | /** 192 | * 异常构造方法 193 | * 194 | * @param serviceException 195 | * 196 | * @return 197 | */ 198 | @Deprecated 199 | public ResponseEntity ERROR(T data, ServiceException serviceException) { 200 | this.setData(data); 201 | this.setMessage(serviceException.getMessage()); 202 | this.setCode(serviceException.getErrorCode()); 203 | return this; 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/com/panda/core/ServiceException.java: -------------------------------------------------------------------------------- 1 | package com.panda.core; 2 | 3 | public class ServiceException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | private Integer errorCode; 8 | 9 | /** 10 | * 通过错误信息实例构造ServiceException 11 | * 12 | * @param errorInfoEntity 错误信息Enum实例 13 | */ 14 | public ServiceException(ErrorInfoEntity errorInfoEntity) { 15 | super(errorInfoEntity.getErrorMsg()); 16 | this.errorCode = errorInfoEntity.getErrorCode(); 17 | } 18 | 19 | /** 20 | * 通过错误信息实例构造ServiceException,错误信息附带参数 21 | * 22 | * @param errorInfoEntity 错误信息Enum实例 23 | * @param info 需要在错误信息中添加的信息 24 | */ 25 | public ServiceException(ErrorInfoEntity errorInfoEntity, Object... info) { 26 | super(String.format(errorInfoEntity.getErrorMsg(), info)); 27 | this.errorCode = errorInfoEntity.getErrorCode(); 28 | } 29 | 30 | public ServiceException(String message) { 31 | super(message); 32 | this.errorCode = ErrorCode.ESYS9999.getValue(); 33 | } 34 | 35 | public ServiceException(int errorCode, String message) { 36 | super(message); 37 | this.errorCode = errorCode; 38 | } 39 | 40 | /** 41 | * 可自定义Message 42 | * Creates a new instance of ServiceException. 43 | * 44 | * @param message 45 | * @param errorCode 46 | */ 47 | public ServiceException(String message, ErrorCode errorCode) { 48 | super(message); 49 | this.errorCode = errorCode.getValue(); 50 | } 51 | 52 | public ServiceException(Throwable cause) { 53 | super(cause); 54 | } 55 | 56 | public ServiceException(String message, Throwable cause) { 57 | super(message, cause); 58 | this.errorCode = ErrorCode.ESYS9999.getValue(); 59 | } 60 | 61 | 62 | public Integer getErrorCode() { 63 | return errorCode; 64 | } 65 | 66 | public void setErrorCode(Integer errorCode) { 67 | this.errorCode = errorCode; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/panda/model/ClientParamVo.java: -------------------------------------------------------------------------------- 1 | package com.panda.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @author 丁许 9 | * @date 2019-01-25 9:48 10 | */ 11 | @Data 12 | public class ClientParamVo implements Serializable { 13 | 14 | private static final long serialVersionUID = 2822768619906469920L; 15 | 16 | private String userId; 17 | 18 | private String message; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/panda/model/ClientSocket.java: -------------------------------------------------------------------------------- 1 | package com.panda.model; 2 | 3 | import com.panda.utils.socket.client.SocketClient; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | import java.util.concurrent.ScheduledExecutorService; 8 | 9 | /** 10 | * @author 丁许 11 | * @date 2019-01-25 10:17 12 | */ 13 | @Data 14 | @AllArgsConstructor 15 | public class ClientSocket { 16 | 17 | private SocketClient socketClient; 18 | 19 | private ScheduledExecutorService clientHeartExecutor; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/panda/model/ServerParamVo.java: -------------------------------------------------------------------------------- 1 | package com.panda.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @author 丁许 9 | * @date 2019-01-25 14:20 10 | */ 11 | @Data 12 | public class ServerParamVo implements Serializable { 13 | 14 | private static final long serialVersionUID = 5267331270045085979L; 15 | 16 | private String userId; 17 | 18 | private String message; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/panda/service/SocketClientService.java: -------------------------------------------------------------------------------- 1 | package com.panda.service; 2 | 3 | /** 4 | * @author 丁许 5 | * @date 2019-01-25 9:49 6 | */ 7 | public interface SocketClientService { 8 | 9 | /** 10 | * 开始一个socket客户端 11 | * 12 | * @param userId 用户id 13 | */ 14 | void startOneClient(String userId); 15 | 16 | void closeOneClient(String userId); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/panda/service/impl/SocketClientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.panda.service.impl; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.core.ServiceException; 5 | import com.panda.model.ClientSocket; 6 | import com.panda.service.SocketClientService; 7 | import com.panda.utils.socket.client.SocketClient; 8 | import com.panda.utils.socket.constants.SocketConstant; 9 | import com.panda.utils.socket.dto.ClientSendDto; 10 | import com.panda.utils.socket.dto.ServerSendDto; 11 | import com.panda.utils.socket.enums.FunctionCodeEnum; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 14 | import org.springframework.stereotype.Service; 15 | 16 | import javax.annotation.Resource; 17 | import java.net.InetAddress; 18 | import java.net.UnknownHostException; 19 | import java.util.Date; 20 | import java.util.concurrent.*; 21 | 22 | /** 23 | * @author 丁许 24 | * @date 2019-01-25 9:50 25 | */ 26 | @Service 27 | @Slf4j 28 | public class SocketClientServiceImpl implements SocketClientService { 29 | 30 | /** 31 | * 全局缓存,用于存储已存在的socket客户端连接 32 | */ 33 | public static ConcurrentMap existSocketClientMap = new ConcurrentHashMap<>(); 34 | 35 | 36 | @Resource(name = "clientTaskPool") 37 | private ThreadPoolTaskExecutor clientExecutor; 38 | 39 | @Resource(name = "clientMessageTaskPool") 40 | private ThreadPoolTaskExecutor messageExecutor; 41 | 42 | @Override 43 | public void startOneClient(String userId) { 44 | if (existSocketClientMap.containsKey(userId)) { 45 | throw new ServiceException("该用户已登陆"); 46 | } 47 | //异步创建socket 48 | clientExecutor.execute(() -> { 49 | //新建一个socket连接 50 | SocketClient client; 51 | try { 52 | client = new SocketClient(InetAddress.getByName("127.0.0.1"), 60000); 53 | } catch (UnknownHostException e) { 54 | throw new ServiceException("socket新建失败"); 55 | } 56 | client.setLastOnTime(new Date()); 57 | 58 | ScheduledExecutorService clientHeartExecutor = Executors 59 | .newSingleThreadScheduledExecutor(r -> new Thread(r, "socket_client_heart_" + r.hashCode())); 60 | ClientSocket clientSocket = new ClientSocket(client,clientHeartExecutor); 61 | //登陆 62 | ClientSendDto dto = new ClientSendDto(); 63 | dto.setFunctionCode(FunctionCodeEnum.LOGIN.getValue()); 64 | dto.setUserId(userId); 65 | client.println(JSONObject.toJSONString(dto)); 66 | messageExecutor.submit(() -> { 67 | try { 68 | String message; 69 | while ((message = client.readLine()) != null) { 70 | log.info("客户端:{},获得消息:{}", userId, message); 71 | ServerSendDto serverSendDto; 72 | try { 73 | serverSendDto = JSONObject.parseObject(message, ServerSendDto.class); 74 | } catch (Exception e) { 75 | ClientSendDto sendDto = new ClientSendDto(); 76 | sendDto.setFunctionCode(FunctionCodeEnum.MESSAGE.getValue()); 77 | sendDto.setMessage("data error"); 78 | client.println(JSONObject.toJSONString(sendDto)); 79 | break; 80 | } 81 | Integer functionCode = serverSendDto.getFunctionCode(); 82 | if (functionCode.equals(FunctionCodeEnum.HEART.getValue())) { 83 | //心跳类型 84 | client.setLastOnTime(new Date()); 85 | } 86 | } 87 | } catch (Exception e) { 88 | log.error("客户端异常,userId:{},exception:{}", userId, e.getMessage()); 89 | client.close(); 90 | existSocketClientMap.remove(userId); 91 | } 92 | }); 93 | clientHeartExecutor.scheduleWithFixedDelay(() -> { 94 | try { 95 | 96 | Date lastOnTime = client.getLastOnTime(); 97 | long heartDuration = (new Date()).getTime() - lastOnTime.getTime(); 98 | if (heartDuration > SocketConstant.HEART_RATE) { 99 | //心跳超时,关闭当前线程 100 | log.error("心跳超时"); 101 | throw new Exception("服务端已断开socket"); 102 | } 103 | ClientSendDto heartDto = new ClientSendDto(); 104 | heartDto.setFunctionCode(FunctionCodeEnum.HEART.getValue()); 105 | client.println(JSONObject.toJSONString(heartDto)); 106 | } catch (Exception e) { 107 | log.error("客户端异常,userId:{},exception:{}", userId, e.getMessage()); 108 | client.close(); 109 | existSocketClientMap.remove(userId); 110 | clientHeartExecutor.shutdown(); 111 | } 112 | 113 | }, 0, 5, TimeUnit.SECONDS); 114 | existSocketClientMap.put(userId, clientSocket); 115 | }); 116 | } 117 | 118 | @Override 119 | public void closeOneClient(String userId) { 120 | if (!existSocketClientMap.containsKey(userId)) { 121 | throw new ServiceException("该用户未登陆,不能关闭"); 122 | } 123 | ClientSocket clientSocket = existSocketClientMap.get(userId); 124 | clientSocket.getClientHeartExecutor().shutdown(); 125 | clientSocket.getSocketClient().close(); 126 | existSocketClientMap.remove(userId); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/client/SocketClient.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.client; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.utils.socket.dto.ClientSendDto; 5 | import com.panda.utils.socket.dto.ServerReceiveDto; 6 | import com.panda.utils.socket.enums.FunctionCodeEnum; 7 | import lombok.Data; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.io.*; 11 | import java.net.InetAddress; 12 | import java.net.Socket; 13 | import java.net.UnknownHostException; 14 | import java.util.Date; 15 | 16 | /** 17 | * @author 丁许 18 | */ 19 | @Slf4j 20 | @Data 21 | public class SocketClient { 22 | 23 | private Socket socket; 24 | 25 | private Date lastOnTime; 26 | 27 | public SocketClient(InetAddress ip, int port) { 28 | try { 29 | socket = new Socket(ip, port); 30 | socket.setKeepAlive(true); 31 | } catch (IOException e) { 32 | // TODO Auto-generated catch block 33 | log.error(e.getMessage()); 34 | } 35 | } 36 | 37 | public void println(String message) { 38 | PrintWriter writer; 39 | try { 40 | writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true); 41 | writer.println(message); 42 | } catch (IOException e) { 43 | // TODO Auto-generated catch block 44 | log.error(e.getMessage()); 45 | } 46 | } 47 | 48 | /** 49 | * This function blocks. 50 | * 51 | * @return 52 | */ 53 | public String readLine() throws Exception { 54 | BufferedReader reader; 55 | reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 56 | return reader.readLine(); 57 | } 58 | 59 | /** 60 | * Ready for use. 61 | */ 62 | public void close() { 63 | try { 64 | // Send a message to tell the server to close the connection. 65 | PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true); 66 | ServerReceiveDto dto = new ServerReceiveDto(); 67 | dto.setFunctionCode(FunctionCodeEnum.CLOSE.getValue()); 68 | writer.println(JSONObject.toJSONString(dto)); 69 | 70 | if (socket != null && !socket.isClosed()) { 71 | socket.close(); 72 | } 73 | } catch (IOException e) { 74 | // TODO Auto-generated catch block 75 | log.error(e.getMessage()); 76 | } 77 | } 78 | 79 | public static void main(String[] args) throws UnknownHostException, InterruptedException { 80 | SocketClient client = new SocketClient(InetAddress.getByName("127.0.0.1"), 60000); 81 | ClientSendDto dto = new ClientSendDto(); 82 | dto.setFunctionCode(FunctionCodeEnum.LOGIN.getValue()); 83 | dto.setUserId("test1"); 84 | dto.setMessage("登陆信息啦\n"); 85 | // Thread.sleep(6*1000); 86 | client.println(JSONObject.toJSONString(dto)); 87 | while (true) { 88 | } 89 | // client.close(); 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/constants/SocketConstant.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.constants; 2 | 3 | /** 4 | * @author 丁许 5 | * @date 2019-01-24 20:29 6 | */ 7 | public class SocketConstant { 8 | 9 | /** 10 | * 心跳频率为10s 11 | */ 12 | public static final int HEART_RATE = 10*1000; 13 | 14 | /** 15 | * 允许一个连接身份验证延迟10s,10s后还没有完成身份验证则自动关闭该客户端链接的socket 16 | */ 17 | public static final int LOGIN_DELAY = 10*1000; 18 | 19 | /** 20 | * 最多开2000个socket线程,超过的直接拒绝 21 | */ 22 | public static final int MAX_SOCKET_THREAD_NUM = 2000; 23 | 24 | /** 25 | * 重试次数:3 26 | */ 27 | public static final int RETRY_COUNT = 3; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/dto/ClientSendDto.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @author 丁许 9 | * @date 2019-01-24 16:11 10 | */ 11 | @Data 12 | public class ClientSendDto implements Serializable { 13 | 14 | private static final long serialVersionUID = 97085384412852967L; 15 | 16 | /** 17 | * 功能码 0 心跳 1 登陆 2 登出 3 发送消息 18 | */ 19 | private Integer functionCode; 20 | 21 | /** 22 | * 用户id 23 | */ 24 | private String userId; 25 | 26 | /** 27 | * 这边假设是string的消息体 28 | */ 29 | private String message; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/dto/ServerReceiveDto.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @author 丁许 9 | * @date 2019-01-24 15:10 10 | */ 11 | @Data 12 | public class ServerReceiveDto implements Serializable { 13 | 14 | private static final long serialVersionUID = 6600253865619639317L; 15 | 16 | /** 17 | * 功能码 0 心跳 1 登陆 2 登出 3 发送消息 18 | */ 19 | private Integer functionCode; 20 | 21 | /** 22 | * 用户id 23 | */ 24 | private String userId; 25 | 26 | /** 27 | * 这边假设是string的消息体 28 | */ 29 | private String message; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/dto/ServerSendDto.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @author 丁许 9 | * @date 2019-01-24 15:13 10 | */ 11 | @Data 12 | public class ServerSendDto implements Serializable { 13 | 14 | private static final long serialVersionUID = -7453297551797390215L; 15 | 16 | /** 17 | * 状态码 20000 成功,否则有errorMessage 18 | */ 19 | private Integer statusCode; 20 | 21 | private String message; 22 | 23 | /** 24 | * 功能码 25 | */ 26 | private Integer functionCode; 27 | 28 | /** 29 | * 错误消息 30 | */ 31 | private String errorMessage; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/enums/FunctionCodeEnum.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.enums; 2 | 3 | /** 4 | * 功能码 0 心跳 1 登陆 2 登出 3 发送消息 5 | * 6 | * @author 丁许 7 | * @date 2019-01-24 15:53 8 | */ 9 | public enum FunctionCodeEnum { 10 | /** 11 | * 心跳 12 | */ 13 | HEART(0, "心跳"), 14 | /** 15 | * 用户鉴权 16 | */ 17 | LOGIN(1, "登陆"), 18 | 19 | /** 20 | * 客户端关闭 21 | */ 22 | CLOSE(2, "客户端关闭"), 23 | 24 | /** 25 | * 发送消息 26 | */ 27 | MESSAGE(3, "发送消息"); 28 | 29 | private Integer value; 30 | 31 | private String desc; 32 | 33 | FunctionCodeEnum(Integer value, String desc) { 34 | this.value = value; 35 | this.desc = desc; 36 | } 37 | 38 | public Integer getValue() { 39 | return value; 40 | } 41 | 42 | public void setValue(Integer value) { 43 | this.value = value; 44 | } 45 | 46 | public String getDesc() { 47 | return desc; 48 | } 49 | 50 | public void setDesc(String desc) { 51 | this.desc = desc; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/handler/LoginHandler.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.handler; 2 | 3 | /** 4 | * @author 丁许 5 | * @date 2019-01-23 22:28 6 | */ 7 | public interface LoginHandler { 8 | 9 | /** 10 | * client登陆的处理函数 11 | * 12 | * @param userId 用户id 13 | * 14 | * @return 是否验证通过 15 | */ 16 | boolean canLogin(String userId); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/handler/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.handler; 2 | 3 | import com.panda.utils.socket.dto.ServerReceiveDto; 4 | import com.panda.utils.socket.server.Connection; 5 | 6 | /** 7 | * 处理消息的接口 8 | * 9 | * @author 丁许 10 | */ 11 | public interface MessageHandler { 12 | 13 | /** 14 | * 获得消息的处理函数 15 | * 16 | * @param connection 封装了客户端的socket 17 | * @param dto 接收到的dto 18 | */ 19 | void onReceive(Connection connection, ServerReceiveDto dto); 20 | } -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/server/Connection.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.server; 2 | 3 | import lombok.Data; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStreamWriter; 8 | import java.io.PrintWriter; 9 | import java.net.Socket; 10 | import java.util.Date; 11 | 12 | import static com.panda.utils.socket.constants.SocketConstant.RETRY_COUNT; 13 | 14 | /** 15 | * 封装socket添加println方法 16 | * 17 | * @author 丁许 18 | */ 19 | @Slf4j 20 | @Data 21 | public class Connection { 22 | 23 | /** 24 | * 当前的socket连接实例 25 | */ 26 | private Socket socket; 27 | 28 | /** 29 | * 当前连接线程 30 | */ 31 | private ConnectionThread connectionThread; 32 | 33 | /** 34 | * 当前连接是否登陆 35 | */ 36 | private boolean isLogin; 37 | 38 | /** 39 | * 存储当前的user信息 40 | */ 41 | private String userId; 42 | 43 | /** 44 | * 创建时间 45 | */ 46 | private Date createTime; 47 | 48 | /** 49 | * 最后一次更新时间,用于判断心跳 50 | */ 51 | private Date lastOnTime; 52 | 53 | public Connection(Socket socket, ConnectionThread connectionThread) { 54 | this.socket = socket; 55 | this.connectionThread = connectionThread; 56 | } 57 | 58 | public void println(String message) { 59 | int count = 0; 60 | PrintWriter writer; 61 | do { 62 | try { 63 | writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true); 64 | writer.println(message); 65 | break; 66 | } catch (IOException e) { 67 | count++; 68 | if (count >= RETRY_COUNT) { 69 | //重试多次失败,说明client端socket异常 70 | this.connectionThread.stopRunning(); 71 | } 72 | } 73 | try { 74 | Thread.sleep(2 * 1000); 75 | } catch (InterruptedException e1) { 76 | log.error("Connection.println.IOException interrupt,userId:{}", userId); 77 | } 78 | } while (count < 3); 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/server/ConnectionThread.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.server; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.utils.socket.dto.ServerReceiveDto; 5 | import com.panda.utils.socket.dto.ServerSendDto; 6 | import com.panda.utils.socket.enums.FunctionCodeEnum; 7 | import lombok.Data; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.net.Socket; 14 | import java.util.Date; 15 | 16 | /** 17 | * 每一个client连接开一个线程 18 | * 19 | * @author 丁许 20 | */ 21 | @Slf4j 22 | @Data 23 | public class ConnectionThread extends Thread { 24 | 25 | /** 26 | * 客户端的socket 27 | */ 28 | private Socket socket; 29 | 30 | /** 31 | * 服务socket 32 | */ 33 | private SocketServer socketServer; 34 | 35 | /** 36 | * 封装的客户端连接socket 37 | */ 38 | private Connection connection; 39 | 40 | /** 41 | * 判断当前连接是否运行 42 | */ 43 | private boolean isRunning; 44 | 45 | public ConnectionThread(Socket socket, SocketServer socketServer) { 46 | this.socket = socket; 47 | this.socketServer = socketServer; 48 | connection = new Connection(socket, this); 49 | Date now = new Date(); 50 | connection.setCreateTime(now); 51 | connection.setLastOnTime(now); 52 | isRunning = true; 53 | } 54 | 55 | @Override 56 | public void run() { 57 | while (isRunning) { 58 | // Check whether the socket is closed. 59 | if (socket.isClosed()) { 60 | isRunning = false; 61 | break; 62 | } 63 | BufferedReader reader; 64 | try { 65 | reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 66 | String message; 67 | while ((message = reader.readLine()) != null) { 68 | log.info("服务端收到消息:" + message); 69 | ServerReceiveDto receiveDto; 70 | try { 71 | receiveDto = JSONObject.parseObject(message, ServerReceiveDto.class); 72 | } catch (Exception e) { 73 | ServerSendDto dto = new ServerSendDto(); 74 | dto.setStatusCode(999); 75 | dto.setErrorMessage("data error"); 76 | connection.println(JSONObject.toJSONString(dto)); 77 | break; 78 | } 79 | Integer functionCode = receiveDto.getFunctionCode(); 80 | if (functionCode.equals(FunctionCodeEnum.HEART.getValue())) { 81 | //心跳类型 82 | connection.setLastOnTime(new Date()); 83 | ServerSendDto dto = new ServerSendDto(); 84 | dto.setFunctionCode(FunctionCodeEnum.HEART.getValue()); 85 | connection.println(JSONObject.toJSONString(dto)); 86 | } else if (functionCode.equals(FunctionCodeEnum.LOGIN.getValue())) { 87 | //登陆,身份验证 88 | String userId = receiveDto.getUserId(); 89 | if (socketServer.getLoginHandler().canLogin(userId)) { 90 | connection.setLogin(true); 91 | connection.setUserId(userId); 92 | if (socketServer.getExistSocketMap().containsKey(userId)) { 93 | //存在已登录的用户,发送登出指令并主动关闭该socket 94 | Connection existConnection = socketServer.getExistSocketMap().get(userId); 95 | ServerSendDto dto = new ServerSendDto(); 96 | dto.setStatusCode(999); 97 | dto.setFunctionCode(FunctionCodeEnum.MESSAGE.getValue()); 98 | dto.setErrorMessage("force logout"); 99 | existConnection.println(JSONObject.toJSONString(dto)); 100 | existConnection.getConnectionThread().stopRunning(); 101 | log.error("用户被客户端重入踢出,userId:{}", userId); 102 | } 103 | //添加到已登录map中 104 | socketServer.getExistSocketMap().put(userId, connection); 105 | } else { 106 | //用户鉴权失败 107 | ServerSendDto dto = new ServerSendDto(); 108 | dto.setStatusCode(999); 109 | dto.setFunctionCode(FunctionCodeEnum.MESSAGE.getValue()); 110 | dto.setErrorMessage("user valid failed"); 111 | connection.println(JSONObject.toJSONString(dto)); 112 | log.error("用户鉴权失败,userId:{}", userId); 113 | } 114 | } else if (functionCode.equals(FunctionCodeEnum.MESSAGE.getValue())) { 115 | //发送一些其他消息等 116 | socketServer.getMessageHandler().onReceive(connection, receiveDto); 117 | } else if (functionCode.equals(FunctionCodeEnum.CLOSE.getValue())) { 118 | //主动关闭客户端socket 119 | log.info("客户端主动登出socket"); 120 | this.stopRunning(); 121 | } 122 | 123 | } 124 | } catch (IOException e) { 125 | log.error("ConnectionThread.run failed. IOException:{}", e.getMessage()); 126 | this.stopRunning(); 127 | } 128 | } 129 | } 130 | 131 | public void stopRunning() { 132 | if (this.connection.isLogin()) { 133 | log.info("停止一个socket连接,ip:{},userId:{}", this.socket.getInetAddress().toString(), 134 | this.connection.getUserId()); 135 | } else { 136 | log.info("停止一个还未身份验证的socket连接,ip:{}", this.socket.getInetAddress().toString()); 137 | } 138 | isRunning = false; 139 | try { 140 | socket.close(); 141 | } catch (IOException e) { 142 | log.error("ConnectionThread.stopRunning failed.exception:{}", e); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/server/ListeningThread.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.server; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.panda.utils.socket.constants.SocketConstant; 5 | import com.panda.utils.socket.dto.ServerSendDto; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.io.IOException; 9 | import java.io.OutputStreamWriter; 10 | import java.io.PrintWriter; 11 | import java.net.ServerSocket; 12 | import java.net.Socket; 13 | 14 | @Slf4j 15 | class ListeningThread extends Thread { 16 | 17 | private SocketServer socketServer; 18 | 19 | private ServerSocket serverSocket; 20 | 21 | private boolean isRunning; 22 | 23 | public ListeningThread(SocketServer socketServer) { 24 | this.socketServer = socketServer; 25 | this.serverSocket = socketServer.getServerSocket(); 26 | isRunning = true; 27 | log.info("socket服务端开始监听"); 28 | } 29 | 30 | @Override 31 | public void run() { 32 | while (isRunning) { 33 | if (serverSocket.isClosed()) { 34 | isRunning = false; 35 | break; 36 | } 37 | try { 38 | Socket socket; 39 | socket = serverSocket.accept(); 40 | if (socketServer.getExistConnectionThreadList().size() > SocketConstant.MAX_SOCKET_THREAD_NUM) { 41 | //超过线程数量 42 | PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true); 43 | ServerSendDto dto = new ServerSendDto(); 44 | dto.setStatusCode(999); 45 | dto.setErrorMessage("已超过连接最大数限制,请稍后再试"); 46 | writer.println(JSONObject.toJSONString(dto)); 47 | socket.close(); 48 | } 49 | //设置超时时间为5s(有心跳机制了不需要设置) 50 | // socket.setSoTimeout(5 * 1000); 51 | ConnectionThread connectionThread = new ConnectionThread(socket, socketServer); 52 | socketServer.getExistConnectionThreadList().add(connectionThread); 53 | //todo:这边最好用线程池 54 | connectionThread.start(); 55 | } catch (IOException e) { 56 | log.error("ListeningThread.run failed,exception:{}", e.getMessage()); 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * 关闭所有的socket客户端连接的线程 63 | */ 64 | public void stopRunning() { 65 | for (ConnectionThread currentThread : socketServer.getExistConnectionThreadList()) { 66 | currentThread.stopRunning(); 67 | } 68 | isRunning = false; 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/com/panda/utils/socket/server/SocketServer.java: -------------------------------------------------------------------------------- 1 | package com.panda.utils.socket.server; 2 | 3 | import com.panda.utils.socket.constants.SocketConstant; 4 | import com.panda.utils.socket.handler.LoginHandler; 5 | import com.panda.utils.socket.handler.MessageHandler; 6 | import lombok.Data; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.io.IOException; 10 | import java.net.ServerSocket; 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.Date; 14 | import java.util.List; 15 | import java.util.concurrent.*; 16 | 17 | /** 18 | * @author 丁许 19 | */ 20 | @Slf4j 21 | @Data 22 | public class SocketServer { 23 | 24 | private ServerSocket serverSocket; 25 | 26 | /** 27 | * 服务监听主线程 28 | */ 29 | private ListeningThread listeningThread; 30 | 31 | /** 32 | * 消息处理器 33 | */ 34 | private MessageHandler messageHandler; 35 | 36 | /** 37 | * 登陆处理器 38 | */ 39 | private LoginHandler loginHandler; 40 | 41 | /** 42 | * 用户扫已有的socket处理线程 43 | * 1. 没有的线程不引用 44 | * 2. 关注是否有心跳 45 | * 3. 关注是否超过登陆时间 46 | */ 47 | private ScheduledExecutorService scheduleSocketMonitorExecutor = Executors 48 | .newSingleThreadScheduledExecutor(r -> new Thread(r, "socket_monitor_" + r.hashCode())); 49 | 50 | /** 51 | * 存储只要有socket处理的线程 52 | */ 53 | private List existConnectionThreadList = Collections.synchronizedList(new ArrayList<>()); 54 | 55 | /** 56 | * 中间list,用于遍历的时候删除 57 | */ 58 | private List noConnectionThreadList = Collections.synchronizedList(new ArrayList<>()); 59 | 60 | /** 61 | * 存储当前由用户信息活跃的的socket线程 62 | */ 63 | private ConcurrentMap existSocketMap = new ConcurrentHashMap<>(); 64 | 65 | public SocketServer(int port) { 66 | try { 67 | serverSocket = new ServerSocket(port); 68 | } catch (IOException e) { 69 | log.error("本地socket服务启动失败.exception:{}", e); 70 | } 71 | } 72 | 73 | /** 74 | * 开一个线程来开启本地socket服务,开启一个monitor线程 75 | */ 76 | public void start() { 77 | listeningThread = new ListeningThread(this); 78 | listeningThread.start(); 79 | //每隔1s扫一次ThreadList 80 | scheduleSocketMonitorExecutor.scheduleWithFixedDelay(() -> { 81 | Date now = new Date(); 82 | //删除list中没有用的thread引用 83 | existConnectionThreadList.forEach(connectionThread -> { 84 | if (!connectionThread.isRunning()) { 85 | noConnectionThreadList.add(connectionThread); 86 | } else { 87 | //还在运行的线程需要判断心跳是否ok以及是否身份验证了 88 | Date lastOnTime = connectionThread.getConnection().getLastOnTime(); 89 | long heartDuration = now.getTime() - lastOnTime.getTime(); 90 | if (heartDuration > SocketConstant.HEART_RATE) { 91 | //心跳超时,关闭当前线程 92 | log.error("心跳超时"); 93 | connectionThread.stopRunning(); 94 | } 95 | if (!connectionThread.getConnection().isLogin()) { 96 | //还没有用户登陆成功 97 | Date createTime = connectionThread.getConnection().getCreateTime(); 98 | long loginDuration = now.getTime() - createTime.getTime(); 99 | if (loginDuration > SocketConstant.LOGIN_DELAY) { 100 | //身份验证超时 101 | log.error("身份验证超时"); 102 | connectionThread.stopRunning(); 103 | } 104 | } 105 | } 106 | }); 107 | noConnectionThreadList.forEach(connectionThread -> { 108 | existConnectionThreadList.remove(connectionThread); 109 | if (connectionThread.getConnection().isLogin()) { 110 | //说明用户已经身份验证成功了,需要删除map 111 | this.existSocketMap.remove(connectionThread.getConnection().getUserId()); 112 | } 113 | }); 114 | noConnectionThreadList.clear(); 115 | }, 0, 1, TimeUnit.SECONDS); 116 | } 117 | 118 | /** 119 | * 关闭本地socket服务 120 | */ 121 | public void close() { 122 | try { 123 | //先关闭monitor线程,防止遍历list的时候 124 | scheduleSocketMonitorExecutor.shutdownNow(); 125 | if (serverSocket != null && !serverSocket.isClosed()) { 126 | listeningThread.stopRunning(); 127 | listeningThread.suspend(); 128 | listeningThread.stop(); 129 | 130 | serverSocket.close(); 131 | } 132 | } catch (IOException e) { 133 | log.error("SocketServer.close failed.exception:{}", e); 134 | } 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /src/main/resources/Socket测试.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "736c14d3-6dc1-42d7-ac52-ef990034a044", 4 | "name": "Socket测试", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "服务端", 10 | "item": [ 11 | { 12 | "name": "获得当前连接用户列表", 13 | "request": { 14 | "method": "GET", 15 | "header": [], 16 | "body": { 17 | "mode": "raw", 18 | "raw": "" 19 | }, 20 | "url": { 21 | "raw": "{{ip}}/socket-server/get-users", 22 | "host": [ 23 | "{{ip}}" 24 | ], 25 | "path": [ 26 | "socket-server", 27 | "get-users" 28 | ] 29 | } 30 | }, 31 | "response": [] 32 | }, 33 | { 34 | "name": "发送一个消息", 35 | "request": { 36 | "method": "POST", 37 | "header": [ 38 | { 39 | "key": "Content-Type", 40 | "name": "Content-Type", 41 | "value": "application/json", 42 | "type": "text" 43 | } 44 | ], 45 | "body": { 46 | "mode": "raw", 47 | "raw": "{\n\t\"userId\":\"dingxu2\",\n\t\"message\":\"app打开视频啦\"\n}" 48 | }, 49 | "url": { 50 | "raw": "{{ip}}/socket-server/send-message", 51 | "host": [ 52 | "{{ip}}" 53 | ], 54 | "path": [ 55 | "socket-server", 56 | "send-message" 57 | ] 58 | } 59 | }, 60 | "response": [] 61 | } 62 | ] 63 | }, 64 | { 65 | "name": "客户端", 66 | "item": [ 67 | { 68 | "name": "开始一个socket客户端", 69 | "request": { 70 | "method": "POST", 71 | "header": [ 72 | { 73 | "key": "Content-Type", 74 | "name": "Content-Type", 75 | "value": "application/json", 76 | "type": "text" 77 | } 78 | ], 79 | "body": { 80 | "mode": "raw", 81 | "raw": "{\n\t\"userId\":\"dingxu1\"\n}" 82 | }, 83 | "url": { 84 | "raw": "{{ip}}/socket-client/start", 85 | "host": [ 86 | "{{ip}}" 87 | ], 88 | "path": [ 89 | "socket-client", 90 | "start" 91 | ] 92 | } 93 | }, 94 | "response": [] 95 | }, 96 | { 97 | "name": "发送一个消息", 98 | "request": { 99 | "method": "POST", 100 | "header": [ 101 | { 102 | "key": "Content-Type", 103 | "name": "Content-Type", 104 | "type": "text", 105 | "value": "application/json" 106 | } 107 | ], 108 | "body": { 109 | "mode": "raw", 110 | "raw": "{\n\t\"userId\":\"dingxu1\",\n\t\"message\":\"app收到消息啦\"\n}" 111 | }, 112 | "url": { 113 | "raw": "{{ip}}/socket-client/send-message", 114 | "host": [ 115 | "{{ip}}" 116 | ], 117 | "path": [ 118 | "socket-client", 119 | "send-message" 120 | ] 121 | } 122 | }, 123 | "response": [] 124 | }, 125 | { 126 | "name": "关闭一个socket客户端", 127 | "request": { 128 | "method": "POST", 129 | "header": [ 130 | { 131 | "key": "Content-Type", 132 | "name": "Content-Type", 133 | "type": "text", 134 | "value": "application/json" 135 | } 136 | ], 137 | "body": { 138 | "mode": "raw", 139 | "raw": "{\n\t\"userId\":\"dingxu1\"\n}" 140 | }, 141 | "url": { 142 | "raw": "{{ip}}/socket-client/close", 143 | "host": [ 144 | "{{ip}}" 145 | ], 146 | "path": [ 147 | "socket-client", 148 | "close" 149 | ] 150 | } 151 | }, 152 | "response": [] 153 | }, 154 | { 155 | "name": "查看已开启的客户端", 156 | "request": { 157 | "method": "GET", 158 | "header": [], 159 | "body": { 160 | "mode": "raw", 161 | "raw": "" 162 | }, 163 | "url": { 164 | "raw": "{{ip}}/socket-client/get-users", 165 | "host": [ 166 | "{{ip}}" 167 | ], 168 | "path": [ 169 | "socket-client", 170 | "get-users" 171 | ] 172 | } 173 | }, 174 | "response": [] 175 | } 176 | ] 177 | } 178 | ] 179 | } -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 -------------------------------------------------------------------------------- /src/main/resources/socket测试.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "45558e3a-e452-472f-8bfc-845080ed523c", 3 | "name": "socket测试", 4 | "values": [ 5 | { 6 | "key": "ip", 7 | "value": "localhost:8080", 8 | "description": "", 9 | "enabled": true 10 | } 11 | ], 12 | "_postman_variable_scope": "environment", 13 | "_postman_exported_at": "2019-01-31T14:49:20.497Z", 14 | "_postman_exported_using": "Postman/6.7.2" 15 | } --------------------------------------------------------------------------------