├── .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 | }
--------------------------------------------------------------------------------