# easy-chat-server **Repository Path**: yuanyang888/easy-chat-server ## Basic Information - **Project Name**: easy-chat-server - **Description**: 手写一个基于netty和spring实现的聊天服务 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2020-08-24 - **Last Updated**: 2021-01-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # easy-chat-server #### 介绍 基于netty和spring手写一个聊天服务 ####软件架构说明 服务端和客户端采用websocket长连接的方式进行服务的请求和推送。 ###### 如果采用传统的netty接收请求,根据请求的某个字段判断到底应该走哪个具体业务类,得写各种if else, 显然这么做是不优雅的,可以采用策略模式,每一种类型的消息写一个处理类,但是策略模式的缺点就是类会很多 ,那么可以做到像spring mvc那种根据url去映射走具体的controller方式吗,于是自己封装了框架,把这些 if else 从代码种解耦了出来 ##### 1.自定义注解 ##### @accept @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Accept { } ##### @AcceptMapping @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AcceptMapping { String value() default ""; } ##### 2.核心代码 //基于spring框架 扫描带有自定义注解的类缓存起来 @Component public class AcceptPostProcess implements InitializingBean, ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public void afterPropertiesSet() { String[] beanNames = applicationContext.getBeanNamesForAnnotation(Accept.class); try { for (String beanName : beanNames) { String baseUrl = ""; Object bean = applicationContext.getBean(beanName); Class clazz = bean.getClass(); if (clazz.isAnnotationPresent(AcceptMapping.class)) { AcceptMapping acceptMapping = clazz.getAnnotation(AcceptMapping.class); baseUrl = acceptMapping.value(); } Method[] methods = clazz.getMethods(); for (Method method : methods) { if (!method.isAnnotationPresent(AcceptMapping.class)) { continue; } AcceptMapping acceptMapping = method.getAnnotation(AcceptMapping.class); String regex = ("/" + baseUrl + acceptMapping.value()).replaceAll("/+", "/"); Pattern pattern = Pattern.compile(regex); AcceptRespository.addBean(new AcceptBean(bean, method, pattern)); System.out.println("mapping " + regex + "," + method); } } } catch (Exception e) { e.printStackTrace(); } } } ##### //通过反射技术,根据入参的messageUrl字段找到对应的accept并且执行方法 @Component public class AcceptInvoke { public Response invoke(JSONObject param, ChannelHandlerContext ctx) throws Exception { String messageType = param.getString("messageUrl"); if (StringUtils.isEmpty(messageType)) { throw new RuntimeException("messageUrl字段不能为空"); } AcceptBean acceptBean = getAcceptBean(messageType); Method method = acceptBean.getMethod(); if (acceptBean == null) { throw new RuntimeException("message路径不正确"); } Class[] paramTypes = method.getParameterTypes(); Object[] paramValues = new Object[paramTypes.length]; Map paramMap = acceptBean.getParamIndexMapping(); //容器启动时已经做过判断了,这里只有两种入参 ChatMessage和 ChannelHandlerContext if (paramMap.containsKey(ChannelHandlerContext.class.getName())) { int index = paramMap.get(ChannelHandlerContext.class.getName()); paramValues[index] = ctx; } Class c = getClass(acceptBean.getMethod()); if (paramMap.containsKey(c.getName())) { int index = paramMap.get(c.getName()); paramValues[index] = JSON.toJavaObject(param, c); } //反射执行方法 Object result = method.invoke(acceptBean.getAccept(),paramValues); if (result != null) { if (!(result instanceof Response)) { throw new RuntimeException("返回类必须为Response"); } } return (Response) result; } private AcceptBean getAcceptBean(String messageUrl) { List acceptBeans = AcceptRespository.getAcceptBeans(); if (CollectionUtils.isEmpty(acceptBeans)) { return null; } messageUrl = messageUrl.replaceAll("/+", "/"); for (AcceptBean acceptBean : acceptBeans) { try { Matcher matcher = acceptBean.getPattern().matcher(messageUrl); if (!matcher.matches()) { continue; } return acceptBean; } catch (Exception e) { throw e; } } return null; } private Class getClass(Method method) { Class[] paramTypes = method.getParameterTypes(); for (Class c : paramTypes) { if (ChatMessage.class.isAssignableFrom(c)) { return c; } } throw new RuntimeException("accept方法参数不正确"); } } ##### 3.accept层展示(类似于controller) @AcceptMapping(value = "/chat") @Accept @Component public class ChatAccept { @Autowired private ChatService singleChatService; /** * 单聊 * @param msg */ @AcceptMapping(value = "/single") public void singleChat(SingleMsg msg){ singleChatService.singleChat(msg); } /** * 群聊 * @param msg */ @AcceptMapping(value = "/group") public void groupChat(GroupChatMsg msg){ singleChatService.groupChat(msg); } } #### 整个过程和spring mvc类似,但是为了避免长连接的资源利用,实际工作中不建议所有的通信都走socket ,还是建议走http协议