├── front └── im_ui │ ├── vue.config.js │ ├── babel.config.js │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── assets │ │ ├── logo.png │ │ └── pk.svg │ ├── store │ │ └── index.js │ ├── App.vue │ ├── ws │ │ ├── OffLineNotify.js │ │ ├── login.js │ │ ├── addFriend.js │ │ ├── getUserList.js │ │ ├── ws.js │ │ └── sendMsg.js │ ├── views │ │ ├── Main.vue │ │ ├── Room.vue │ │ ├── Friends.vue │ │ ├── Chat.vue │ │ └── Login.vue │ ├── main.js │ ├── router │ │ └── index.js │ └── components │ │ └── Message.vue │ ├── record │ └── 前端开发记录.md │ ├── .gitignore │ ├── README.md │ └── package.json ├── doc ├── ttt.gif ├── yyy.png ├── NIO组件.md ├── devNote.md └── netty的组件.md ├── src ├── main │ ├── java │ │ └── com │ │ │ └── wks │ │ │ ├── customProtocol │ │ │ ├── readMe.md │ │ │ ├── packet │ │ │ │ ├── data │ │ │ │ │ ├── EncodeData.java │ │ │ │ │ ├── MessageRequestData.java │ │ │ │ │ ├── LoginResponseData.java │ │ │ │ │ ├── LoginRequestData.java │ │ │ │ │ └── MessageResponseData.java │ │ │ │ ├── PacketAnalysis.java │ │ │ │ ├── Command.java │ │ │ │ └── Packet.java │ │ │ ├── server │ │ │ │ ├── Session.java │ │ │ │ ├── AuthHandler.java │ │ │ │ ├── MessageHandler.java │ │ │ │ ├── LoginHandler.java │ │ │ │ ├── LifeCyCleTestHandler.java │ │ │ │ ├── ServerHandler.java │ │ │ │ └── server.java │ │ │ ├── serializer │ │ │ │ ├── Serializer.java │ │ │ │ ├── SerializerAlgorithm.java │ │ │ │ └── JSONSerializer.java │ │ │ ├── client │ │ │ │ ├── MessageHandler.java │ │ │ │ ├── ClientHandler.java │ │ │ │ ├── LoginHandler.java │ │ │ │ └── client.java │ │ │ ├── codec │ │ │ │ ├── PacketEncoder.java │ │ │ │ ├── PacketDecoder.java │ │ │ │ └── Spliter.java │ │ │ └── Utils.java │ │ │ └── wsIm │ │ │ ├── domain │ │ │ ├── req │ │ │ │ ├── Login.java │ │ │ │ ├── AddFriendReq.java │ │ │ │ └── SendMsg.java │ │ │ ├── resp │ │ │ │ ├── SendResp.java │ │ │ │ ├── LoginResp.java │ │ │ │ ├── ErrorResp.java │ │ │ │ ├── CommonResp.java │ │ │ │ ├── OffLineNotify.java │ │ │ │ ├── AddFriendNotify.java │ │ │ │ ├── ReceiveNotify.java │ │ │ │ ├── AddFriendResp.java │ │ │ │ └── UserResp.java │ │ │ ├── Commands.java │ │ │ └── Packet.java │ │ │ ├── biz │ │ │ ├── BaseService.java │ │ │ ├── MsgContext.java │ │ │ ├── Command.java │ │ │ ├── Session.java │ │ │ ├── GetUserListService.java │ │ │ ├── UserInfo.java │ │ │ ├── WriteService.java │ │ │ ├── AddFriendsService.java │ │ │ ├── UserService.java │ │ │ ├── OffLineService.java │ │ │ ├── LoginService.java │ │ │ ├── Router.java │ │ │ └── SendMessageService.java │ │ │ ├── serializer │ │ │ ├── Serializer.java │ │ │ └── JSONSerializer.java │ │ │ ├── server │ │ │ ├── IMIdleStateHandler.java │ │ │ ├── WebSocketServerInitializer.java │ │ │ ├── WebSocketServer.java │ │ │ ├── WebSocketServerIndexPage.java │ │ │ ├── WebSocketFrameHandler.java │ │ │ ├── WebSocketIndexPageHandler.java │ │ │ └── HttpStaticFileServerHandler.java │ │ │ └── Util │ │ │ ├── GodChannel.java │ │ │ ├── StrUtil.java │ │ │ └── ClassUtil.java │ └── resources │ │ ├── logback.xml │ │ └── META-INF │ │ └── mime.types └── test │ └── java │ ├── com │ └── wks │ │ ├── netty_test │ │ ├── FirstServerHandler.java │ │ ├── ByteBufTest.java │ │ ├── NettyClient.java │ │ ├── FirstClientHandler.java │ │ └── NettyServer.java │ │ ├── newIO │ │ ├── Client.java │ │ └── MyNioServer.java │ │ └── protocolTest │ │ └── TestProtocol.java │ └── Test │ ├── TestVolatile.java │ ├── Test5.java │ ├── TestVol3.java │ ├── Test4.java │ ├── Test.java │ └── TestVo2.java ├── .gitignore ├── Netty_IM.iml ├── README.md └── pom.xml /front/im_ui/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} -------------------------------------------------------------------------------- /doc/ttt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangkeshan9538/Netty_IM/HEAD/doc/ttt.gif -------------------------------------------------------------------------------- /doc/yyy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangkeshan9538/Netty_IM/HEAD/doc/yyy.png -------------------------------------------------------------------------------- /front/im_ui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /front/im_ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangkeshan9538/Netty_IM/HEAD/front/im_ui/public/favicon.ico -------------------------------------------------------------------------------- /front/im_ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangkeshan9538/Netty_IM/HEAD/front/im_ui/src/assets/logo.png -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/readMe.md: -------------------------------------------------------------------------------- 1 | 按照掘金的小册做的netty demo, 2 | 自定义了协议 ,协议规范以及数据domain 在 packet包下, 3 | 报文长度控制以及编码解码器在codec下, 4 | 序列化方法在serializer下, 5 | 业务处理handler在server下。 -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/req/Login.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.req; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Login { 7 | private String userName; 8 | private String passwd; 9 | } 10 | -------------------------------------------------------------------------------- /front/im_ui/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | }, 9 | mutations: { 10 | }, 11 | actions: { 12 | }, 13 | modules: { 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/req/AddFriendReq.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.req; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class AddFriendReq { 7 | //操作人 8 | private String userId; 9 | //添加的userId 10 | private String addId; 11 | } 12 | -------------------------------------------------------------------------------- /front/im_ui/record/前端开发记录.md: -------------------------------------------------------------------------------- 1 | 中间的content 出现了两个问题: 2 | 1. 上面的 nav-bar 和下面的 input-field 都设置成了fixed ,就脱离了文档流,中间的content 内容就会和上下重合 3 | 2. 因为父div 层层height 100% 来占满屏幕, 但发消息导致content 长度增加结果超过了屏幕高度, 4 | 所以最后决定用 absolute + top + bottum 来写死content 的位置,看了上下nav-bar 和input-field ,height都设置的是指定高度,没有百分比,所以也挺合理 -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/data/EncodeData.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet.data; 2 | 3 | /** 4 | * 如果要一个统一的encode层的话 ,就需要一个统一的Data 基类 , 5 | * 也再次 提供一个方法 获得Data 对应的Command 6 | */ 7 | public interface EncodeData { 8 | 9 | byte getCommand(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/BaseService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | /** 4 | * 两个参数不能再有泛型,不然在路由层不好锁定接收返回类型 5 | * @param req 6 | * @param resp 7 | */ 8 | public abstract class BaseService { 9 | 10 | abstract I process(MsgContext context,T t); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/req/SendMsg.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.req; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | @Data 8 | public class SendMsg { 9 | String snedFromID; 10 | String sendToId; 11 | Date time; 12 | String msg; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/SendResp.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | public class SendResp extends CommonResp{ 6 | 7 | 8 | public SendResp(String success, String reason) { 9 | super(success, reason); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/LoginResp.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | 4 | import lombok.Data; 5 | 6 | @Data 7 | public class LoginResp { 8 | private String userId; 9 | 10 | public LoginResp(String userId) { 11 | this.userId = userId; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/ErrorResp.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ErrorResp { 7 | private String errorReason; 8 | 9 | public ErrorResp(String errorReason) { 10 | this.errorReason = errorReason; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | target -------------------------------------------------------------------------------- /front/im_ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /front/im_ui/README.md: -------------------------------------------------------------------------------- 1 | # im_ui 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /front/im_ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/MsgContext.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import io.netty.channel.Channel; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | 9 | /** 10 | * 作为路由和 nettyHandler之间调用的一个上下文 11 | */ 12 | @Getter 13 | @Setter 14 | public class MsgContext { 15 | private String traceId; 16 | private Channel channel; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/CommonResp.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | 4 | import lombok.Data; 5 | 6 | @Data 7 | public class CommonResp { 8 | private String success; 9 | private String reason; 10 | 11 | public CommonResp(String success, String reason) { 12 | this.success = success; 13 | this.reason = reason; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/Session.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Session { 7 | 8 | private String userId; 9 | private String userName; 10 | 11 | public Session(String userId, String userName) { 12 | this.userId = userId; 13 | this.userName = userName; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/Command.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.TYPE) 10 | public @interface Command { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/OffLineNotify.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OffLineNotify { 7 | private String userId; 8 | private String userName; 9 | 10 | public OffLineNotify(String userId, String userName) { 11 | this.userId = userId; 12 | this.userName = userName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/AddFriendNotify.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class AddFriendNotify { 7 | private String userId; 8 | private String userName; 9 | 10 | public AddFriendNotify(String userId, String userName) { 11 | this.userId = userId; 12 | this.userName = userName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/Session.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.domain.resp.UserResp; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | 9 | /** 10 | * 标志 channel 里的用户信息 11 | */ 12 | @Setter 13 | @Getter 14 | public class Session { 15 | 16 | 17 | UserInfo user; 18 | 19 | public Session(UserInfo user) { 20 | this.user = user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/serializer/Serializer.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.serializer; 2 | 3 | import com.wks.wsIm.domain.Packet; 4 | 5 | /** 6 | * 序列化和反序列化的抽象类 7 | * 8 | * @param 用于内部处理的,实体对象 9 | * @param 序列化后的对象 10 | */ 11 | public interface Serializer { 12 | I serialize(T t); 13 | 14 | T deserialize(I i); 15 | 16 | Object desData(T packet, Class reqtype); 17 | 18 | I serData(Object data); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/ReceiveNotify.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ReceiveNotify { 7 | 8 | private String fromId; 9 | private String fromName; 10 | private String msg; 11 | 12 | public ReceiveNotify(String fromId, String fromName, String msg) { 13 | this.fromId = fromId; 14 | this.fromName = fromName; 15 | this.msg = msg; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/serializer/Serializer.java: -------------------------------------------------------------------------------- 1 | 2 | package com.wks.customProtocol.serializer; 3 | 4 | public interface Serializer { 5 | 6 | /** 7 | * 序列化算法 8 | */ 9 | byte getSerializerAlgorithm(); 10 | 11 | /** 12 | * java 对象转换成二进制 13 | */ 14 | byte[] serialize(Object object); 15 | 16 | /** 17 | * 二进制转换成 java 对象 18 | */ 19 | T deserialize(Class clazz, byte[] bytes); 20 | 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/AddFriendResp.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class AddFriendResp extends CommonResp { 7 | 8 | //添加的好友的信息 9 | private String userId; 10 | private String userName; 11 | 12 | public AddFriendResp(String success, String reason, String userId, String userName) { 13 | super(success, reason); 14 | this.userId = userId; 15 | this.userName = userName; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/PacketAnalysis.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | 5 | /** 6 | * 规定出 packet 解析 这个行为 7 | * 泛型 表示使用的 packet 类型 8 | */ 9 | public interface PacketAnalysis { 10 | ByteBuf encode(ByteBuf byteBuf); 11 | 12 | /** 13 | * 当出现更多类型协议的时候 ,这里就 14 | * 15 | * @param buf 16 | * @return 17 | */ 18 | T decode(ByteBuf buf); 19 | 20 | byte getCommandOperation(); 21 | 22 | byte getSerializerAlgorithm(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /front/im_ui/src/ws/OffLineNotify.js: -------------------------------------------------------------------------------- 1 | import { commands, registerHandle } from '@/ws/ws.js'; 2 | import { friendsList } from "@/ws/addFriend.js"; 3 | import { Notify } from 'vant'; 4 | 5 | function offLineHandle(obj) { 6 | var data = obj.data 7 | var user=friendsList.list.find((v)=>{return v.userId===data.userId}) 8 | user.status='0' 9 | Notify({ 10 | type: 'success', 11 | message: '好友下线:'+data.userName, 12 | duration: 1000 13 | }); 14 | } 15 | 16 | 17 | registerHandle(commands.get('OFFLINE_NOTIFY'), offLineHandle) -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/serializer/SerializerAlgorithm.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.serializer; 2 | 3 | 4 | public interface SerializerAlgorithm { 5 | /** 6 | * json 序列化标识 7 | */ 8 | byte JSON = 1; 9 | 10 | 11 | Serializer DEFAULT = new JSONSerializer(); 12 | 13 | //根据给的选项获得序列化工具实例 14 | static Serializer getSerializer(byte a) { 15 | switch (a) { 16 | case SerializerAlgorithm.JSON: 17 | return SerializerAlgorithm.DEFAULT; 18 | } 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/data/MessageRequestData.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet.data; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class MessageRequestData implements EncodeData{ 8 | 9 | private String toUser; 10 | 11 | private String message; 12 | 13 | public MessageRequestData(String toUser, String message) { 14 | this.toUser = toUser; 15 | this.message = message; 16 | } 17 | 18 | @Override 19 | public byte getCommand() { 20 | return Command.MESSAGE_REQUEST; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/serializer/JSONSerializer.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.serializer; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | 5 | public class JSONSerializer implements Serializer{ 6 | @Override 7 | public byte getSerializerAlgorithm() { 8 | return SerializerAlgorithm.JSON; 9 | } 10 | 11 | @Override 12 | public byte[] serialize(Object object) { 13 | return JSON.toJSONBytes(object); 14 | } 15 | 16 | @Override 17 | public T deserialize(Class clazz, byte[] bytes) { 18 | return JSON.parseObject(bytes, clazz); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/Commands.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain; 2 | 3 | /** 4 | * 定义所有的命令类型 5 | */ 6 | public class Commands { 7 | 8 | public final static String LOGIN = "0"; 9 | 10 | public final static String GET_USER_LIST = "1"; 11 | 12 | public final static String SEND_MSSAGE = "2"; 13 | 14 | public final static String ADD_FRIENDS = "3"; 15 | 16 | public final static String ADD_NOTIRY = "4"; 17 | 18 | public final static String MESSAGE_NOTIFY = "5"; 19 | 20 | public final static String OFFLINE_NOTIFY = "6"; 21 | 22 | public final static String ERROR="7"; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/data/LoginResponseData.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet.data; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class LoginResponseData implements EncodeData{ 8 | 9 | private Boolean isSuccessful; 10 | 11 | private Integer code; 12 | 13 | public LoginResponseData(Boolean isSuccessful, Integer code) { 14 | this.isSuccessful = isSuccessful; 15 | this.code = code; 16 | } 17 | 18 | 19 | @Override 20 | public byte getCommand() { 21 | return Command.LOGIN_RESPONSE; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/resp/UserResp.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserResp { 7 | 8 | String userId; 9 | String userName; 10 | 11 | public UserResp(String userId, String userName) { 12 | this.userId = userId; 13 | this.userName = userName; 14 | } 15 | 16 | public UserResp(String userId) { 17 | this.userId = userId; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object obj) { 22 | return (obj instanceof UserResp) && ((UserResp) obj).userId.equals(this.userId); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /front/im_ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | im_ui 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /front/im_ui/src/ws/login.js: -------------------------------------------------------------------------------- 1 | import {send,commands,packet,MsgHandle} from '@/ws/ws.js'; 2 | 3 | class req { 4 | constructor(userName,passwd) { this.userName = userName, this.passwd =passwd } 5 | } 6 | 7 | class resp { constructor(userId) { this.userId = userId } } 8 | 9 | export {req,resp} 10 | 11 | 12 | function loginFuc(userName,passwd,func){ 13 | var traceId=new Date().getTime(); 14 | //组req 15 | var r=new req(userName,passwd) 16 | //组packet 17 | var p=new packet(commands.get('login'),traceId,r) 18 | 19 | //组handle 20 | var msg=new MsgHandle(p,func); 21 | send(msg) 22 | } 23 | 24 | 25 | export{loginFuc} -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/domain/Packet.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.domain; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 描述一个完整的指令及数据 7 | */ 8 | @Data 9 | public class Packet { 10 | 11 | private String command; 12 | 13 | /** 14 | * 不为空则说明是应答模式 15 | */ 16 | private String traceId; 17 | 18 | private Object data; 19 | 20 | public Packet(String command, String traceId, Object data) { 21 | this.command = command; 22 | this.traceId = traceId; 23 | this.data = data; 24 | } 25 | 26 | public Packet(String traceId, Object data) { 27 | this.traceId = traceId; 28 | this.data = data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /front/im_ui/src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 25 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/data/LoginRequestData.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet.data; 2 | 3 | 4 | import com.wks.customProtocol.packet.Command; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class LoginRequestData implements EncodeData{ 9 | 10 | private String userId; 11 | 12 | private String username; 13 | 14 | private String password; 15 | 16 | public LoginRequestData(String userId, String username, String password) { 17 | this.userId = userId; 18 | this.username = username; 19 | this.password = password; 20 | } 21 | 22 | @Override 23 | public byte getCommand() { 24 | return Command.LOGIN_REQUEST; 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/client/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.client; 2 | 3 | import com.wks.customProtocol.packet.data.MessageResponseData; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | 7 | public class MessageHandler extends SimpleChannelInboundHandler { 8 | @Override 9 | protected void channelRead0(ChannelHandlerContext ctx, MessageResponseData msg) throws Exception { 10 | String fromUserId = msg.getFromUserId(); 11 | String fromUserName = msg.getFromUserName(); 12 | System.out.println(fromUserId + ":" + fromUserName + " -> " + msg.getMessageResponse()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /front/im_ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "im_ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.4.4", 11 | "element-ui": "^2.13.0", 12 | "mint-ui": "^2.2.13", 13 | "vant": "^2.4.1", 14 | "vue": "^2.6.10", 15 | "vue-router": "^3.1.3", 16 | "vuex": "^3.1.2" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^4.1.0", 20 | "@vue/cli-plugin-router": "^4.1.0", 21 | "@vue/cli-plugin-vuex": "^4.1.0", 22 | "@vue/cli-service": "^4.1.0", 23 | "vue-template-compiler": "^2.6.10" 24 | }, 25 | "browserslist": [ 26 | "> 1%", 27 | "last 2 versions" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/data/MessageResponseData.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet.data; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class MessageResponseData implements EncodeData { 8 | 9 | private String fromUserId; 10 | 11 | private String fromUserName; 12 | 13 | 14 | private String MessageResponse; 15 | 16 | public MessageResponseData(String fromUserId, String fromUserName, String messageResponse) { 17 | this.fromUserId = fromUserId; 18 | this.fromUserName = fromUserName; 19 | MessageResponse = messageResponse; 20 | } 21 | 22 | @Override 23 | public byte getCommand() { 24 | return Command.MESSGAE_RESPONSE; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /doc/NIO组件.md: -------------------------------------------------------------------------------- 1 | ## NIO 的主要组件和使用 2 | * Selector 3 | 4 | 选择器不多说 5 | 6 | * ServerSocketChannel 7 | 8 | Channels read data into Buffers, and Buffers write data into Channels 9 | channel 表示的是 与一个连接。 10 | ServerSocketChannel 是服务器 对socket的监控通道,相似于serverSocket 11 | * SelectionKey 12 | 13 | 表示 SelectableChannel 在 Selector 中的注册的标记。 14 | 通过select 监控key 来判断channel的而状态 15 | 16 | * SocketChannel 17 | 18 | 表示一个Client连接 19 | 20 | * Buffer 21 | 22 | 承接 从Channel 读取的数据,或将此数据传输到Channel 23 | 24 | ## 出现的问题 25 | 1. 用浏览器 对NIOServer 进行请求 ,发现 Server的key accept了多次,猜测是因为 http的TCP 连接会有不同, 26 | 如果 直接写socket 进行连接的话,那么 serverKey 只会accept 一次。 27 | 28 | 2. NIO 对应的通信模型是IO多路复用,但是多路复用在 拷贝数据阶段,是会阻塞线程的,那这个阻塞时间,会不会导致延时太久? 29 | 因为是直接从内核态到用户态的内存传输,所以这并不会耗费太多时间,而且系统还有直接映射,不通过内核态的系统调用可以解决这个问题, -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/codec/PacketEncoder.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.codec; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import com.wks.customProtocol.packet.Packet; 5 | import com.wks.customProtocol.packet.data.EncodeData; 6 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.handler.codec.MessageToByteEncoder; 10 | 11 | public class PacketEncoder extends MessageToByteEncoder { 12 | 13 | @Override 14 | protected void encode(ChannelHandlerContext ctx, EncodeData msg, ByteBuf out) throws Exception { 15 | Packet p = new Packet(msg.getCommand(), SerializerAlgorithm.DEFAULT.serialize(msg)); 16 | p.encode(out); 17 | //看起来好像不需要再writeFlush 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/GetUserListService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | 4 | import com.wks.wsIm.domain.resp.UserResp; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static com.wks.wsIm.domain.Commands.GET_USER_LIST; 11 | 12 | @Command(GET_USER_LIST) 13 | public class GetUserListService extends BaseService { 14 | @Override 15 | List process(MsgContext context, Void aVoid) { 16 | return getOnlineUsers(); 17 | } 18 | 19 | public static List getOnlineUsers() { 20 | List users = new ArrayList<>(); 21 | Map user = UserService.getUserPool(); 22 | user.forEach((key, value) -> { 23 | UserResp u = new UserResp(value.getUserId(), value.getUserName()); 24 | users.add(u); 25 | }); 26 | return users; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.google.common.collect.Sets; 4 | import io.netty.channel.Channel; 5 | import lombok.Data; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | import java.util.Set; 10 | 11 | @Setter 12 | @Getter 13 | public class UserInfo { 14 | 15 | //岂不是所有的牵扯并发访问的字段,都得加volatile 16 | private volatile String userId; 17 | private volatile String userName; 18 | private volatile Channel channel; 19 | private final Set friends= Sets.newConcurrentHashSet(); 20 | private volatile String status; 21 | 22 | public UserInfo(String userId, String userName, Channel channel,String status) { 23 | this.userId = userId; 24 | this.userName = userName; 25 | this.channel = channel; 26 | this.status=status; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object obj) { 31 | return (obj instanceof UserInfo) && ((UserInfo) obj).userId.equals(this.userId); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/codec/PacketDecoder.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.codec; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import com.wks.customProtocol.packet.Packet; 5 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 6 | import io.netty.buffer.ByteBuf; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.handler.codec.ByteToMessageDecoder; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 将解码操作 单独做一层, 14 | */ 15 | public class PacketDecoder extends ByteToMessageDecoder { 16 | 17 | @Override 18 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { 19 | //解码成Packet对象 20 | Packet p2 = new Packet(); 21 | p2.decode(in); 22 | 23 | //根据 序列化方式 和command 获得 24 | Object data = SerializerAlgorithm.getSerializer(p2.getSerializerAlgorithm()) 25 | .deserialize(Command.getRequestDataType(p2.getCommand()), p2.data); 26 | 27 | out.add(data); 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/wks/netty_test/FirstServerHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.netty_test; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | 7 | import java.nio.charset.Charset; 8 | import java.util.Date; 9 | 10 | public class FirstServerHandler extends ChannelInboundHandlerAdapter { 11 | 12 | @Override 13 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 14 | ByteBuf byteBuf = (ByteBuf) msg; 15 | 16 | System.out.println(new Date() + ": 服务端读到数据 -> " + byteBuf.toString(Charset.forName("utf-8"))); 17 | 18 | ByteBuf out = getByteBuf(ctx); 19 | ctx.channel().writeAndFlush(out); 20 | } 21 | 22 | private ByteBuf getByteBuf(ChannelHandlerContext ctx) { 23 | byte[] bytes = "你好,欢迎关注我的微信公众号,《闪电侠的博客》!".getBytes(Charset.forName("utf-8")); 24 | 25 | ByteBuf buffer = ctx.alloc().buffer(); 26 | 27 | buffer.writeBytes(bytes); 28 | 29 | return buffer; 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/AuthHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import com.wks.customProtocol.Utils; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | 7 | 8 | /** 9 | * 登录验证handler 10 | */ 11 | public class AuthHandler extends ChannelInboundHandlerAdapter { 12 | 13 | @Override 14 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 15 | if (Utils.getSession(ctx.channel()) == null) { 16 | ctx.channel().close(); 17 | } else { 18 | // 一行代码实现逻辑的删除 19 | ctx.pipeline().remove(this); 20 | super.channelRead(ctx, msg); 21 | } 22 | } 23 | 24 | @Override 25 | public void handlerRemoved(ChannelHandlerContext ctx) { 26 | if (Utils.getSession(ctx.channel()) != null) { 27 | System.out.println("当前连接登录验证完毕,无需再次验证, AuthHandler 被移除"); 28 | } else { 29 | System.out.println("无登录验证,强制关闭连接!"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/serializer/JSONSerializer.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.serializer; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.wks.wsIm.domain.Commands; 6 | import com.wks.wsIm.domain.Packet; 7 | 8 | import java.util.Enumeration; 9 | 10 | 11 | public class JSONSerializer implements Serializer{ 12 | 13 | 14 | @Override 15 | public String serialize(Packet packet) { 16 | return JSONObject.toJSONString(packet,false); 17 | } 18 | 19 | @Override 20 | public Packet deserialize(String s) { 21 | return JSONObject.parseObject(s,Packet.class); 22 | } 23 | 24 | @Override 25 | public Object desData(Packet packet, Class reqtype) { 26 | if(reqtype.equals( Void.TYPE)){ 27 | return null; 28 | } 29 | 30 | JSONObject o=(JSONObject)packet.getData(); 31 | return o.toJavaObject(reqtype); 32 | } 33 | 34 | @Override 35 | public String serData(Object data) { 36 | return null; 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /front/im_ui/src/views/Room.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/Command.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet; 2 | 3 | import com.wks.customProtocol.packet.data.LoginRequestData; 4 | import com.wks.customProtocol.packet.data.LoginResponseData; 5 | import com.wks.customProtocol.packet.data.MessageRequestData; 6 | import com.wks.customProtocol.packet.data.MessageResponseData; 7 | 8 | public interface Command { 9 | 10 | byte LOGIN_REQUEST = 1; 11 | 12 | byte LOGIN_RESPONSE = 2; 13 | 14 | byte MESSAGE_REQUEST = 3; 15 | 16 | byte MESSGAE_RESPONSE = 4; 17 | 18 | // 在handler的处理方法中,通过判断指令 来 反序列化对应的class ,并进不同的handler ,那这个方法似乎并没有必要 19 | // 2019-2-11 当需要根据class 分发对应的handler时则需要这个方法 20 | static Class getRequestDataType(byte type) { 21 | switch (type) { 22 | case 1: 23 | return LoginRequestData.class; 24 | case 2: 25 | return LoginResponseData.class; 26 | case 3: 27 | return MessageRequestData.class; 28 | case 4: 29 | return MessageResponseData.class; 30 | } 31 | return null; 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/java/com/wks/newIO/Client.java: -------------------------------------------------------------------------------- 1 | package com.wks.newIO; 2 | 3 | 4 | import java.io.*; 5 | import java.net.InetSocketAddress; 6 | import java.net.Socket; 7 | 8 | 9 | public class Client { 10 | public static void main(String[] args) throws IOException { 11 | Socket client = null; 12 | PrintWriter printWriter = null; 13 | BufferedReader bufferedReader = null; 14 | try { 15 | client = new Socket(); 16 | client.connect(new InetSocketAddress("localhost",8080)); 17 | printWriter = new PrintWriter(client.getOutputStream(),true); 18 | printWriter.println("hello"); 19 | printWriter.println("hello2"); 20 | printWriter.println("hello3"); 21 | 22 | printWriter.flush(); 23 | 24 | int i=1; 25 | while( i>0){ 26 | i=client.getInputStream().read(); 27 | System.out.print(new Character((char)i)); 28 | } 29 | 30 | client.close(); 31 | 32 | } catch (IOException e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/IMIdleStateHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.server; 2 | 3 | import com.wks.wsIm.biz.Session; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.timeout.IdleStateEvent; 7 | import io.netty.handler.timeout.IdleStateHandler; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static com.wks.wsIm.biz.LoginService.SESSION; 13 | 14 | @Slf4j 15 | public class IMIdleStateHandler extends IdleStateHandler { 16 | 17 | private static final int READER_IDLE_TIME = 30; 18 | 19 | public IMIdleStateHandler() { 20 | super(READER_IDLE_TIME, 0, 0, TimeUnit.MINUTES); 21 | } 22 | 23 | @Override 24 | protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) { 25 | Channel c=ctx.channel(); 26 | Session s=c.attr(SESSION).get(); 27 | if(s!=null &&s.getUser()!=null){ 28 | log.info("关闭空闲连接"+ s.getUser().getUserName()); 29 | }else{ 30 | log.info("关闭空闲连接"); 31 | } 32 | ctx.channel().close(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/Test/TestVolatile.java: -------------------------------------------------------------------------------- 1 | package Test; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public class TestVolatile { 6 | public static volatile long[] arr = new long[20]; 7 | public static void main(String[] args) throws Exception { 8 | //线程1 9 | new Thread(new Thread(){ 10 | @Override 11 | public void run() { 12 | //Thread A 13 | try { 14 | TimeUnit.MILLISECONDS.sleep(1000); 15 | System.out.println("end 1"); 16 | } catch (InterruptedException e) { 17 | e.printStackTrace(); 18 | } 19 | arr[19] = 2; 20 | } 21 | }).start(); 22 | //线程2 23 | new Thread(new Thread(){ 24 | @Override 25 | public void run() { 26 | //Thread B 27 | while (arr[19] != 2) { 28 | System.out.println("test"); 29 | } 30 | System.out.println("Jump out of the loop!"); 31 | } 32 | }).start(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/codec/Spliter.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.codec; 2 | 3 | import com.wks.customProtocol.packet.Packet; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.codec.LengthFieldBasedFrameDecoder; 7 | 8 | /** 9 | * 兼具 模数检查 和 拆包处理的功能 10 | */ 11 | public class Spliter extends LengthFieldBasedFrameDecoder { 12 | private static final int LENGTH_FIELD_OFFSET = 7; 13 | private static final int LENGTH_FIELD_LENGTH = 4; 14 | 15 | public Spliter() { 16 | super(Integer.MAX_VALUE, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH); 17 | } 18 | 19 | @Override 20 | protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { 21 | // 屏蔽非本协议的客户端 22 | if (in.getInt(in.readerIndex()) != Packet.MAGIC_NUM) { 23 | System.out.println("关闭连接"); 24 | 25 | ctx.channel().close(); 26 | 27 | return null; 28 | } 29 | 30 | return super.decode(ctx, in); 31 | } 32 | 33 | 34 | @Override 35 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 36 | //do nothing 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /front/im_ui/src/views/Friends.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /front/im_ui/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import Mint from 'mint-ui'; 6 | import Vant from 'vant'; 7 | import 'vant/lib/index.css'; 8 | 9 | //让这些js初始化 10 | import './ws/ws.js' 11 | import './ws/getUserList.js' 12 | import './ws/addFriend.js' 13 | import './ws/sendMsg.js' 14 | import './ws/OffLineNotify.js' 15 | 16 | 17 | Vue.use(Vant); 18 | Vue.use(Mint); 19 | Vue.config.productionTip = false 20 | 21 | new Vue({ 22 | router, 23 | store, 24 | render: h => h(App) 25 | }).$mount('#app') 26 | 27 | 28 | 29 | Date.prototype.Format = function (fmt) { //author: meizz 30 | var o = { 31 | "M+": this.getMonth() + 1, //月份 32 | "d+": this.getDate(), //日 33 | "h+": this.getHours(), //小时 34 | "m+": this.getMinutes(), //分 35 | "s+": this.getSeconds(), //秒 36 | "q+": Math.floor((this.getMonth() + 3) / 3), //季度 37 | "S": this.getMilliseconds() //毫秒 38 | }; 39 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); 40 | for (var k in o) 41 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 42 | return fmt; 43 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import com.wks.customProtocol.Utils; 4 | import com.wks.customProtocol.packet.Command; 5 | import com.wks.customProtocol.packet.Packet; 6 | import com.wks.customProtocol.packet.data.MessageRequestData; 7 | import com.wks.customProtocol.packet.data.MessageResponseData; 8 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.channel.SimpleChannelInboundHandler; 12 | 13 | public class MessageHandler extends SimpleChannelInboundHandler { 14 | @Override 15 | protected void channelRead0(ChannelHandlerContext ctx, MessageRequestData msg) throws Exception { 16 | 17 | //拿到发送方的session 18 | Session session = Utils.getSession(ctx.channel()); 19 | 20 | if (session == null) { 21 | return; 22 | } 23 | 24 | //构建 转发消息 25 | MessageResponseData messageResponseData = new MessageResponseData(session.getUserId(), session.getUserName(), msg.getMessage()); 26 | //转发 27 | Channel beCalled = Utils.getChannel(msg.getToUser()); 28 | beCalled.writeAndFlush(messageResponseData); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /front/im_ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Login from '../views/Login.vue' 4 | import {pySegSort,userList,refreshList} from '@/ws/getUserList.js' 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Login', 11 | component: Login 12 | }, 13 | { 14 | path: '/main', 15 | name: 'main', 16 | // route level code-splitting 17 | // this generates a separate chunk (about.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: () => import(/* webpackChunkName: "about" */ '../views/Main.vue'), 20 | children: [ 21 | 22 | { 23 | path: 'Friends', 24 | component: () => import(/* webpackChunkName: "about" */ '../views/Friends.vue'), 25 | 26 | }, 27 | { 28 | path: 'Room', 29 | component: () => import(/* webpackChunkName: "about" */ '../views/Room.vue'), 30 | } 31 | ], 32 | redirect: '/main/Friends' 33 | }, { 34 | path: '/Chat/:sendToId/:sendToName', 35 | name: 'Chat', 36 | component: () => import(/* webpackChunkName: "about" */ '../views/Chat.vue'), 37 | props: true 38 | } 39 | 40 | ] 41 | 42 | const router = new VueRouter({ 43 | routes 44 | }) 45 | 46 | 47 | 48 | export default router 49 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/WriteService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.Util.GodChannel; 4 | import com.wks.wsIm.domain.Packet; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelFuture; 7 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.util.List; 11 | 12 | import static com.wks.wsIm.biz.UserService.GOD_ID; 13 | import static com.wks.wsIm.server.WebSocketFrameHandler.serializer; 14 | 15 | @Slf4j 16 | public class WriteService { 17 | public static ChannelFuture send( Channel channel, Packet sendMsg) { 18 | 19 | if (channel instanceof GodChannel){ 20 | return channel.writeAndFlush(serializer.serialize(sendMsg)); 21 | } 22 | 23 | if (!(channel != null && channel.isWritable() && channel.isActive())) { 24 | throw new RuntimeException("用户不存在"); 25 | } 26 | 27 | return channel.writeAndFlush(new TextWebSocketFrame(serializer.serialize(sendMsg))); 28 | } 29 | 30 | public static void spead(Packet p, List users) { 31 | users.forEach((u) -> { 32 | try { 33 | send(u.getChannel(), p); 34 | } catch (Exception e) { 35 | //ignore 36 | log.info("广播失败"); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/wks/protocolTest/TestProtocol.java: -------------------------------------------------------------------------------- 1 | package com.wks.protocolTest; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import com.wks.customProtocol.packet.Packet; 5 | import com.wks.customProtocol.packet.PacketAnalysis; 6 | import com.wks.customProtocol.packet.data.LoginRequestData; 7 | import com.wks.customProtocol.serializer.Serializer; 8 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 9 | import io.netty.buffer.ByteBuf; 10 | import io.netty.buffer.ByteBufAllocator; 11 | 12 | import static com.wks.customProtocol.packet.Packet.*; 13 | 14 | public class TestProtocol { 15 | 16 | public static void main(String[] args) { 17 | 18 | // packet 到buffer 19 | LoginRequestData data = new LoginRequestData("1", "wks", "wks"); 20 | Packet p = new Packet(Command.LOGIN_REQUEST, SerializerAlgorithm.DEFAULT.serialize(data)); 21 | ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer(); 22 | 23 | ByteBuf buf = p.encode(byteBuf); 24 | 25 | // buffer 到 packet 再到 指令和对象 26 | Packet p2 = new Packet(); 27 | p2.decode(buf); 28 | Class c = Command.getRequestDataType(p2.getCommandOperation()); 29 | Object o = SerializerAlgorithm.getSerializer(p2.getSerializerAlgorithm()).deserialize(c, p2.data); 30 | System.out.println(o); 31 | 32 | PacketAnalysis p3 = new Packet(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/Test/Test5.java: -------------------------------------------------------------------------------- 1 | package Test; 2 | 3 | 4 | import java.util.concurrent.TimeUnit; 5 | 6 | public class Test5 { 7 | public static volatile String flag="1"; 8 | public static volatile String vol="2"; 9 | public static String noVol="3"; 10 | 11 | 12 | public static void main(String[] args) throws Exception { 13 | //线程1 14 | new Thread(new Thread(){ 15 | @Override 16 | public void run() { 17 | //Thread A 18 | System.out.println(noVol); 19 | while(true){ 20 | String s=vol; 21 | if(noVol.equals("33")){ 22 | 23 | break; 24 | } 25 | } 26 | } 27 | }).start(); 28 | //线程2 29 | new Thread(new Thread(){ 30 | @Override 31 | public void run() { 32 | //Thread B 33 | try { 34 | TimeUnit.MILLISECONDS.sleep(2000L); 35 | //setVol( "22"); 36 | //setFlag( "2"); 37 | //vol="22"; 38 | noVol= "33"; 39 | } catch (InterruptedException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | }).start(); 44 | } 45 | 46 | 47 | } 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /front/im_ui/src/ws/addFriend.js: -------------------------------------------------------------------------------- 1 | import { send, commands, packet, MsgHandle, registerHandle } from '@/ws/ws.js'; 2 | import { Notify } from 'vant'; 3 | 4 | class req { constructor(userId, addId) { this.userId = userId, this.addId = addId } } 5 | 6 | 7 | 8 | function addFriend(addId) { 9 | var userid = JSON.parse(sessionStorage.getItem('loginUser')).userId; 10 | var addFriendReq = new req(userid, addId) 11 | 12 | var traceId = new Date().getTime(); 13 | //组packet 14 | var p = new packet(commands.get('addFriend'), traceId, addFriendReq) 15 | 16 | var msg = new MsgHandle(p, addfunc); 17 | send(msg) 18 | } 19 | 20 | //添加方的回调 21 | function addfunc(obj) { 22 | var data = obj.resp.data; 23 | Notify({ 24 | type: 'success', 25 | message: '添加成功:'+data.userName, 26 | duration: 1000 27 | }); 28 | friendsList.list.push({ 'userId': data.userId, 'userName': data.userName, status: 1 ,infoCount:1}) 29 | } 30 | 31 | 32 | //被添加方的回调 33 | function addedFunc(obj) { 34 | var data = obj.data; 35 | Notify({ 36 | type: 'success', 37 | message: '有新的好友添加:'+data.userName, 38 | duration: 1000 39 | }); 40 | friendsList.list.push({ 'userId': data.userId, 'userName': data.userName, status: 1 ,infoCount:1}) 41 | } 42 | registerHandle(commands.get('ADD_NOTIRY'), addedFunc) 43 | 44 | //好友列表 45 | var friendsList = { list: [] }; 46 | 47 | 48 | 49 | 50 | export { addFriend, friendsList } -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/LoginHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import com.wks.customProtocol.Utils; 4 | import com.wks.customProtocol.packet.Command; 5 | import com.wks.customProtocol.packet.Packet; 6 | import com.wks.customProtocol.packet.data.LoginRequestData; 7 | import com.wks.customProtocol.packet.data.LoginResponseData; 8 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.channel.SimpleChannelInboundHandler; 11 | 12 | import static com.wks.customProtocol.Utils.LOGIN_SUCCESS; 13 | 14 | public class LoginHandler extends SimpleChannelInboundHandler { 15 | 16 | @Override 17 | protected void channelRead0(ChannelHandlerContext ctx, LoginRequestData msg) throws Exception { 18 | // 验证 19 | if (valid(msg)) { 20 | LoginResponseData response = new LoginResponseData(true, LOGIN_SUCCESS); 21 | ctx.channel().writeAndFlush(response); 22 | 23 | //绑定session 24 | Utils.bindSession(new Session(msg.getUserId(), msg.getUsername()), ctx.channel()); 25 | } 26 | } 27 | 28 | 29 | @Override 30 | public void channelInactive(ChannelHandlerContext ctx) { 31 | Utils.unBindSession(ctx.channel()); 32 | } 33 | 34 | 35 | private boolean valid(LoginRequestData o) { 36 | System.out.println("登录信息:" + o); 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/AddFriendsService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.domain.Packet; 4 | import com.wks.wsIm.domain.req.AddFriendReq; 5 | import com.wks.wsIm.domain.resp.AddFriendNotify; 6 | import com.wks.wsIm.domain.resp.AddFriendResp; 7 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 8 | 9 | import java.util.Map; 10 | 11 | import static com.wks.wsIm.biz.UserService.GOD_ID; 12 | import static com.wks.wsIm.biz.WriteService.send; 13 | import static com.wks.wsIm.domain.Commands.ADD_FRIENDS; 14 | import static com.wks.wsIm.domain.Commands.ADD_NOTIRY; 15 | import static com.wks.wsIm.server.WebSocketFrameHandler.serializer; 16 | 17 | @Command(ADD_FRIENDS) 18 | public class AddFriendsService extends BaseService { 19 | 20 | 21 | @Override 22 | AddFriendResp process(MsgContext context, AddFriendReq addFriendReq) { 23 | 24 | UserService.addFriend(addFriendReq.getUserId(), addFriendReq.getAddId()); 25 | 26 | //响应被添加者 27 | UserInfo userInfo = UserService.getUserInfo(addFriendReq.getUserId()); 28 | Packet p = new Packet(ADD_NOTIRY, null, new AddFriendNotify(userInfo.getUserId(), userInfo.getUserName())); 29 | UserInfo addedUserInfo = UserService.getUserInfo(addFriendReq.getAddId()); 30 | send(addedUserInfo.getChannel(), p); 31 | 32 | return new AddFriendResp("SUCCESS", "SUCCESS", addedUserInfo.getUserId(), addedUserInfo.getUserName()); 33 | 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/Test/TestVol3.java: -------------------------------------------------------------------------------- 1 | package Test; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public class TestVol3 { 6 | public static int[] arr = new int[20]; 7 | public static boolean flag = true; 8 | 9 | 10 | public static void main(String[] args) throws Exception { 11 | new Thread(new Thread() { 12 | @Override 13 | public void run() { 14 | //线程1 15 | try { 16 | TimeUnit.MILLISECONDS.sleep(3000); 17 | //arr[19] = 2; 18 | flag = false; 19 | //TimeUnit.MILLISECONDS.sleep(20000); 20 | //System.out.println("end 1"); 21 | int count=0; 22 | while(true){ 23 | count++; 24 | } 25 | 26 | } catch (InterruptedException e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | }).start(); 31 | new Thread(new Thread() { 32 | @Override 33 | public void run() { 34 | //线程2 35 | while (flag) { 36 | System.out.println("flag--->"); //就这个影响了 刷新,有就刷,没有就不刷,很奇怪 37 | // if () { 38 | // System.out.println("flag in loop--->" + flag); 39 | // break; 40 | // } 41 | } 42 | System.out.println("Jump out of the loop!"); 43 | } 44 | }).start(); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/UserService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.Util.GodChannel; 4 | import lombok.NonNull; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | public class UserService { 13 | 14 | private final static Map USER_POOL = new ConcurrentHashMap<>(); 15 | 16 | public final static String GOD_ID="123456789"; 17 | //放入我自己 18 | static { 19 | USER_POOL.put(GOD_ID,new UserInfo(GOD_ID,"王柯杉",new GodChannel(),"1")); 20 | } 21 | 22 | 23 | public static Map getUserPool() { 24 | return USER_POOL; 25 | } 26 | 27 | public static boolean addFriend(@NonNull String userId, @NonNull String addId) { 28 | USER_POOL.get(userId).getFriends().add(USER_POOL.get(addId)); 29 | USER_POOL.get(addId).getFriends().add(USER_POOL.get(userId)); 30 | return true; 31 | } 32 | 33 | public static UserInfo getUserInfo(String userId) { 34 | return USER_POOL.get(userId); 35 | } 36 | 37 | public static boolean addUserInfo(String userId, UserInfo userInfo) { 38 | USER_POOL.put(userId, userInfo); 39 | return true; 40 | } 41 | 42 | public static UserInfo removeUser(String userId) { 43 | 44 | return USER_POOL.remove(userId); 45 | } 46 | 47 | public static List getAllUsers(){ 48 | List userInfos=new ArrayList<>(); 49 | USER_POOL.forEach((key, value) -> { 50 | userInfos.add(value); 51 | }); 52 | return userInfos; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/OffLineService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.domain.Packet; 4 | import com.wks.wsIm.domain.resp.OffLineNotify; 5 | import com.wks.wsIm.domain.resp.UserResp; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import static com.wks.wsIm.biz.LoginService.SESSION; 14 | import static com.wks.wsIm.domain.Commands.GET_USER_LIST; 15 | import static com.wks.wsIm.domain.Commands.OFFLINE_NOTIFY; 16 | 17 | @Slf4j 18 | public class OffLineService { 19 | 20 | public static void process(ChannelHandlerContext ctx) { 21 | //删除session 和 用户信息 22 | Session session = ctx.channel().attr(SESSION).get(); 23 | if (session == null || session.getUser() == null) return; 24 | log.info(session.getUser().getUserName() + "退出登录"); 25 | UserInfo user = UserService.removeUser(session.getUser().getUserId()); 26 | user.setStatus("0"); 27 | //删除好友关系 28 | user.getFriends().forEach((v)->{v.getFriends().remove(user);}); 29 | 30 | 31 | //广播用户列表 32 | List users = GetUserListService.getOnlineUsers(); 33 | List userInfos = UserService.getAllUsers(); 34 | Packet p = new Packet(GET_USER_LIST, null, users); 35 | WriteService.spead(p, userInfos); 36 | 37 | //广播下线通知 38 | OffLineNotify notify = new OffLineNotify(user.getUserId(), user.getUserName()); 39 | Packet offLinePacket = new Packet(OFFLINE_NOTIFY, null, notify); 40 | WriteService.spead(offLinePacket, new ArrayList<>(user.getFriends())); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | ${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log 17 | 18 | 30 19 | 20 | 21 | 22 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 23 | 24 | 25 | 26 | 10MB 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /front/im_ui/src/components/Message.vue: -------------------------------------------------------------------------------- 1 | > 16 | 17 | 18 | 25 | 26 | -------------------------------------------------------------------------------- /doc/devNote.md: -------------------------------------------------------------------------------- 1 | ## 开发心路历程 2 | packet 主要想做成 **数据包** 的实体类,但为了简便,直接增加了decode和encode 的方法,所以packet 就是数据包entity 并 拥有与 buffer之间的 转换方法, 3 | 4 | 在开发中 一直在思考,如果 数据包结构变动 要怎么包容,举例来说,各部分的长度变动, 5 | 刚开始把内容全部变成byte[] , 但不能明确表现数据长度,所以如果 协议有变动,最好还是直接继承packet,甚至直接给出packet的接口, 6 | 出现一个问题是 : packet 与 command SerializerAlgorithm 之间 还是有关系,这个关系要怎么确定,目前是直接 packet 里的command直接使用byte 与command 7 | SerializerAlgorithm 直接对应 ,但以后如果这层关系需要解耦的话,就需要再加一层,可以把这个判断逻辑方法直接写在packet里 8 | 9 | 我做了一个packetAnalysis 的接口 定义decode encode 方法 以及 packet 与 command Serializer 的转换逻辑 , 还想定义一个packet本身的接口 ,但问题在于packet应该是一个纯的entity, 10 | 那这个接口,就只有get set 方法可以定义,但如果协议各部分长度不一样,那就影响到了get方法的返回值,所以协议entity之间并不应该有 继承关系,而PacketAnalysis 就带一个泛型 来指定packet类, 11 | 12 | 一个疑问:buffer 和channel 之间是怎么样的一个关系,buffer 的回收是怎样的 13 | 14 | 15 | 本来 担心 输入线程写channel 会和心跳检查的操作 冲突 16 | 检查后发现channel 是线程安全的: 沿着调用链走 发现: 17 | ```AbstractChannelHandlerContext.java 18 | 19 | private void write(Object msg, boolean flush, ChannelPromise promise) { 20 | AbstractChannelHandlerContext next = findContextOutbound(); 21 | final Object m = pipeline.touch(msg, next); 22 | EventExecutor executor = next.executor(); 23 | if (executor.inEventLoop()) { 24 | if (flush) { 25 | next.invokeWriteAndFlush(m, promise); 26 | } else { 27 | next.invokeWrite(m, promise); 28 | } 29 | } else { 30 | AbstractWriteTask task; 31 | if (flush) { 32 | task = WriteAndFlushTask.newInstance(next, m, promise); 33 | } else { 34 | task = WriteTask.newInstance(next, m, promise); 35 | } 36 | safeExecute(executor, task, promise, m); 37 | } 38 | } 39 | ``` 40 | 如果channelContext 的当前线程 是在Event Loop 的线程,那么直接写,感觉就是在正常的event loop里,但如果当前线程不是EventLoop ,那么就把写任务放在Execute里,所以相当于任务的处理一直在Execute里,所以是线程安全的 -------------------------------------------------------------------------------- /front/im_ui/src/ws/getUserList.js: -------------------------------------------------------------------------------- 1 | import {send,commands,packet,registerHandle} from '@/ws/ws.js'; 2 | 3 | //按照拼音和字母分类联系人列表 4 | function pySegSort(arr, empty) { 5 | if (!String.prototype.localeCompare) 6 | return null; 7 | var letters = "*abcdefghjklmnopqrstwxyz".split(''); 8 | var en = "abcdefghjklmnopqrstwxyz".split(''); 9 | var zh = "阿八嚓哒妸发旮哈讥咔垃痳拏噢妑七呥扨它穵夕丫帀".split(''); 10 | var segs = []; 11 | var curr; 12 | letters.forEach(function (item, index) { 13 | curr = { letter: item, data: [] }; 14 | arr.forEach(function (user, index2) { 15 | var item2=user.userName; 16 | var reg = new RegExp('[a-z]') 17 | var firstWord = item2.split('')[0].toLowerCase() 18 | if (reg.test(firstWord)) { 19 | if ((!en[index - 1] || en[index - 1].localeCompare(item2, "en") <= 0) && (item2.localeCompare(en[index], "en") == -1|| !en[index] )) { 20 | curr.data.push(user); 21 | } 22 | } else { 23 | if ((!zh[index - 1] || zh[index - 1].localeCompare(item2, "zh") <= 0) && (item2.localeCompare(zh[index], "zh") == -1|| !zh[index])) { 24 | curr.data.push(user); 25 | } 26 | } 27 | }); 28 | if (empty || curr.data.length) { 29 | segs.push(curr); 30 | } 31 | }); 32 | return segs; 33 | } 34 | 35 | 36 | //数据类型 37 | 38 | //在线用户列表 39 | var userList={list:[]}; 40 | 41 | //消息处理handler 42 | function handler(packet){ 43 | var list=pySegSort(packet.data) 44 | userList.list=list; 45 | } 46 | 47 | //向ws core 中 注册消息处理方法 48 | registerHandle(commands.get('getUserList'),handler) 49 | 50 | //对外提供注册方法 51 | function refreshList(){ 52 | //组packet 53 | var p=new packet(commands.get('getUserList'),null,null) 54 | send(p) 55 | } 56 | 57 | 58 | export {pySegSort,userList,refreshList} -------------------------------------------------------------------------------- /src/test/java/Test/Test4.java: -------------------------------------------------------------------------------- 1 | package Test; 2 | 3 | public class Test4 implements Runnable { 4 | private static volatile ObjectA a; // 加上volatile也无法结束While循环了 5 | 6 | public Test4(ObjectA a) { 7 | this.a = a; 8 | } 9 | 10 | public ObjectA getA() { 11 | return a; 12 | } 13 | 14 | public void setA(ObjectA a) { 15 | this.a = a; 16 | } 17 | 18 | @Override 19 | public void run() { 20 | long i = 0; 21 | ObjectASub sub = a.getSub(); 22 | while (true) { 23 | //a.getSub(); 24 | if(sub.isFlag()){ 25 | break; 26 | } 27 | i++; } 28 | System.out.println("stop My Thread " + i); 29 | } 30 | 31 | public static void main(String[] args) throws InterruptedException { 32 | // 如果启动的时候加上-server 参数则会 输出 Java HotSpot(TM) Server VM 33 | System.out.println(System.getProperty("java.vm.name")); 34 | ObjectASub sub = new ObjectASub(); 35 | ObjectA sa = new ObjectA(); 36 | sa.setSub(sub); 37 | Test4 test = new Test4(sa); 38 | new Thread(test).start(); 39 | 40 | 41 | Thread.sleep(1000); 42 | sub.setFlag(true); 43 | Thread.sleep(1000); 44 | System.out.println("Main Thread " + sub.isFlag()); 45 | } 46 | 47 | static class ObjectA { 48 | private ObjectASub sub; 49 | 50 | public ObjectASub getSub() { 51 | return sub; 52 | } 53 | 54 | public void setSub(ObjectASub sub) { 55 | this.sub = sub; 56 | } 57 | } 58 | 59 | static class ObjectASub{ 60 | private boolean flag=false; 61 | 62 | public boolean isFlag() { 63 | return flag; 64 | } 65 | 66 | public void setFlag(boolean flag) { 67 | this.flag = flag; 68 | } 69 | 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/Utils.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.util.Attribute; 5 | import io.netty.util.AttributeKey; 6 | import com.wks.customProtocol.server.Session; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * 偷个懒 把所有的常量和 常用方法 放在一起 12 | */ 13 | public class Utils { 14 | 15 | //登录相关code 16 | public final static Integer LOGIN_SUCCESS = 1000; 17 | 18 | public final static AttributeKey LOGIN_STATUS = AttributeKey.newInstance("LOGIN"); 19 | 20 | 21 | public final static AttributeKey SESSION = AttributeKey.newInstance("SESSION"); 22 | 23 | 24 | private static final Map userIdChannelMap = new ConcurrentHashMap<>(); 25 | 26 | 27 | public static void markLogin(Channel channel) { 28 | channel.attr(LOGIN_STATUS).set(true); 29 | } 30 | 31 | /** 32 | * 用于客户端判断是否登录 33 | * 34 | * @param channel 35 | * @return 36 | */ 37 | public static boolean clientHasLogin(Channel channel) { 38 | Attribute loginAttr = channel.attr(LOGIN_STATUS); 39 | 40 | return loginAttr.get() != null; 41 | } 42 | 43 | 44 | public static void bindSession(Session session, Channel channel) { 45 | userIdChannelMap.put(session.getUserId(), channel); 46 | channel.attr(SESSION).set(session); 47 | } 48 | 49 | public static void unBindSession(Channel channel) { 50 | if (getSession(channel) != null) { 51 | userIdChannelMap.remove(getSession(channel).getUserId()); 52 | channel.attr(SESSION).set(null); 53 | } 54 | } 55 | 56 | 57 | public static Session getSession(Channel channel) { 58 | 59 | return channel.hasAttr(SESSION) ? channel.attr(SESSION).get() : null; 60 | } 61 | 62 | public static Channel getChannel(String userId) { 63 | 64 | return userIdChannelMap.get(userId); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/LoginService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.domain.Packet; 4 | import com.wks.wsIm.domain.req.Login; 5 | import com.wks.wsIm.domain.resp.LoginResp; 6 | import com.wks.wsIm.domain.resp.SendResp; 7 | import com.wks.wsIm.domain.resp.UserResp; 8 | import io.netty.channel.Channel; 9 | import io.netty.util.AttributeKey; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.util.*; 13 | 14 | import static com.wks.wsIm.biz.WriteService.send; 15 | import static com.wks.wsIm.domain.Commands.GET_USER_LIST; 16 | import static com.wks.wsIm.domain.Commands.LOGIN; 17 | import static com.wks.wsIm.domain.Commands.SEND_MSSAGE; 18 | 19 | @Command(LOGIN) 20 | @Slf4j 21 | public class LoginService extends BaseService { 22 | 23 | 24 | public final static AttributeKey SESSION = AttributeKey.newInstance("SESSION"); 25 | 26 | 27 | @Override 28 | LoginResp process(MsgContext context, Login login) { 29 | //如果已经登录过,就用之前的userId 30 | Session lastLogin = context.getChannel().attr(SESSION).get(); 31 | String oldUserId = null; 32 | if (lastLogin != null) { 33 | oldUserId = lastLogin.getUser().getUserId(); 34 | } 35 | 36 | log.info("登录=》"+ login.getUserName()); 37 | 38 | //generate userID 39 | String userId = oldUserId == null ? generateUserId() : oldUserId; 40 | 41 | UserInfo user = new UserInfo(userId, login.getUserName(), context.getChannel(),"1"); 42 | 43 | UserService.addUserInfo(userId, user); 44 | context.getChannel().attr(SESSION).set(new Session(user)); 45 | 46 | //广播用户列表 47 | List users= GetUserListService.getOnlineUsers(); 48 | List userInfos=UserService.getAllUsers(); 49 | Packet p=new Packet(GET_USER_LIST,null,users); 50 | WriteService.spead(p,userInfos); 51 | 52 | 53 | return new LoginResp(userId); 54 | } 55 | 56 | 57 | public static String generateUserId() { 58 | return UUID.randomUUID().toString(); 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/LifeCyCleTestHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelInboundHandlerAdapter; 5 | 6 | public class LifeCyCleTestHandler extends ChannelInboundHandlerAdapter { 7 | @Override 8 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 9 | System.out.println("逻辑处理器被添加:handlerAdded()"); 10 | super.handlerAdded(ctx); 11 | } 12 | 13 | @Override 14 | public void channelRegistered(ChannelHandlerContext ctx) throws Exception { 15 | System.out.println("channel 绑定到线程(NioEventLoop):channelRegistered()"); 16 | super.channelRegistered(ctx); 17 | } 18 | 19 | @Override 20 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 21 | System.out.println("channel 准备就绪:channelActive()"); 22 | super.channelActive(ctx); 23 | } 24 | 25 | @Override 26 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 27 | System.out.println("channel 有数据可读:channelRead()"); 28 | super.channelRead(ctx, msg); 29 | } 30 | 31 | @Override 32 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 33 | System.out.println("channel 某次数据读完:channelReadComplete()"); 34 | super.channelReadComplete(ctx); 35 | } 36 | 37 | @Override 38 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 39 | System.out.println("channel 被关闭:channelInactive()"); 40 | super.channelInactive(ctx); 41 | } 42 | 43 | @Override 44 | public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { 45 | System.out.println("channel 取消线程(NioEventLoop) 的绑定: channelUnregistered()"); 46 | super.channelUnregistered(ctx); 47 | } 48 | 49 | @Override 50 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 51 | System.out.println("逻辑处理器被移除:handlerRemoved()"); 52 | super.handlerRemoved(ctx); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/client/ClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.client; 2 | 3 | import com.wks.customProtocol.Utils; 4 | import com.wks.customProtocol.packet.Command; 5 | import com.wks.customProtocol.packet.Packet; 6 | import com.wks.customProtocol.packet.data.LoginRequestData; 7 | import com.wks.customProtocol.packet.data.LoginResponseData; 8 | import com.wks.customProtocol.packet.data.MessageResponseData; 9 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 10 | import io.netty.buffer.ByteBuf; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.ChannelInboundHandlerAdapter; 13 | 14 | 15 | @Deprecated 16 | public class ClientHandler extends ChannelInboundHandlerAdapter { 17 | 18 | @Override 19 | public void channelActive(ChannelHandlerContext ctx) { 20 | LoginRequestData data = new LoginRequestData("1", "wks", "wks"); 21 | Packet p = new Packet(Command.LOGIN_REQUEST, SerializerAlgorithm.DEFAULT.serialize(data)); 22 | ByteBuf buf = p.encode(ctx.alloc().buffer()); 23 | ctx.channel().writeAndFlush(buf); 24 | } 25 | 26 | 27 | @Override 28 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 29 | ByteBuf byteBuf = (ByteBuf) msg; 30 | 31 | //解码 32 | Packet p2 = new Packet(); 33 | p2.decode(byteBuf); 34 | 35 | if (p2.getCommandOperation() == Command.LOGIN_REQUEST) { 36 | //反序列化数据 37 | LoginResponseData o = SerializerAlgorithm.getSerializer(p2.getSerializerAlgorithm()).deserialize(LoginResponseData.class, p2.data); 38 | // 验证 39 | if (o.getIsSuccessful()) { 40 | Utils.markLogin(ctx.channel()); 41 | System.out.println("登录成功"); 42 | } else { 43 | System.out.println("登录失败" + "错误代码:" + o.getCode()); 44 | } 45 | } else if (p2.getCommand() == Command.MESSAGE_REQUEST) { 46 | MessageResponseData o = SerializerAlgorithm.getSerializer(p2.getSerializerAlgorithm()).deserialize(MessageResponseData.class, p2.data); 47 | System.out.println("服务器返回内容:" + o); 48 | 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Netty_IM.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /doc/netty的组件.md: -------------------------------------------------------------------------------- 1 | ## netty 的组件 2 | * handler 3 | ```apple js 4 | 第一个子接口是 ChannelInboundHandler,从字面意思也可以猜到,他是处理读数据的逻辑,比如,我们在一端读到一段数据,首先要解析这段数据,然后对这些数据做一系列逻辑处理,最终把响应写到对端, 在开始组装响应之前的所有的逻辑,都可以放置在 ChannelInboundHandler 里处理,它的一个最重要的方法就是 channelRead()。读者可以将 ChannelInboundHandler 的逻辑处理过程与 TCP 的七层协议的解析联系起来,收到的数据一层层从物理层上升到我们的应用层。 5 | 6 | 第二个子接口 ChannelOutBoundHandler 是处理写数据的逻辑,它是定义我们一端在组装完响应之后,把数据写到对端的逻辑,比如,我们封装好一个 response 对象,接下来我们有可能对这个 response 做一些其他的特殊逻辑,然后,再编码成 ByteBuf,最终写到对端,它里面最核心的一个方法就是 write(),读者可以将 ChannelOutBoundHandler 的逻辑处理过程与 TCP 的七层协议的封装过程联系起来,我们在应用层组装响应之后,通过层层协议的封装,直到最底层的物理层。 7 | 8 | 为什么 read 方法中msg 类型是 Object,因为pipeline 采用的是责任链模型,上一个msg 会传入到下一个handler,所以第一个handler的实例其实是byteBuffer,而后面的handler的msg 取决于前面的handler传入类型 9 | 10 | 基于 SimpleChannelInboundHandler,我们可以实现每一种指令的处理,不再需要强转,不再有冗长乏味的 if else 逻辑,不需要手动传递对象。 11 | 12 | ``` 13 | 14 | * EventLoopGroup 15 | ``` 16 | 是处理I/O操作的多线程事件循环。 Netty为不同类型的传输提供了各种EventLoopGroup实现。 在此示例中,实现的是服务器端应用程序,因此将使用两个NioEventLoopGroup。 17 | 第一个通常称为“boss”,接受传入连接。 第二个通常称为“worker”,当“boss”接受连接并且向“worker”注册接受连接,则“worker”处理所接受连接的流量。 18 | 使用多少个线程以及如何将它们映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。 19 | ``` 20 | 21 | * ServerBootstrap 22 | 23 | 24 | * channel 25 | 26 | 与NIO中的概念一样,代表一个连接, channel 是线程安全的,可以随意的写,即使是自定义的线程里 27 | * pipeline 28 | 29 | pipeline() 返回的是和这条连接相关的逻辑处理链,采用了责任链模式 30 | pipline 的链中顺序对channel 和message 进行处理 31 | * ChannelFuture 32 | 继承于future, 33 | * buffer 34 | 35 | * ChannelHandlerContext 36 | 37 | * 解码器 38 | 39 | 基于 ByteToMessageDecoder,我们可以实现自定义解码,而不用关心 ByteBuf 的强转和 解码结果的传递。 40 | 基于 MessageToByteEncoder,我们可以实现自定义编码,而不用关心 ByteBuf 的创建,不用每次向对端写 Java 对象都进行一次编码。 41 | 且encode 和decode 可以自动回收buffer 42 | 43 | * 拆包 44 | ```apple js 45 | 1. 固定长度的拆包器 FixedLengthFrameDecoder 46 | 如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。 47 | 48 | 2. 行拆包器 LineBasedFrameDecoder 49 | 从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。 50 | 51 | 3. 分隔符拆包器 DelimiterBasedFrameDecoder 52 | DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。 53 | 54 | 4. 基于长度域拆包器 LengthFieldBasedFrameDecoder 55 | 最后一种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。由于上面三种拆包器比较简单,读者可以自行写出 demo,接下来,我们就结合我们小册的自定义协议,来学习一下如何使用基于长度域的拆包器来拆解我们的数据包。 56 | 57 | ``` -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/client/LoginHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.client; 2 | 3 | import com.wks.customProtocol.Utils; 4 | import com.wks.customProtocol.packet.Command; 5 | import com.wks.customProtocol.packet.Packet; 6 | import com.wks.customProtocol.packet.data.LoginResponseData; 7 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.SimpleChannelInboundHandler; 10 | 11 | public class LoginHandler extends SimpleChannelInboundHandler { 12 | 13 | 14 | @Override 15 | public void channelActive(ChannelHandlerContext ctx) { 16 | 17 | } 18 | 19 | @Override 20 | protected void channelRead0(ChannelHandlerContext ctx, LoginResponseData msg) throws Exception { 21 | // 验证 22 | if (msg.getIsSuccessful()) { 23 | System.out.println("登录成功"); 24 | 25 | //唤醒输入线程 26 | synchronized (client.waitForLoginEnd){ 27 | client.waitForLoginEnd.notify(); 28 | } 29 | 30 | Utils.markLogin(ctx.channel()); 31 | } else { 32 | System.out.println("登录失败" + "错误代码:" + msg.getCode()); 33 | } 34 | } 35 | 36 | 37 | public static void main(String[] args) throws InterruptedException { 38 | 39 | Object o = new Object(); 40 | 41 | final Thread a = new Thread(new Runnable() { 42 | @Override 43 | public void run() { 44 | System.out.println("ss"); 45 | 46 | synchronized (o) { 47 | try { 48 | o.wait(); 49 | } catch (InterruptedException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | 54 | System.out.println("bb"); 55 | } 56 | }); 57 | a.start(); 58 | Thread.sleep(1000); 59 | Thread b = new Thread(new Runnable() { 60 | @Override 61 | public void run() { 62 | synchronized (o) { 63 | System.out.println("唤醒"); 64 | o.notify(); 65 | System.out.println("end"); 66 | } 67 | } 68 | }); 69 | b.start(); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/WebSocketServerInitializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.wks.wsIm.server; 17 | 18 | import io.netty.channel.ChannelInitializer; 19 | import io.netty.channel.ChannelPipeline; 20 | import io.netty.channel.socket.SocketChannel; 21 | import io.netty.handler.codec.http.HttpObjectAggregator; 22 | import io.netty.handler.codec.http.HttpServerCodec; 23 | import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; 24 | import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; 25 | import io.netty.handler.ssl.SslContext; 26 | import io.netty.handler.stream.ChunkedWriteHandler; 27 | 28 | /** 29 | */ 30 | public class WebSocketServerInitializer extends ChannelInitializer { 31 | 32 | private static final String WEBSOCKET_PATH = "/websocket"; 33 | 34 | private final SslContext sslCtx; 35 | 36 | public WebSocketServerInitializer(SslContext sslCtx) { 37 | this.sslCtx = sslCtx; 38 | } 39 | 40 | @Override 41 | public void initChannel(SocketChannel ch) throws Exception { 42 | ChannelPipeline pipeline = ch.pipeline(); 43 | if (sslCtx != null) { 44 | pipeline.addLast(sslCtx.newHandler(ch.alloc())); 45 | } 46 | pipeline.addLast(new IMIdleStateHandler()); 47 | pipeline.addLast(new HttpServerCodec()); 48 | pipeline.addLast(new HttpObjectAggregator(65536)); //聚合 htp requet中的chunk内容, 49 | pipeline.addLast(new ChunkedWriteHandler());//聚合response中的大量数据内容 50 | pipeline.addLast(new WebSocketServerCompressionHandler());//在这里处理websocket的扩展,协议升级 51 | pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));//当 会自动add一个握手协议 52 | pipeline.addLast(new HttpStaticFileServerHandler());//静态页面服务 53 | pipeline.addLast(new WebSocketFrameHandler()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/wks/netty_test/ByteBufTest.java: -------------------------------------------------------------------------------- 1 | package com.wks.netty_test; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufAllocator; 5 | 6 | public class ByteBufTest { 7 | public static void main(String[] args) { 8 | ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(9, 100); 9 | 10 | print("allocate ByteBuf(9, 100)", buffer); 11 | 12 | // write 方法改变写指针,写完之后写指针未到 capacity 的时候,buffer 仍然可写 13 | buffer.writeBytes(new byte[]{1, 2, 3, 4}); 14 | print("writeBytes(1,2,3,4)", buffer); 15 | 16 | // write 方法改变写指针,写完之后写指针未到 capacity 的时候,buffer 仍然可写, 写完 int 类型之后,写指针增加4 17 | buffer.writeInt(12); 18 | print("writeInt(12)", buffer); 19 | 20 | // write 方法改变写指针, 写完之后写指针等于 capacity 的时候,buffer 不可写 TODO 这个地方很奇怪,为什么指针等于capacity 的时候 ,不可写 而再写的时候 就扩容了 21 | buffer.writeBytes(new byte[]{5}); 22 | print("writeBytes(5)", buffer); 23 | 24 | // write 方法改变写指针,写的时候发现 buffer 不可写则开始扩容,扩容之后 capacity 随即改变 25 | buffer.writeBytes(new byte[]{6}); 26 | print("writeBytes(6)", buffer); 27 | 28 | // get 方法不改变读写指针 29 | System.out.println("getByte(3) return: " + buffer.getByte(3)); 30 | System.out.println("getShort(3) return: " + buffer.getShort(3)); 31 | System.out.println("getInt(3) return: " + buffer.getInt(3)); 32 | print("getByte()", buffer); 33 | 34 | 35 | // set 方法不改变读写指针 36 | buffer.setByte(buffer.readableBytes() + 1, 0); 37 | print("setByte()", buffer); 38 | 39 | // read 方法改变读指针 40 | byte[] dst = new byte[buffer.readableBytes()]; 41 | buffer.readBytes(dst); 42 | print("readBytes(" + dst.length + ")", buffer); 43 | 44 | 45 | } 46 | 47 | private static void print(String action, ByteBuf buffer) { 48 | System.out.println("after ===========" + action + "============"); 49 | System.out.println("capacity(): " + buffer.capacity()); 50 | System.out.println("maxCapacity(): " + buffer.maxCapacity()); 51 | System.out.println("readerIndex(): " + buffer.readerIndex()); 52 | System.out.println("readableBytes(): " + buffer.readableBytes()); 53 | System.out.println("isReadable(): " + buffer.isReadable()); 54 | System.out.println("writerIndex(): " + buffer.writerIndex()); 55 | System.out.println("writableBytes(): " + buffer.writableBytes()); 56 | System.out.println("isWritable(): " + buffer.isWritable()); 57 | System.out.println("maxWritableBytes(): " + buffer.maxWritableBytes()); 58 | System.out.println(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 使用vue ,netty 基于websocket的 即时通讯系统。 2 | github: https://github.com/wangkeshan9538/Netty_IM 3 | site: http://47.105.88.240:8080/ 4 | 5 | # 使用示例 6 | 7 | ![avatar](./doc/ttt.gif) 8 | 9 | # 主要结构 10 | 11 | ![avatar](./doc/yyy.png) 12 | 13 | 14 | ## 数据包结构为json形式: 15 | ```java 16 | private String command; 17 | 18 | private String traceId; 19 | 20 | private Object data; 21 | ``` 22 | command 表示数据包指令类型, 23 | 因为websocket协议是全双工,并不能像http协议一样一问一答的形式,所以对于client,在原生协议上无法链接一个request和一个response,所以使用traceId来判断, 24 | data为数据 25 | 26 | ## 前端结构说明 27 | 如果是一问一答的需要traceId 的应答模式,则对req,以及对应的回调方法打包进队列,如果resp回应,则回调处理 28 | 如果是单向的,则根据command 来找到注册的处理方法处理 29 | 30 | ## 后端结构 31 | handler: 32 | 33 | ``` java 34 | pipeline.addLast(new IMIdleStateHandler()); //空闲连接处理,超时则关闭 35 | pipeline.addLast(new HttpServerCodec());//http协议编解码器 36 | pipeline.addLast(new HttpObjectAggregator(65536)); //聚合 htp requet中的chunk内容, 37 | pipeline.addLast(new ChunkedWriteHandler());//聚合response中的大量数据内容 38 | pipeline.addLast(new WebSocketServerCompressionHandler());//处理websocket的扩展以及判断协议升级 39 | pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));//会自动添加websocket握手handler,握手完成会添加websocket的编解码器 40 | pipeline.addLast(new HttpStaticFileServerHandler());//静态页面服务, 41 | pipeline.addLast(new WebSocketFrameHandler()); //业务流程的开始 42 | ``` 43 | 44 | 业务方法举例: 45 | 46 | ``` java 47 | @Command(ADD_FRIENDS) 48 | public class AddFriendsService extends BaseService { 49 | 50 | @Override 51 | AddFriendResp process(MsgContext context, AddFriendReq addFriendReq) { 52 | UserService.addFriend(addFriendReq.getUserId(), addFriendReq.getAddId()); 53 | 54 | //响应被添加者 55 | UserInfo userInfo = UserService.getUserInfo(addFriendReq.getUserId()); 56 | Packet p = new Packet(ADD_NOTIRY, null, new AddFriendNotify(userInfo.getUserId(), userInfo.getUserName())); 57 | UserInfo addedUserInfo = UserService.getUserInfo(addFriendReq.getAddId()); 58 | send(addedUserInfo.getChannel(), p); 59 | 60 | return new AddFriendResp("SUCCESS", "SUCCESS", addedUserInfo.getUserId(), addedUserInfo.getUserName()); 61 | } 62 | } 63 | 64 | ``` 65 | @Command为自定义注解,标注处理的command类型,泛型为req,resp的类型, 66 | 67 | ## 运行 68 | 前端: vue ui直接打包 69 | 后端: java -jar -Dfront_dir=E:\code\Netty_IM\front\im_ui\dist -Dport=8080 netty_im-1.0.jar 70 | front_dir 为前端目录 71 | port 72 | 73 | ## TODO 74 | 其实看起来完全就是个粗糙的玩具,列一些后续可以继续做的方向吧,虽然我这么懒估计也不会去做 75 | 1. netty 用起来还是有点太底层了,静态服务器的代码都要自己找,所以可以考虑换成vertx,比较小清新,还能做集群 76 | 2. 想做一个压测工具,来压测下性能, 77 | 3. ssl的支持, 78 | -------------------------------------------------------------------------------- /src/test/java/Test/Test.java: -------------------------------------------------------------------------------- 1 | package Test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.alibaba.fastjson.JSON; 7 | 8 | class User { 9 | private String name; 10 | private int age; 11 | 12 | public String getName() { 13 | return name; 14 | } 15 | 16 | public void setName(String name) { 17 | this.name = name; 18 | } 19 | 20 | public int getAge() { 21 | return age; 22 | } 23 | 24 | public void setAge(int age) { 25 | this.age = age; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "User [name=" + name + ", age=" + age + "]"; 31 | } 32 | }; 33 | 34 | class UserGroup { 35 | private String name; 36 | private List users = new ArrayList(); 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | public void setName(String name) { 43 | this.name = name; 44 | } 45 | 46 | public List getUsers() { 47 | return users; 48 | } 49 | 50 | public void setUsers(List users) { 51 | this.users = users; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "UserGroup [name=" + name + ", users=" + users + "]"; 57 | } 58 | } 59 | 60 | class FastJsonTest { 61 | public static void main(String[] args) { 62 | // 构建用户geust 63 | User guestUser = new User(); 64 | guestUser.setName("guest"); 65 | guestUser.setAge(28); 66 | // 构建用户root 67 | User rootUser = new User(); 68 | rootUser.setName("root"); 69 | guestUser.setAge(35); 70 | // 构建用户组对象 71 | UserGroup group = new UserGroup(); 72 | group.setName("admin"); 73 | group.getUsers().add(guestUser); 74 | group.getUsers().add(rootUser); 75 | // 用户组对象转JSON串 76 | String jsonString = JSON.toJSONString(group); 77 | System.out.println("jsonString:" + jsonString); 78 | // JSON串转用户组对象 79 | UserGroup group2 = JSON.parseObject(jsonString, UserGroup.class); 80 | System.out.println("group2:" + group2); 81 | 82 | System.out.println( JSON.parseObject(jsonString,UserGroup.class)); 83 | 84 | /* // 构建用户对象数组 85 | Object[] users = new Object[2]; 86 | users[0] = guestUser; 87 | users[1] = rootUser; 88 | // 用户对象数组转JSON串 89 | String jsonString2 = JSON.toJSONString(users); 90 | System.out.println("jsonString2:" + jsonString2); 91 | // JSON串转用户对象列表 92 | List users2 = JSON.parseArray(jsonString2, User.class); 93 | System.out.println("users2:" + users2);*/ 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/ServerHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import com.wks.customProtocol.packet.Command; 4 | import com.wks.customProtocol.packet.Packet; 5 | import com.wks.customProtocol.packet.data.LoginRequestData; 6 | import com.wks.customProtocol.packet.data.LoginResponseData; 7 | import com.wks.customProtocol.packet.data.MessageRequestData; 8 | import com.wks.customProtocol.packet.data.MessageResponseData; 9 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 10 | import io.netty.buffer.ByteBuf; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.ChannelInboundHandlerAdapter; 13 | import static com.wks.customProtocol.Utils.LOGIN_SUCCESS; 14 | 15 | @Deprecated 16 | public class ServerHandler extends ChannelInboundHandlerAdapter { 17 | 18 | @Override 19 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 20 | 21 | //解码 22 | ByteBuf requestByteBuf = (ByteBuf) msg; 23 | Packet p2 = new Packet(); 24 | p2.decode(requestByteBuf); 25 | 26 | if (p2.getCommandOperation() == Command.LOGIN_REQUEST) { 27 | //反序列化数据 28 | LoginRequestData o = SerializerAlgorithm.getSerializer(p2.getSerializerAlgorithm()).deserialize(LoginRequestData.class, p2.data); 29 | System.out.println(o); 30 | // 验证 31 | if (valid(o)) { 32 | //标记登录 33 | //Utils.markAsLogin(ctx.channel()); 34 | //返回 35 | LoginResponseData response = new LoginResponseData(true, LOGIN_SUCCESS); 36 | Packet p = new Packet(Command.LOGIN_REQUEST, SerializerAlgorithm.DEFAULT.serialize(response)); 37 | ByteBuf buf = p.encode(ctx.alloc().buffer()); 38 | ctx.channel().writeAndFlush(buf); 39 | } 40 | 41 | } else if(p2.getCommandOperation() == Command.MESSAGE_REQUEST){ 42 | //反序列化数据 43 | MessageRequestData o = SerializerAlgorithm.getSerializer(p2.getSerializerAlgorithm()).deserialize(MessageRequestData.class, p2.data); 44 | System.out.println("客户端消息:"+o); 45 | 46 | //反馈 47 | MessageResponseData messageResponseData=new MessageResponseData("","","服务端回复【" + o.getMessage() + "】"); 48 | Packet p = new Packet(Command.MESSAGE_REQUEST, SerializerAlgorithm.DEFAULT.serialize(messageResponseData)); 49 | ByteBuf buf = p.encode(ctx.alloc().buffer()); 50 | ctx.channel().writeAndFlush(buf); 51 | } 52 | } 53 | 54 | private boolean valid(LoginRequestData o) { 55 | System.out.println("登录信息:" + o); 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/Router.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | import com.wks.wsIm.Util.ClassUtil; 4 | import com.wks.wsIm.domain.Packet; 5 | import com.wks.wsIm.domain.resp.ErrorResp; 6 | import com.wks.wsIm.serializer.JSONSerializer; 7 | import com.wks.wsIm.serializer.Serializer; 8 | import lombok.extern.slf4j.Slf4j; 9 | import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl; 10 | 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.lang.reflect.ParameterizedType; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | import static com.wks.wsIm.domain.Commands.ERROR; 18 | 19 | /** 20 | * 根据 packet来路由到对应的service 21 | */ 22 | @Slf4j 23 | public class Router { 24 | 25 | 26 | public final static Map mapping = new HashMap<>(); 27 | 28 | JSONSerializer serializer = new JSONSerializer(); 29 | 30 | static { 31 | //TODO 可以再加一个Order 注解 来表示一个命令的 处理 pipeline 32 | Set> services = ClassUtil.scanPackage("com.wks.wsIm.biz", (c) -> 33 | c.getAnnotation(Command.class) != null && 34 | c.getSuperclass().equals(BaseService.class) && 35 | c.getAnnotation(Deprecated.class) == null); 36 | services.forEach((c) -> { 37 | mapping.put(c.getAnnotation(Command.class).value(), c); 38 | }); 39 | } 40 | 41 | public Packet router(MsgContext context, Packet p) throws ClassNotFoundException { 42 | Class clezz = mapping.get(p.getCommand()); 43 | 44 | Object result = null; 45 | 46 | Class reqType = Class.forName(((ParameterizedType) clezz.getGenericSuperclass()).getActualTypeArguments()[0].getTypeName()); 47 | Class resptype = Class.forName(((ParameterizedType) clezz.getGenericSuperclass()).getActualTypeArguments()[1].getTypeName()); 48 | 49 | 50 | //填充req 51 | // TODO 这块对于入参出参的处理 明显不够好,入参出参带个泛型,就没办法处理了,我觉得用泛型来标志入参出参并不是一个好的方式,应该需要看下springmvc对入参出参怎么处理的 52 | Object req = null; 53 | if (!reqType.equals(Void.class)) { 54 | req = serializer.desData(p, reqType); 55 | } 56 | 57 | //process req 58 | try { 59 | result = clezz.getDeclaredMethod("process", MsgContext.class, Object.class).invoke(clezz.newInstance(), context, req); 60 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException e) { 61 | log.error("方法invoeke 失败" + p, e); 62 | return new Packet(ERROR, p.getTraceId(), new ErrorResp("未知错误")); 63 | } 64 | 65 | 66 | //处理result 67 | if (!resptype.equals(Void.class)) { 68 | return new Packet(p.getCommand(), p.getTraceId(), result); 69 | } 70 | return null; 71 | } 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /front/im_ui/src/ws/ws.js: -------------------------------------------------------------------------------- 1 | 2 | //初始化 3 | var socket; 4 | if (!window.WebSocket) { 5 | window.WebSocket = window.MozWebSocket; 6 | } 7 | if (window.WebSocket) { 8 | socket = new WebSocket("ws://"+window.location.host+"/websocket"); 9 | socket.onmessage = function (event) { 10 | var packet=JSON.parse( event.data); 11 | //查找下有没有traceId 12 | if(undefined== packet.traceId ||null==packet.traceId){ 13 | //直接进注册的handle 14 | var handle=cmdHandle.get(packet.command) 15 | handle(packet) 16 | }else{ 17 | //进队列中原消息的handle 18 | var index=wsMsgQueue.findIndex((n)=> { 19 | return n.req.command==packet.command&&n.req.traceId==packet.traceId 20 | }) 21 | var msghandle; 22 | //remove 23 | if(index>=0){ 24 | msghandle=wsMsgQueue.splice(index, 1)[0] 25 | msghandle.resp=packet; 26 | msghandle.handle(msghandle); 27 | } 28 | 29 | } 30 | 31 | }; 32 | socket.onopen = function (event) { 33 | console.info("websocket打开连接") 34 | }; 35 | socket.onclose = function (event) { 36 | console.info("websocket关闭连接") 37 | }; 38 | } else { 39 | alert("Your browser does not support Web Socket."); 40 | } 41 | 42 | 43 | function send(msg) { 44 | if (!window.WebSocket) { return; } 45 | if (socket.readyState == WebSocket.OPEN) { 46 | 47 | if(msg instanceof packet){ 48 | socket.send(JSON.stringify(msg)); 49 | 50 | }else if(msg instanceof MsgHandle){ 51 | socket.send(JSON.stringify( msg.req)); 52 | //进队列 53 | wsMsgQueue.push(msg) 54 | } 55 | 56 | } else { 57 | alert("The socket is not open."); 58 | } 59 | } 60 | 61 | function registerHandle(command,handle){ 62 | cmdHandle.set(command,handle) 63 | } 64 | 65 | var wsMsgQueue=[] 66 | 67 | 68 | //数据包格式 69 | class packet{ 70 | constructor(command,traceId,data){ 71 | this.command = command, 72 | this.traceId = traceId, 73 | this.data = data 74 | } 75 | } 76 | 77 | //消息带handler 78 | class MsgHandle{ 79 | constructor(req,handle){ 80 | this.req=req, //packet 81 | this.resp={}, //packet 82 | this.handle=handle 83 | } 84 | } 85 | 86 | 87 | //存储命令 88 | const commands =new Map([ 89 | ['login','0'], 90 | ['getUserList','1'], 91 | ['SEND_MSSAGE','2'], 92 | ['addFriend','3'], 93 | ['ADD_NOTIRY','4'], 94 | ['MESSAGE_NOTIFY','5'], 95 | ['OFFLINE_NOTIFY','6'] 96 | ]) 97 | 98 | //存储命令对应的处理器 99 | var cmdHandle=new Map([ 100 | 101 | ]) 102 | 103 | 104 | export {send,commands,packet,MsgHandle,registerHandle} -------------------------------------------------------------------------------- /src/test/java/com/wks/netty_test/NettyClient.java: -------------------------------------------------------------------------------- 1 | package com.wks.netty_test; 2 | 3 | import io.netty.bootstrap.Bootstrap; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelInitializer; 6 | import io.netty.channel.ChannelOption; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | import io.netty.channel.socket.nio.NioSocketChannel; 9 | import io.netty.handler.codec.string.StringEncoder; 10 | import io.netty.util.AttributeKey; 11 | 12 | import java.util.Date; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public class NettyClient { 16 | public static void main(String[] args) throws InterruptedException { 17 | Bootstrap bootstrap = new Bootstrap(); 18 | NioEventLoopGroup group = new NioEventLoopGroup(); 19 | 20 | bootstrap.group(group) 21 | .attr(AttributeKey.newInstance("clientName"), "nettyClient") 22 | .channel(NioSocketChannel.class) 23 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) 24 | .option(ChannelOption.SO_KEEPALIVE, true) 25 | .option(ChannelOption.TCP_NODELAY, true) 26 | .handler(new ChannelInitializer() { 27 | @Override 28 | protected void initChannel(Channel ch) { 29 | //ch.pipeline().addLast(new FirstClientHandler()); 30 | } 31 | }); 32 | 33 | /*Channel channel = bootstrap.connect("127.0.0.1", 8080).addListener(future -> { 34 | if (future.isSuccess()) { 35 | System.out.println("连接成功!"); 36 | } else { 37 | System.err.println("连接失败!"); 38 | } 39 | 40 | }).channel();*/ 41 | 42 | connect(bootstrap, "localhost", 8080, MAX_RETRY); 43 | } 44 | 45 | final static int MAX_RETRY=5; 46 | 47 | private static void connect(Bootstrap bootstrap, String host, int port, int retry) { 48 | bootstrap.connect(host, port).addListener(future -> { 49 | if (future.isSuccess()) { 50 | System.out.println("连接成功!"); 51 | } else if (retry == 0) { 52 | System.err.println("重试次数已用完,放弃连接!"); 53 | } else { 54 | // 第几次重连 55 | int order = (MAX_RETRY - retry) + 1; 56 | // 本次重连的间隔 57 | int delay = 1 << order; 58 | System.out.println(delay+"sss"); 59 | System.err.println(new Date() + ": 连接失败,第" + order + "次重连……"); 60 | bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit 61 | .SECONDS); 62 | } 63 | }); 64 | } 65 | 66 | } 67 | 68 | 69 | 70 | /* 调 workerGroup 的 schedule 方法即可实现定时任务逻辑。 并不是循环执行 ,只执行一次 */ 71 | 72 | /* ch.pipeline() 返回的是和这条连接相关的逻辑处理链,采用了责任链模式*/ -------------------------------------------------------------------------------- /front/im_ui/src/ws/sendMsg.js: -------------------------------------------------------------------------------- 1 | import { send, commands, packet, MsgHandle, registerHandle } from '@/ws/ws.js'; 2 | import { friendsList } from "@/ws/addFriend.js"; 3 | import { Notify } from 'vant'; 4 | import router from "@/router"; 5 | 6 | class req { constructor(snedFromID, sendToId, msg) { this.snedFromID = snedFromID, this.sendToId = sendToId, this.msg = msg, this.time = new Date() } } 7 | 8 | 9 | function sendMsg(sendToId, msg) { 10 | var userid = JSON.parse(sessionStorage.getItem('loginUser')).userId; 11 | var sendReq = new req(userid, sendToId, msg) 12 | 13 | var traceId = new Date().getTime(); 14 | //组packet 15 | var p = new packet(commands.get('SEND_MSSAGE'), traceId, sendReq) 16 | 17 | 18 | //添加到消息box里 19 | var m = new Msg('send', msg, 'P') 20 | if (undefined == msgBox[sendToId] || null == msgBox[sendToId]) { 21 | msgBox[sendToId] = [m] //直接添加会导致vue无法监控,但 实际上这行在流程上不会被运行, 22 | } else { 23 | msgBox[sendToId].push(m) 24 | } 25 | 26 | 27 | var msgWithHandle = new MsgHandle(p, (obj) => { 28 | var data = obj.resp.data 29 | if (data.success != 'SUCCESS') { 30 | Notify({ 31 | type: 'danger', 32 | message: '发送失败:' + data.reason, 33 | duration: 2000, 34 | }); 35 | m.status = 'F' 36 | } else { 37 | m.status = 'S' 38 | } 39 | }); 40 | send(msgWithHandle) 41 | } 42 | 43 | 44 | 45 | //接收 46 | function receiveMsg(obj) { 47 | 48 | var data = obj.data; 49 | var snedFromID = data.fromId 50 | var m = new Msg('receive', data.msg, 'S') 51 | //添加到msgBox 52 | if (undefined == msgBox[snedFromID] || null == msgBox[snedFromID]) { 53 | msgBox[snedFromID] = [m] //如果chat还没渲染,那么在这添加,等再进入时,是可以监控到的 54 | } else { 55 | msgBox[snedFromID].push(m) 56 | } 57 | //添加到friend 58 | var user = friendsList.list.find((v) => { return v.userId === snedFromID }) 59 | user.infoCount++; 60 | 61 | //如果在聊天界面且对方是这个消息的发送人,则不提示 62 | if (router.app._route.name != 'Chat' || router.app._route.params.sendToId!=snedFromID) { 63 | Notify({ 64 | type: 'success', 65 | message: '新消息,' + data.fromName + ':' + data.msg, 66 | duration: 2000, 67 | onClick: function () { 68 | router.push("/Chat/" + snedFromID + "/" + data.fromName); 69 | } 70 | }); 71 | } 72 | 73 | } 74 | 75 | //注册接收信息的handle 76 | registerHandle(commands.get('MESSAGE_NOTIFY'), receiveMsg) 77 | 78 | 79 | class Msg { 80 | constructor(direct, msg, status) { 81 | this.direct = direct, 82 | this.status = status, 83 | this.msg = msg, 84 | this.time = new Date().Format("hh:mm:ss-S") 85 | } 86 | } 87 | 88 | //结构为 89 | var msgBox = {} 90 | 91 | 92 | export { msgBox, sendMsg } 93 | 94 | 95 | -------------------------------------------------------------------------------- /front/im_ui/src/assets/pk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.wks 8 | netty_im 9 | 1.0 10 | 11 | 12 | 13 | 14 | org.apache.maven.plugins 15 | maven-compiler-plugin 16 | 17 | 1.8 18 | 1.8 19 | 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-shade-plugin 24 | 2.4.1 25 | 26 | 27 | package 28 | 29 | shade 30 | 31 | 32 | 33 | 34 | com.wks.wsIm.server.WebSocketServer 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | io.netty 48 | netty-all 49 | 4.1.42.Final 50 | 51 | 52 | 53 | org.projectlombok 54 | lombok 55 | 1.18.2 56 | 57 | 58 | 59 | 60 | com.alibaba 61 | fastjson 62 | 1.2.54 63 | 64 | 65 | 66 | 67 | com.google.guava 68 | guava 69 | 28.2-jre 70 | 71 | 72 | 73 | 74 | ch.qos.logback 75 | logback-classic 76 | 1.2.3 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/biz/SendMessageService.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.biz; 2 | 3 | 4 | import com.wks.wsIm.domain.Packet; 5 | import com.wks.wsIm.domain.req.SendMsg; 6 | import com.wks.wsIm.domain.resp.ReceiveNotify; 7 | import com.wks.wsIm.domain.resp.SendResp; 8 | import io.netty.channel.Channel; 9 | import io.netty.channel.ChannelFuture; 10 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 11 | 12 | import static com.wks.wsIm.biz.LoginService.SESSION; 13 | import static com.wks.wsIm.biz.UserService.GOD_ID; 14 | import static com.wks.wsIm.biz.WriteService.send; 15 | import static com.wks.wsIm.domain.Commands.MESSAGE_NOTIFY; 16 | import static com.wks.wsIm.domain.Commands.SEND_MSSAGE; 17 | import static com.wks.wsIm.server.WebSocketFrameHandler.serializer; 18 | 19 | @Command(SEND_MSSAGE) 20 | public class SendMessageService extends BaseService { 21 | @Override 22 | Void process(MsgContext context, SendMsg sendMsg) { 23 | 24 | 25 | UserInfo to = UserService.getUserPool().get(sendMsg.getSendToId()); 26 | Channel from = context.getChannel(); 27 | 28 | //检查是否在线 29 | if (to == null) { 30 | SendResp data = new SendResp("FAIL", "对方不在线"); 31 | Packet p = new Packet(SEND_MSSAGE, context.getTraceId(), data); 32 | send(from, p); 33 | return null; 34 | } 35 | //检查是否添加了好友 36 | if (!from.attr(SESSION).get().getUser().getFriends().contains(to)) { 37 | SendResp data = new SendResp("FAIL", "请先添加好友"); 38 | Packet p = new Packet(SEND_MSSAGE, context.getTraceId(), data); 39 | send(from, p); 40 | return null; 41 | } 42 | 43 | //留言反馈 44 | if(to.getUserId().equals(GOD_ID)){ 45 | Packet p = new Packet(MESSAGE_NOTIFY, null, new ReceiveNotify(GOD_ID, 46 | to.getUserName(), 47 | "好的")); 48 | send(from, p); 49 | } 50 | 51 | 52 | //发送 53 | Packet p = new Packet(MESSAGE_NOTIFY, null, new ReceiveNotify(sendMsg.getSnedFromID(), 54 | UserService.getUserInfo(sendMsg.getSnedFromID()).getUserName(), 55 | sendMsg.getMsg())); 56 | transferMsg(from, to.getChannel(), p, 3, context.getTraceId()); 57 | 58 | return null; 59 | } 60 | 61 | private void transferMsg(Channel from, Channel to, Packet sendMsg, int reTry, String traceId) { 62 | 63 | if (reTry < 1) { 64 | 65 | SendResp data = new SendResp("FAIL", "发送失败"); 66 | Packet p = new Packet(SEND_MSSAGE, traceId, data); 67 | send(from, p); 68 | } 69 | 70 | 71 | send(to, sendMsg).addListener(future -> { 72 | if (future.isSuccess()) { 73 | SendResp data = new SendResp("SUCCESS", "SUCCESS"); 74 | Packet p = new Packet(SEND_MSSAGE, traceId, data); 75 | send(from, p); 76 | 77 | } else { 78 | transferMsg(from, to, sendMsg, reTry - 1, traceId); 79 | } 80 | }); 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/wks/netty_test/FirstClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.wks.netty_test; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | 7 | import java.nio.charset.Charset; 8 | import java.util.Date; 9 | 10 | public class FirstClientHandler extends ChannelInboundHandlerAdapter { 11 | @Override 12 | public void channelActive(ChannelHandlerContext ctx) { 13 | System.out.println(new Date() + ": 客户端写出数据"); 14 | 15 | // 1. 获取数据 16 | ByteBuf buffer = getByteBuf(ctx); 17 | 18 | // 2. 写数据 19 | ctx.channel().writeAndFlush(buffer); 20 | 21 | 22 | buffer.capacity(); 23 | 24 | //如果发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常 25 | buffer.maxCapacity(); 26 | 27 | //writerIndex - readerIndex 当前刻度的字节数 28 | buffer.readableBytes(); 29 | 30 | //当前可写的字节数,单如果容量达到capacity ,那么就拓展,直至到达maxCapacity 31 | buffer.writableBytes(); 32 | 33 | //获得和设置读指针 34 | buffer.readerIndex(); 35 | buffer.readerIndex(1); 36 | 37 | //获得和设置写指针 38 | buffer.writerIndex(); 39 | buffer.writerIndex(1); 40 | 41 | //标记 read指针 中间可以通过writerIndex 指定 read 指针 42 | buffer.markReaderIndex(); 43 | buffer.resetReaderIndex(); 44 | 45 | //读写API 46 | buffer.writeBytes(new byte[2]); 47 | buffer.readBytes(new byte[2]); 48 | 49 | //get set 方法不能修改read write 的指针 50 | buffer.getByte(1); 51 | buffer.setByte(1,1); 52 | 53 | //由于 Netty 使用了堆外内存,而堆外内存是不被 jvm 直接管理的,也就是说申请到的内存无法被垃圾回收器直接回收,所以需要我们手动回收。有点类似于c语言里面,申请到的内存必须手工释放,否则会造成内存泄漏。 54 | //Netty 的 ByteBuf 是通过引用计数的方式管理的,如果一个 ByteBuf 没有地方被引用到,需要回收底层内存。默认情况下,当创建完一个 ByteBuf,它的引用为1,然后每次调用 retain() 方法, 它的引用就加一, release() 方法原理是将引用计数减一,减完之后如果发现引用计数为0,则直接回收 ByteBuf 底层的内存。 55 | buffer.retain(); 56 | buffer.release(); 57 | 58 | // slice 和 duplicate 和buffer 使用同一个引用,但是指针不同,等于多了一套指针组合 59 | //slice() 只截取从 readerIndex 到 writerIndex 之间的数据,它返回的 ByteBuf 的最大容量被限制到 原始 ByteBuf 的 readableBytes(), 而 duplicate() 是把整个 ByteBuf 都与原始的 ByteBuf 共享 60 | // copy() 会直接从原始的 ByteBuf 中拷贝所有的信息,包括读写指针以及底层对应的数据,因此,往 copy() 返回的 ByteBuf 中写数据不会影响到原始的 ByteBuf 61 | //slice() 和 duplicate() 不会改变 ByteBuf 的引用计数,所以原始的 ByteBuf 调用 release() 之后发现引用计数为零,就开始释放内存,调用这两个方法返回的 ByteBuf 也会被释放,这个时候如果再对它们进行读写,就会报错。因此,我们可以通过调用一次 retain() 方法 来增加引用,表示它们对应的底层的内存多了一次引用,引用计数为2,在释放内存的时候,需要调用两次 release() 方法,将引用计数降到零,才会释放内存 62 | buffer.slice(); 63 | buffer.duplicate(); 64 | buffer.copy(); 65 | 66 | //slice 或duplicate的同时增加引用计数 67 | buffer.retainedSlice(); 68 | buffer.retainedDuplicate(); 69 | 70 | 71 | } 72 | 73 | private ByteBuf getByteBuf(ChannelHandlerContext ctx) { 74 | // 1. 获取二进制抽象 ByteBuf 75 | ByteBuf buffer = ctx.alloc().buffer(); 76 | 77 | // 2. 准备数据,指定字符串的字符集为 utf-8 78 | byte[] bytes = "你好,闪电侠!".getBytes(Charset.forName("utf-8")); 79 | 80 | // 3. 填充数据到 ByteBuf 81 | buffer.writeBytes(bytes); 82 | 83 | return buffer; 84 | } 85 | } 86 | 87 | /* 88 | * 89 | * 90 | * 91 | */ -------------------------------------------------------------------------------- /front/im_ui/src/views/Chat.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/server/server.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.server; 2 | 3 | import com.wks.customProtocol.codec.PacketDecoder; 4 | import com.wks.customProtocol.codec.PacketEncoder; 5 | import com.wks.customProtocol.codec.Spliter; 6 | import io.netty.bootstrap.ServerBootstrap; 7 | import io.netty.channel.ChannelInitializer; 8 | import io.netty.channel.ChannelOption; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.nio.NioServerSocketChannel; 11 | import io.netty.channel.socket.nio.NioSocketChannel; 12 | import io.netty.util.AttributeKey; 13 | import io.netty.util.concurrent.Future; 14 | import io.netty.util.concurrent.GenericFutureListener; 15 | 16 | public class server { 17 | 18 | public static void main(String[] args) { 19 | ServerBootstrap serverBootstrap = new ServerBootstrap(); 20 | 21 | NioEventLoopGroup boss = new NioEventLoopGroup(1); 22 | NioEventLoopGroup worker = new NioEventLoopGroup(2); 23 | serverBootstrap 24 | .group(boss, worker) 25 | .attr(AttributeKey.newInstance("serverName"), "nettyServer") 26 | .childAttr(AttributeKey.newInstance("clientKey"), "clientValue") 27 | .channel(NioServerSocketChannel.class) 28 | .handler(new ChannelInitializer() { 29 | @Override 30 | protected void initChannel(NioServerSocketChannel ch) throws Exception { 31 | } 32 | }) 33 | 34 | .childOption(ChannelOption.SO_KEEPALIVE, true) 35 | .childOption(ChannelOption.TCP_NODELAY, true) 36 | .option(ChannelOption.SO_BACKLOG, 1024) 37 | .childHandler(new ChannelInitializer() { 38 | @Override 39 | protected void initChannel(NioSocketChannel ch) { 40 | 41 | ch.pipeline().addLast(new LifeCyCleTestHandler()); 42 | 43 | //检验模数 和拆包 44 | ch.pipeline().addLast(new Spliter()); 45 | 46 | //解码 47 | ch.pipeline().addLast(new PacketDecoder()); 48 | //登录 49 | ch.pipeline().addLast(new LoginHandler()); 50 | //检验登录 51 | ch.pipeline().addLast(new AuthHandler()); 52 | ch.pipeline().addLast(new MessageHandler()); 53 | 54 | ch.pipeline().addLast(new PacketEncoder()); 55 | } 56 | }); 57 | bind(serverBootstrap, 8080); 58 | System.out.println("初始化完成"); 59 | } 60 | 61 | 62 | private static void bind(final ServerBootstrap serverBootstrap, final int port) { 63 | serverBootstrap.bind(port).addListener(new GenericFutureListener>() { 64 | @Override 65 | public void operationComplete(Future future) { 66 | if (future.isSuccess()) { 67 | System.out.println("端口[" + port + "]绑定成功!"); 68 | } else { 69 | System.err.println("端口[" + port + "]绑定失败!"); 70 | bind(serverBootstrap, port + 1); 71 | } 72 | } 73 | }); 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/packet/Packet.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.packet; 2 | 3 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 4 | import io.netty.buffer.ByteBuf; 5 | import lombok.Data; 6 | 7 | 8 | /** 9 | * 数据包类,包含 packet 与buffer之间的转化,但不应该包含其他操作如 序列化数据等 10 | */ 11 | @Data 12 | public class Packet implements PacketAnalysis { 13 | public final static Integer MAGIC_NUM = 1; 14 | /** 15 | * 魔法数 16 | */ 17 | public Integer magicNum; 18 | 19 | /** 20 | * 协议版本 21 | */ 22 | public byte version; 23 | 24 | /** 25 | * 指令 26 | */ 27 | public byte command; 28 | 29 | /** 30 | * 序列化算法 31 | */ 32 | public byte serializerAlgorithm; 33 | 34 | /** 35 | * 数据长度 36 | */ 37 | public Integer dataLength; 38 | 39 | /** 40 | * 数据 41 | */ 42 | public byte[] data; 43 | 44 | //不常变动的数据部分 45 | { 46 | this.magicNum = 1; 47 | this.version = 1; 48 | this.serializerAlgorithm = SerializerAlgorithm.JSON; 49 | } 50 | 51 | public Packet() { 52 | 53 | } 54 | 55 | public Packet(byte command, byte[] data) { 56 | this.command = command; 57 | this.dataLength = data.length; 58 | this.data = data; 59 | } 60 | 61 | public Packet(byte ser, byte command, byte[] data) { 62 | this.serializerAlgorithm = ser; 63 | this.command = command; 64 | this.data = data; 65 | } 66 | 67 | 68 | @Override 69 | public ByteBuf encode(ByteBuf byteBuf) { 70 | 71 | // 3. 实际编码过程 72 | byteBuf.writeInt(this.getMagicNum()); 73 | byteBuf.writeByte(this.getVersion()); 74 | byteBuf.writeByte(this.getSerializerAlgorithm()); 75 | byteBuf.writeByte(this.getCommand()); 76 | byteBuf.writeInt(this.getDataLength()); 77 | byteBuf.writeBytes(this.getData()); 78 | 79 | return byteBuf; 80 | } 81 | 82 | /** 83 | * 唯一需要考虑到 packet中各部分长度的方法,所以如果需要对长度进行修改,那么此方法需要覆盖 84 | * 85 | * @param buf 86 | * @return 87 | */ 88 | @Override 89 | public Packet decode(ByteBuf buf) { 90 | 91 | Integer magicNum = buf.readInt(); 92 | this.setMagicNum(magicNum); 93 | 94 | byte version = buf.readByte(); 95 | this.setVersion(version); 96 | 97 | byte serializerAlgorithm = buf.readByte(); 98 | this.setSerializerAlgorithm(serializerAlgorithm); 99 | 100 | byte command = buf.readByte(); 101 | this.setCommand(command); 102 | 103 | Integer dataLength = buf.readInt(); 104 | this.setDataLength(dataLength); 105 | 106 | byte[] data = new byte[dataLength]; 107 | buf.readBytes(data); 108 | this.setData(data); 109 | 110 | return this; 111 | } 112 | 113 | /** 114 | * 作为packet 与command 之间的对应逻辑方法 115 | * 116 | * @return Command 里的byte 值 117 | */ 118 | @Override 119 | public byte getCommandOperation() { 120 | return this.command; 121 | } 122 | 123 | /** 124 | * 作为packet 与SerializerAlgorithm 的对应逻辑方法 125 | * 126 | * @return SerializerAlgotithm 里的byte 值 127 | */ 128 | @Override 129 | public byte getSerializerAlgorithm() { 130 | return this.serializerAlgorithm; 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /src/test/java/Test/TestVo2.java: -------------------------------------------------------------------------------- 1 | package Test; 2 | 3 | import sun.misc.Unsafe; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class TestVo2 { 9 | 10 | public static Person p = new Person("", 1); 11 | public static volatile Person[] arr = new Person[1]; 12 | 13 | public static void main(String[] args) throws Exception { 14 | new Thread(new Thread() { 15 | @Override 16 | public void run() { 17 | //线程1 18 | try { 19 | TimeUnit.MILLISECONDS.sleep(3000); 20 | arr[0] = new Person("xixi",12); 21 | //arr=arr; 22 | //Person p1 = (Person) arr[0]; 23 | //p.setAge(12); 24 | //p1.setAge(12); 25 | //System.out.println("end 1 "); 26 | int count = 1; 27 | while (true) { 28 | count++; 29 | } 30 | } catch (InterruptedException e) { 31 | e.printStackTrace(); 32 | } 33 | } 34 | }).start(); 35 | new Thread(new Thread() { 36 | @Override 37 | public void run() { 38 | 39 | 40 | //线程2 41 | //Object[] tab=arr; 42 | while (true) { 43 | Person p= (Person) arr[0]; 44 | //Person p = (Person) unsafe.getObjectVolatile(tab,(0L << ASHIFT) + ABASE) ; 45 | 46 | if (p != null && p.getAge() == 12) { 47 | break; 48 | } 49 | //System.out.println("o"); 50 | 51 | } 52 | System.out.println("Jump out of the loop!"); 53 | } 54 | }).start(); 55 | 56 | } 57 | 58 | private static Unsafe reflectGetUnsafe() { 59 | try { 60 | Field field = Unsafe.class.getDeclaredField("theUnsafe"); 61 | field.setAccessible(true); 62 | return (Unsafe) field.get(null); 63 | } catch (Exception e) { 64 | e.printStackTrace(); 65 | return null; 66 | } 67 | } 68 | 69 | private static final long ABASE; 70 | private static final int ASHIFT; 71 | static Unsafe unsafe=reflectGetUnsafe(); 72 | static { 73 | try { 74 | 75 | Class ak = Person[].class; 76 | ABASE = unsafe.arrayBaseOffset(ak); 77 | int scale = unsafe.arrayIndexScale(ak); 78 | if ((scale & (scale - 1)) != 0) 79 | throw new Error("data type scale not a power of two"); 80 | ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); 81 | } catch (Exception e) { 82 | throw new Error(e); 83 | } 84 | } 85 | } 86 | 87 | class Person { 88 | String name; 89 | int age; 90 | 91 | public Person(String name, int age) { 92 | this.name = name; 93 | this.age = age; 94 | } 95 | 96 | public String getName() { 97 | return name; 98 | } 99 | 100 | public void setName(String name) { 101 | this.name = name; 102 | } 103 | 104 | public int getAge() { 105 | return age; 106 | } 107 | 108 | public void setAge(int age) { 109 | this.age = age; 110 | } 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/test/java/com/wks/netty_test/NettyServer.java: -------------------------------------------------------------------------------- 1 | package com.wks.netty_test; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.*; 5 | import io.netty.channel.nio.NioEventLoopGroup; 6 | import io.netty.channel.socket.nio.NioServerSocketChannel; 7 | import io.netty.channel.socket.nio.NioSocketChannel; 8 | import io.netty.handler.codec.string.StringDecoder; 9 | import io.netty.util.AttributeKey; 10 | import io.netty.util.concurrent.Future; 11 | import io.netty.util.concurrent.GenericFutureListener; 12 | 13 | public class NettyServer { 14 | public static void main(String[] args) { 15 | ServerBootstrap serverBootstrap = new ServerBootstrap(); 16 | 17 | NioEventLoopGroup boss = new NioEventLoopGroup(1); 18 | NioEventLoopGroup worker = new NioEventLoopGroup(2); 19 | serverBootstrap 20 | .group(boss, worker) 21 | .attr(AttributeKey.newInstance("serverName"), "nettyServer") 22 | .childAttr(AttributeKey.newInstance("clientKey"), "clientValue") 23 | .channel(NioServerSocketChannel.class) 24 | .handler(new ChannelInitializer() { 25 | protected void initChannel(NioServerSocketChannel ch) throws Exception { 26 | /** 27 | * childHandler()用于指定处理新连接数据的读写处理逻辑,handler()用于指定在服务端启动过程中的一些逻辑,通常情况下呢,我们用不着这个方法。 28 | */ 29 | } 30 | }) 31 | 32 | .childOption(ChannelOption.SO_KEEPALIVE, true) 33 | .childOption(ChannelOption.TCP_NODELAY, true) 34 | .option(ChannelOption.SO_BACKLOG, 1024) 35 | .childHandler(new ChannelInitializer() { 36 | protected void initChannel(NioSocketChannel ch) { 37 | //ch.pipeline().addLast(new StringDecoder()); 38 | /*ch.pipeline().addLast(new SimpleChannelInboundHandler() { // 4 39 | @Override 40 | protected void channelRead0(ChannelHandlerContext ctx, String msg) { 41 | System.out.println(msg); 42 | } 43 | });*/ 44 | 45 | ch.pipeline().addLast(new FirstServerHandler()); 46 | } 47 | }); 48 | bind(serverBootstrap, 8080); 49 | } 50 | 51 | 52 | private static void bind(final ServerBootstrap serverBootstrap, final int port) { 53 | serverBootstrap.bind(port).addListener(new GenericFutureListener>() { 54 | public void operationComplete(Future future) { 55 | if (future.isSuccess()) { 56 | System.out.println("端口[" + port + "]绑定成功!"); 57 | } else { 58 | System.err.println("端口[" + port + "]绑定失败!"); 59 | bind(serverBootstrap, port + 1); 60 | } 61 | } 62 | }); 63 | } 64 | } 65 | 66 | 67 | /** 68 | * 此处指定的处理程序将始终由新接受的通道计算。 ChannelInitializer是一个特殊的处理程序,用于帮助用户配置新的通道。 69 | * 很可能要通过添加一些处理程序(例如DiscardServerHandler)来配置新通道的ChannelPipeline来实现您的网络应用程序。 70 | * 随着应用程序变得复杂,可能会向管道中添加更多处理程序,并最终将此匿名类提取到顶级类中。 71 | **/ 72 | 73 | 74 | /* attr()方法可以给服务端的 channel,也就是NioServerSocketChannel指定一些自定义属性, 75 | 然后我们可以通过channel.attr()取出这个属性,比如,上面的代码我们指定我们服务端channel的一个serverName属性,属性值为nettyServer, 76 | 其实说白了就是给NioServerSocketChannel维护一个map而已,通常情况下,我们也用不上这个方法。*/ 77 | 78 | /* childAttr可以给每一条连接指定自定义属性,然后后续我们可以通过channel.attr()取出该属性。*/ 79 | 80 | /* childOption()可以给每条连接设置一些TCP底层相关的属性,比如上面,我们设置了两种TCP属性,其中 */ 81 | 82 | 83 | /* 可以给服务端channel设置一些属性 */ -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/WebSocketServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.wks.wsIm.server; 17 | 18 | import com.wks.wsIm.Util.GodChannel; 19 | import io.netty.bootstrap.ServerBootstrap; 20 | import io.netty.channel.Channel; 21 | import io.netty.channel.EventLoopGroup; 22 | import io.netty.channel.nio.NioEventLoopGroup; 23 | import io.netty.channel.socket.nio.NioServerSocketChannel; 24 | import io.netty.handler.logging.LogLevel; 25 | import io.netty.handler.logging.LoggingHandler; 26 | import io.netty.handler.ssl.SslContext; 27 | import io.netty.handler.ssl.SslContextBuilder; 28 | import io.netty.handler.ssl.util.SelfSignedCertificate; 29 | 30 | /** 31 | * An HTTP server which serves Web Socket requests at: 32 | * 33 | * http://localhost:8080/websocket 34 | * 35 | * Open your browser at http://localhost:8080/, then the demo page will be loaded 36 | * and a Web Socket connection will be made automatically. 37 | * 38 | * This server illustrates support for the different web socket specification versions and will work with: 39 | * 40 | *
    41 | *
  • Safari 5+ (draft-ietf-hybi-thewebsocketprotocol-00) 42 | *
  • Chrome 6-13 (draft-ietf-hybi-thewebsocketprotocol-00) 43 | *
  • Chrome 14+ (draft-ietf-hybi-thewebsocketprotocol-10) 44 | *
  • Chrome 16+ (RFC 6455 aka draft-ietf-hybi-thewebsocketprotocol-17) 45 | *
  • Firefox 7+ (draft-ietf-hybi-thewebsocketprotocol-10) 46 | *
  • Firefox 11+ (RFC 6455 aka draft-ietf-hybi-thewebsocketprotocol-17) 47 | *
48 | */ 49 | public final class WebSocketServer { 50 | 51 | static final boolean SSL =System.getProperty("ssl") != null ; 52 | static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "80")); 53 | 54 | public static void main(String[] args) throws Exception { 55 | // Configure SSL. 56 | final SslContext sslCtx; 57 | if (SSL) { 58 | SelfSignedCertificate ssc = new SelfSignedCertificate(); 59 | sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); 60 | } else { 61 | sslCtx = null; 62 | } 63 | 64 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); 65 | EventLoopGroup workerGroup = new NioEventLoopGroup(); 66 | 67 | //为反馈Channel的eventLoop设置 68 | GodChannel.eventExecutors=workerGroup.next(); 69 | 70 | try { 71 | ServerBootstrap b = new ServerBootstrap(); 72 | b.group(bossGroup, workerGroup) 73 | .channel(NioServerSocketChannel.class) 74 | .handler(new LoggingHandler(LogLevel.INFO)) 75 | .childHandler(new WebSocketServerInitializer(sslCtx)); 76 | 77 | Channel ch = b.bind(PORT).sync().channel(); 78 | 79 | System.out.println("Open your web browser and navigate to " + 80 | (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); 81 | 82 | ch.closeFuture().sync(); 83 | } finally { 84 | bossGroup.shutdownGracefully(); 85 | workerGroup.shutdownGracefully(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/wks/newIO/MyNioServer.java: -------------------------------------------------------------------------------- 1 | package com.wks.newIO; 2 | 3 | 4 | 5 | import java.io.IOException; 6 | import java.net.InetSocketAddress; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.*; 9 | import java.util.Iterator; 10 | import java.util.Set; 11 | 12 | 13 | public class MyNioServer { 14 | private Selector selector; //创建一个选择器 15 | private final static int port = 8686; 16 | private final static int BUF_SIZE = 10240; 17 | 18 | private void initServer() throws IOException { 19 | //创建通道管理器对象selector 20 | this.selector=Selector.open(); 21 | 22 | //创建一个通道对象channel 23 | ServerSocketChannel channel = ServerSocketChannel.open(); 24 | channel.configureBlocking(false); //将通道设置为非阻塞 25 | channel.socket().bind(new InetSocketAddress(port)); //将通道绑定在8686端口 26 | 27 | //将上述的通道管理器和通道绑定,并为该通道注册OP_ACCEPT事件 28 | //注册事件后,当该事件到达时,selector.select()会返回(一个key),如果该事件没到达selector.select()会一直阻塞 29 | SelectionKey selectionKey = channel.register(selector,SelectionKey.OP_ACCEPT); 30 | 31 | while (true){ //轮询 32 | selector.select(); //这是一个阻塞方法,一直等待直到有数据可读,返回值是key的数量(可以有多个) 33 | Set keys = selector.selectedKeys(); //如果channel有数据了,将生成的key访入keys集合中 34 | Iterator iterator = keys.iterator(); //得到这个keys集合的迭代器 35 | while (iterator.hasNext()){ //使用迭代器遍历集合 36 | SelectionKey key = (SelectionKey) iterator.next(); //得到集合中的一个key实例 37 | iterator.remove(); //拿到当前key实例之后记得在迭代器中将这个元素删除,非常重要,否则会出错 38 | if (key.isAcceptable()){ //判断当前key所代表的channel是否在Acceptable状态,如果是就进行接收 39 | doAccept(key); 40 | }else if (key.isReadable()){ 41 | doRead(key); 42 | }else if (key.isWritable() && key.isValid()){ 43 | doWrite(key); 44 | }else if (key.isConnectable()){ 45 | System.out.println("连接成功!"); 46 | } 47 | } 48 | } 49 | } 50 | 51 | public void doAccept(SelectionKey key) throws IOException { 52 | ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); 53 | System.out.println("ServerSocketChannel正在循环监听"); 54 | SocketChannel clientChannel = serverChannel.accept(); 55 | clientChannel.configureBlocking(false); 56 | SelectionKey key1= clientChannel.register(key.selector(),SelectionKey.OP_READ); 57 | System.out.println(key1); 58 | } 59 | 60 | public void doRead(SelectionKey key) throws IOException { 61 | SocketChannel clientChannel = (SocketChannel) key.channel(); 62 | ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE); 63 | long bytesRead = clientChannel.read(byteBuffer); 64 | while (bytesRead>0){ 65 | byteBuffer.flip(); 66 | byte[] data = byteBuffer.array(); 67 | String info = new String(data).trim(); 68 | System.out.println("从客户端发送过来的消息是:"+info); 69 | byteBuffer.clear(); 70 | bytesRead = clientChannel.read(byteBuffer); 71 | } 72 | if (bytesRead==-1){ 73 | clientChannel.close(); 74 | } 75 | } 76 | 77 | public void doWrite(SelectionKey key) throws IOException { 78 | ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE); 79 | byteBuffer.flip(); 80 | SocketChannel clientChannel = (SocketChannel) key.channel(); 81 | while (byteBuffer.hasRemaining()){ 82 | clientChannel.write(byteBuffer); 83 | } 84 | byteBuffer.compact(); 85 | } 86 | 87 | public static void main(String[] args) throws IOException { 88 | MyNioServer myNioServer = new MyNioServer(); 89 | myNioServer.initServer(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/WebSocketServerIndexPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.wks.wsIm.server; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.util.CharsetUtil; 21 | 22 | /** 23 | * Generates the demo HTML page which is served at http://localhost:8080/ 24 | */ 25 | public final class WebSocketServerIndexPage { 26 | 27 | private static final String NEWLINE = "\r\n"; 28 | 29 | public static ByteBuf getContent(String webSocketLocation) { 30 | return Unpooled.copiedBuffer( 31 | "Web Socket Test" + NEWLINE + 32 | "" + NEWLINE + 33 | "" + NEWLINE + 65 | "
" + NEWLINE + 66 | "" + 67 | "" + NEWLINE + 69 | "

Output

" + NEWLINE + 70 | "" + NEWLINE + 71 | "
" + NEWLINE + 72 | "" + NEWLINE + 73 | "" + NEWLINE, CharsetUtil.US_ASCII); 74 | } 75 | 76 | private WebSocketServerIndexPage() { 77 | // Unused 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/WebSocketFrameHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.wks.wsIm.server; 17 | 18 | import com.wks.wsIm.biz.MsgContext; 19 | import com.wks.wsIm.biz.OffLineService; 20 | import com.wks.wsIm.biz.Router; 21 | import com.wks.wsIm.domain.Packet; 22 | import com.wks.wsIm.domain.resp.ErrorResp; 23 | import com.wks.wsIm.serializer.JSONSerializer; 24 | import io.netty.channel.Channel; 25 | import io.netty.channel.ChannelHandlerContext; 26 | import io.netty.channel.SimpleChannelInboundHandler; 27 | import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; 28 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 29 | import lombok.extern.slf4j.Slf4j; 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | import static com.wks.wsIm.biz.WriteService.send; 36 | import static com.wks.wsIm.domain.Commands.ERROR; 37 | 38 | /** 39 | * 处理 WebSocketFrame ,其中控制帧已经在上一个Handler 处理了,所以只需要处理Text 和binary 40 | * 所做的事情: 1.反序列化为Packet,2.拼接分帧的消息 41 | */ 42 | @Slf4j 43 | public class WebSocketFrameHandler extends SimpleChannelInboundHandler { 44 | 45 | List messages = new ArrayList<>(); 46 | 47 | Router router = new Router(); 48 | 49 | public static JSONSerializer serializer = new JSONSerializer(); 50 | 51 | private static int HEARTBEAT_INTERVAL = 10; 52 | 53 | @Override 54 | protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception { 55 | // ping and pong frames already handled 56 | 57 | Packet result = null; 58 | Packet req = null; 59 | String msg = null; 60 | try { 61 | messages.add((TextWebSocketFrame) frame); 62 | if (frame.isFinalFragment()) { 63 | msg = joinMsg(messages); 64 | messages.clear(); 65 | req = serializer.deserialize(msg); 66 | result = router.router(generateContext(ctx.channel(), req.getTraceId()), req); 67 | if (result != null) { 68 | send(ctx.channel(), result); 69 | } 70 | } 71 | } catch (Exception e) { 72 | log.error("handler 错误 " + msg, e); 73 | send(ctx.channel(), new Packet(ERROR, req.getTraceId(), new ErrorResp("handle错误"))); 74 | } 75 | } 76 | 77 | //如果信息被分帧,需要拼接 78 | public String joinMsg(List messages) { 79 | StringBuilder builder = new StringBuilder(); 80 | messages.stream().forEach((c) -> { 81 | builder.append(c.text()); 82 | }); 83 | return builder.toString(); 84 | } 85 | 86 | MsgContext generateContext(Channel channel, String traceId) { 87 | MsgContext context = new MsgContext(); 88 | context.setChannel(channel); 89 | context.setTraceId(traceId); 90 | return context; 91 | } 92 | 93 | //下线 94 | @Override 95 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 96 | OffLineService.process(ctx); 97 | } 98 | 99 | @Override 100 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 101 | scheduleSendHeartBeat(ctx); 102 | 103 | super.channelActive(ctx); 104 | } 105 | 106 | /** 107 | * 发送心跳 108 | * 109 | * @param ctx 110 | * @throws Exception 111 | */ 112 | private void scheduleSendHeartBeat(ChannelHandlerContext ctx) { 113 | ctx.executor().schedule(() -> { 114 | 115 | if (ctx.channel().isActive()) { 116 | ctx.channel().writeAndFlush(new PingWebSocketFrame()); 117 | scheduleSendHeartBeat(ctx); 118 | } 119 | 120 | }, HEARTBEAT_INTERVAL, TimeUnit.MINUTES); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/WebSocketIndexPageHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.wks.wsIm.server; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.ByteBufUtil; 20 | import io.netty.channel.*; 21 | import io.netty.handler.codec.http.*; 22 | import io.netty.handler.ssl.SslHandler; 23 | 24 | import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; 25 | import static io.netty.handler.codec.http.HttpMethod.GET; 26 | import static io.netty.handler.codec.http.HttpResponseStatus.*; 27 | 28 | /** 29 | * Outputs index page content. 30 | */ 31 | public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler { 32 | 33 | private final String websocketPath; 34 | 35 | public WebSocketIndexPageHandler(String websocketPath) { 36 | this.websocketPath = websocketPath; 37 | } 38 | 39 | @Override 40 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { 41 | // Handle a bad request. 42 | if (!req.decoderResult().isSuccess()) { 43 | sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST, 44 | ctx.alloc().buffer(0))); 45 | return; 46 | } 47 | 48 | // Allow only GET methods. 49 | if (!GET.equals(req.method())) { 50 | sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN, 51 | ctx.alloc().buffer(0))); 52 | return; 53 | } 54 | 55 | // Send the index page 56 | if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) { 57 | String webSocketLocation = getWebSocketLocation(ctx.pipeline(), req, websocketPath); 58 | ByteBuf content = WebSocketServerIndexPage.getContent(webSocketLocation); 59 | FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content); 60 | 61 | res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); 62 | HttpUtil.setContentLength(res, content.readableBytes()); 63 | 64 | sendHttpResponse(ctx, req, res); 65 | } else { 66 | sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND, 67 | ctx.alloc().buffer(0))); 68 | } 69 | } 70 | 71 | @Override 72 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 73 | cause.printStackTrace(); 74 | ctx.close(); 75 | } 76 | 77 | private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { 78 | // Generate an error page if response getStatus code is not OK (200). 79 | HttpResponseStatus responseStatus = res.status(); 80 | if (responseStatus.code() != 200) { 81 | ByteBufUtil.writeUtf8(res.content(), responseStatus.toString()); 82 | HttpUtil.setContentLength(res, res.content().readableBytes()); 83 | } 84 | // Send the response and close the connection if necessary. 85 | boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200; 86 | HttpUtil.setKeepAlive(res, keepAlive); 87 | ChannelFuture future = ctx.writeAndFlush(res); 88 | if (!keepAlive) { 89 | future.addListener(ChannelFutureListener.CLOSE); 90 | } 91 | } 92 | 93 | private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) { 94 | String protocol = "ws"; 95 | if (cp.get(SslHandler.class) != null) { 96 | // SSL in use so use Secure WebSockets 97 | protocol = "wss"; 98 | } 99 | return protocol + "://" + req.headers().get(HttpHeaderNames.HOST) + path; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/wks/customProtocol/client/client.java: -------------------------------------------------------------------------------- 1 | package com.wks.customProtocol.client; 2 | 3 | import com.wks.customProtocol.Utils; 4 | import com.wks.customProtocol.codec.PacketDecoder; 5 | import com.wks.customProtocol.codec.PacketEncoder; 6 | import com.wks.customProtocol.codec.Spliter; 7 | import com.wks.customProtocol.packet.Command; 8 | import com.wks.customProtocol.packet.Packet; 9 | import com.wks.customProtocol.packet.data.LoginRequestData; 10 | import com.wks.customProtocol.packet.data.MessageRequestData; 11 | import com.wks.customProtocol.serializer.SerializerAlgorithm; 12 | import io.netty.bootstrap.Bootstrap; 13 | import io.netty.channel.Channel; 14 | import io.netty.channel.ChannelFuture; 15 | import io.netty.channel.ChannelInitializer; 16 | import io.netty.channel.ChannelOption; 17 | import io.netty.channel.nio.NioEventLoopGroup; 18 | import io.netty.channel.socket.nio.NioSocketChannel; 19 | import io.netty.util.AttributeKey; 20 | 21 | import java.util.Scanner; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | public class client { 25 | public static void main(String[] args) throws InterruptedException { 26 | Bootstrap bootstrap = new Bootstrap(); 27 | NioEventLoopGroup group = new NioEventLoopGroup(); 28 | 29 | bootstrap.group(group) 30 | .attr(AttributeKey.newInstance("clientName"), "nettyClient") 31 | .channel(NioSocketChannel.class) 32 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) 33 | .option(ChannelOption.SO_KEEPALIVE, true) 34 | .option(ChannelOption.TCP_NODELAY, true) 35 | .handler(new ChannelInitializer() { 36 | @Override 37 | protected void initChannel(Channel ch) { 38 | 39 | 40 | ch.pipeline().addLast(new Spliter()); 41 | 42 | ch.pipeline().addLast(new PacketDecoder()); 43 | ch.pipeline().addLast(new LoginHandler()); 44 | ch.pipeline().addLast(new MessageHandler()); 45 | 46 | ch.pipeline().addLast(new PacketEncoder()); 47 | } 48 | }); 49 | connect(bootstrap, "localhost", 8080, MAX_RETRY); 50 | } 51 | 52 | final static int MAX_RETRY = 5; 53 | 54 | private static void connect(Bootstrap bootstrap, String host, int port, int retry) { 55 | bootstrap.connect(host, port).addListener(future -> { 56 | if (future.isSuccess()) { 57 | System.out.println("连接成功!"); 58 | startConsoleThread(((ChannelFuture) future).channel()); 59 | } else if (retry == 0) { 60 | System.err.println("重试次数已用完,放弃连接!"); 61 | } else { 62 | // 第几次重连 63 | int order = (MAX_RETRY - retry) + 1; 64 | // 本次重连的间隔 65 | int delay = 1 << order; 66 | bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit 67 | .SECONDS); 68 | } 69 | }); 70 | } 71 | 72 | 73 | public static Object waitForLoginEnd = new Object(); 74 | 75 | private static void startConsoleThread(Channel channel) { 76 | System.out.println("开始监控用户输入"); 77 | Scanner sc = new Scanner(System.in); 78 | new Thread(() -> { 79 | while (!Thread.interrupted()) { 80 | if (!Utils.clientHasLogin(channel)) { 81 | System.out.print("输入userID userName Password 登录: "); 82 | 83 | String userId = sc.next(); 84 | String userName = sc.next(); 85 | String passWord = sc.next(); 86 | 87 | LoginRequestData data = new LoginRequestData(userId, userName, passWord); 88 | channel.writeAndFlush(data); 89 | 90 | //阻塞 91 | synchronized (waitForLoginEnd) { 92 | try { 93 | waitForLoginEnd.wait(); 94 | } catch (InterruptedException e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | System.out.println("登录成功,可以发送"); 99 | } else { 100 | String toUserId = sc.next(); 101 | String message = sc.next(); 102 | channel.writeAndFlush(new MessageRequestData(toUserId, message)); 103 | 104 | /*if(line.equals("exit")){ 105 | channel.close(); 106 | System.exit(0); 107 | }*/ 108 | } 109 | } 110 | }).start(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /front/im_ui/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 82 | 83 | 84 | 105 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/mime.types: -------------------------------------------------------------------------------- 1 | types { 2 | text/html html htm shtml 3 | text/css css 4 | text/xml xml 5 | image/gif gif 6 | image/jpeg jpeg jpg 7 | application/javascript js 8 | application/atom+xml atom 9 | application/rss+xml rss 10 | 11 | text/mathml mml 12 | text/plain txt 13 | text/vnd.sun.j2me.app-descriptor jad 14 | text/vnd.wap.wml wml 15 | text/x-component htc 16 | 17 | image/png png 18 | image/svg+xml svg svgz 19 | image/tiff tif tiff 20 | image/vnd.wap.wbmp wbmp 21 | image/webp webp 22 | image/x-icon ico 23 | image/x-jng jng 24 | image/x-ms-bmp bmp 25 | 26 | font/woff woff 27 | font/woff2 woff2 28 | 29 | application/java-archive jar war ear 30 | application/json json 31 | application/mac-binhex40 hqx 32 | application/msword doc 33 | application/pdf pdf 34 | application/postscript ps eps ai 35 | application/rtf rtf 36 | application/vnd.apple.mpegurl m3u8 37 | application/vnd.google-earth.kml+xml kml 38 | application/vnd.google-earth.kmz kmz 39 | application/vnd.ms-excel xls 40 | application/vnd.ms-fontobject eot 41 | application/vnd.ms-powerpoint ppt 42 | application/vnd.oasis.opendocument.graphics odg 43 | application/vnd.oasis.opendocument.presentation odp 44 | application/vnd.oasis.opendocument.spreadsheet ods 45 | application/vnd.oasis.opendocument.text odt 46 | application/vnd.openxmlformats-officedocument.presentationml.presentation 47 | pptx 48 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 49 | xlsx 50 | application/vnd.openxmlformats-officedocument.wordprocessingml.document 51 | docx 52 | application/vnd.wap.wmlc wmlc 53 | application/x-7z-compressed 7z 54 | application/x-cocoa cco 55 | application/x-java-archive-diff jardiff 56 | application/x-java-jnlp-file jnlp 57 | application/x-makeself run 58 | application/x-perl pl pm 59 | application/x-pilot prc pdb 60 | application/x-rar-compressed rar 61 | application/x-redhat-package-manager rpm 62 | application/x-sea sea 63 | application/x-shockwave-flash swf 64 | application/x-stuffit sit 65 | application/x-tcl tcl tk 66 | application/x-x509-ca-cert der pem crt 67 | application/x-xpinstall xpi 68 | application/xhtml+xml xhtml 69 | application/xspf+xml xspf 70 | application/zip zip 71 | 72 | application/octet-stream bin exe dll 73 | application/octet-stream deb 74 | application/octet-stream dmg 75 | application/octet-stream iso img 76 | application/octet-stream msi msp msm 77 | 78 | audio/midi mid midi kar 79 | audio/mpeg mp3 80 | audio/ogg ogg 81 | audio/x-m4a m4a 82 | audio/x-realaudio ra 83 | 84 | video/3gpp 3gpp 3gp 85 | video/mp2t ts 86 | video/mp4 mp4 87 | video/mpeg mpeg mpg 88 | video/quicktime mov 89 | video/webm webm 90 | video/x-flv flv 91 | video/x-m4v m4v 92 | video/x-mng mng 93 | video/x-ms-asf asx asf 94 | video/x-ms-wmv wmv 95 | video/x-msvideo avi 96 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/Util/GodChannel.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.Util; 2 | 3 | import io.netty.buffer.ByteBufAllocator; 4 | import io.netty.channel.*; 5 | import io.netty.util.Attribute; 6 | import io.netty.util.AttributeKey; 7 | import io.netty.util.concurrent.EventExecutor; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.lang.reflect.Constructor; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.net.SocketAddress; 13 | 14 | @Slf4j 15 | public class GodChannel implements Channel { 16 | 17 | public static EventExecutor eventExecutors; 18 | 19 | public static String prefix="用户反馈"; 20 | @Override 21 | public ChannelId id() { 22 | return null; 23 | } 24 | 25 | @Override 26 | public EventLoop eventLoop() { 27 | return (EventLoop) eventExecutors; 28 | } 29 | 30 | @Override 31 | public Channel parent() { 32 | return null; 33 | } 34 | 35 | @Override 36 | public ChannelConfig config() { 37 | return null; 38 | } 39 | 40 | @Override 41 | public boolean isOpen() { 42 | return false; 43 | } 44 | 45 | @Override 46 | public boolean isRegistered() { 47 | return false; 48 | } 49 | 50 | @Override 51 | public boolean isActive() { 52 | return true; 53 | } 54 | 55 | @Override 56 | public ChannelMetadata metadata() { 57 | return null; 58 | } 59 | 60 | @Override 61 | public SocketAddress localAddress() { 62 | return null; 63 | } 64 | 65 | @Override 66 | public SocketAddress remoteAddress() { 67 | return null; 68 | } 69 | 70 | @Override 71 | public ChannelFuture closeFuture() { 72 | return null; 73 | } 74 | 75 | @Override 76 | public boolean isWritable() { 77 | return true; 78 | } 79 | 80 | @Override 81 | public long bytesBeforeUnwritable() { 82 | return 0; 83 | } 84 | 85 | @Override 86 | public long bytesBeforeWritable() { 87 | return 0; 88 | } 89 | 90 | @Override 91 | public Unsafe unsafe() { 92 | return null; 93 | } 94 | 95 | @Override 96 | public ChannelPipeline pipeline() { 97 | return null; 98 | } 99 | 100 | @Override 101 | public ByteBufAllocator alloc() { 102 | return null; 103 | } 104 | 105 | @Override 106 | public ChannelFuture bind(SocketAddress localAddress) { 107 | return null; 108 | } 109 | 110 | @Override 111 | public ChannelFuture connect(SocketAddress remoteAddress) { 112 | return null; 113 | } 114 | 115 | @Override 116 | public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { 117 | return null; 118 | } 119 | 120 | @Override 121 | public ChannelFuture disconnect() { 122 | return null; 123 | } 124 | 125 | @Override 126 | public ChannelFuture close() { 127 | return null; 128 | } 129 | 130 | @Override 131 | public ChannelFuture deregister() { 132 | return null; 133 | } 134 | 135 | @Override 136 | public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { 137 | return null; 138 | } 139 | 140 | @Override 141 | public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { 142 | return null; 143 | } 144 | 145 | @Override 146 | public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { 147 | return null; 148 | } 149 | 150 | @Override 151 | public ChannelFuture disconnect(ChannelPromise promise) { 152 | return null; 153 | } 154 | 155 | @Override 156 | public ChannelFuture close(ChannelPromise promise) { 157 | return null; 158 | } 159 | 160 | @Override 161 | public ChannelFuture deregister(ChannelPromise promise) { 162 | return null; 163 | } 164 | 165 | @Override 166 | public Channel read() { 167 | return null; 168 | } 169 | 170 | @Override 171 | public ChannelFuture write(Object msg) { 172 | log.info(prefix+"=>"+msg); 173 | return newSucceededFuture(); 174 | } 175 | 176 | @Override 177 | public ChannelFuture write(Object msg, ChannelPromise promise) { 178 | return write(msg); 179 | } 180 | 181 | @Override 182 | public Channel flush() { 183 | return null; 184 | } 185 | 186 | @Override 187 | public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { 188 | return write(msg); 189 | } 190 | 191 | @Override 192 | public ChannelFuture writeAndFlush(Object msg) { 193 | return write(msg); 194 | } 195 | 196 | @Override 197 | public ChannelPromise newPromise() { 198 | return null; 199 | } 200 | 201 | @Override 202 | public ChannelProgressivePromise newProgressivePromise() { 203 | return null; 204 | } 205 | 206 | @Override 207 | public ChannelFuture newSucceededFuture() { 208 | try { 209 | Class clazz = Class.forName("io.netty.channel.SucceededChannelFuture"); 210 | Constructor constructor = clazz.getDeclaredConstructor(Channel.class, EventExecutor.class); 211 | constructor.setAccessible(true); 212 | Object future = constructor.newInstance(this, eventExecutors); 213 | return (ChannelFuture)future; 214 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 215 | e.printStackTrace(); 216 | } 217 | return null; 218 | } 219 | 220 | @Override 221 | public ChannelFuture newFailedFuture(Throwable cause) { 222 | return null; 223 | } 224 | 225 | @Override 226 | public ChannelPromise voidPromise() { 227 | return null; 228 | } 229 | 230 | @Override 231 | public Attribute attr(AttributeKey key) { 232 | return null; 233 | } 234 | 235 | @Override 236 | public boolean hasAttr(AttributeKey key) { 237 | return false; 238 | } 239 | 240 | @Override 241 | public int compareTo(Channel o) { 242 | return 0; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/Util/StrUtil.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.Util; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | /** 8 | * 字符串工具类 9 | * @author xiaoleilu 10 | * 11 | */ 12 | public class StrUtil { 13 | 14 | public static final String DOT = "."; 15 | public static final String SLASH = "/"; 16 | public static final String EMPTY = ""; 17 | 18 | /** 19 | * 字符串是否为空白 空白的定义如下:
20 | * 1、为null
21 | * 2、为不可见字符(如空格)
22 | * 3、""
23 | * 24 | * @param str 被检测的字符串 25 | * @return 是否为空 26 | */ 27 | public static boolean isBlank(String str) { 28 | return str == null || str.trim().length() == 0; 29 | } 30 | 31 | /** 32 | * 字符串是否为空,空的定义如下 33 | * 1、为null
34 | * 2、为""
35 | * @param str 被检测的字符串 36 | * @return 是否为空 37 | */ 38 | public static boolean isEmpty(String str) { 39 | return str == null || str.length() == 0; 40 | } 41 | 42 | /** 43 | * 获得set或get方法对应的标准属性名
44 | * 例如:setName 返回 name 45 | * @param getOrSetMethodName 46 | * @return 如果是set或get方法名,返回field, 否则null 47 | */ 48 | public static String getGeneralField(String getOrSetMethodName){ 49 | if(getOrSetMethodName.startsWith("get") || getOrSetMethodName.startsWith("set")) { 50 | return cutPreAndLowerFirst(getOrSetMethodName, 3); 51 | } 52 | return null; 53 | } 54 | 55 | /** 56 | * 生成set方法名
57 | * 例如:name 返回 setName 58 | * @param fieldName 属性名 59 | * @return setXxx 60 | */ 61 | public static String genSetter(String fieldName){ 62 | return upperFirstAndAddPre(fieldName, "set"); 63 | } 64 | 65 | /** 66 | * 生成get方法名 67 | * @param fieldName 属性名 68 | * @return getXxx 69 | */ 70 | public static String genGetter(String fieldName){ 71 | return upperFirstAndAddPre(fieldName, "get"); 72 | } 73 | 74 | /** 75 | * 去掉首部指定长度的字符串并将剩余字符串首字母小写
76 | * 例如:str=setName, preLength=3 -> return name 77 | * @param str 被处理的字符串 78 | * @param preLength 去掉的长度 79 | * @return 处理后的字符串,不符合规范返回null 80 | */ 81 | public static String cutPreAndLowerFirst(String str, int preLength) { 82 | if(str == null) { 83 | return null; 84 | } 85 | if(str.length() > preLength) { 86 | char first = Character.toLowerCase(str.charAt(preLength)); 87 | if(str.length() > preLength +1) { 88 | return first + str.substring(preLength +1); 89 | } 90 | return String.valueOf(first); 91 | } 92 | return null; 93 | } 94 | 95 | /** 96 | * 原字符串首字母大写并在其首部添加指定字符串 97 | * 例如:str=name, preString=get -> return getName 98 | * @param str 被处理的字符串 99 | * @param preString 添加的首部 100 | * @return 处理后的字符串 101 | */ 102 | public static String upperFirstAndAddPre(String str, String preString) { 103 | if(str == null || preString == null) { 104 | return null; 105 | } 106 | return preString + upperFirst(str); 107 | } 108 | 109 | /** 110 | * 大写首字母
111 | * 例如:str = name, return Name 112 | * @param str 字符串 113 | * @return 114 | */ 115 | public static String upperFirst(String str) { 116 | return Character.toUpperCase(str.charAt(0)) + str.substring(1); 117 | } 118 | 119 | /** 120 | * 小写首字母
121 | * 例如:str = Name, return name 122 | * @param str 字符串 123 | * @return 124 | */ 125 | public static String lowerFirst(String str) { 126 | return Character.toLowerCase(str.charAt(0)) + str.substring(1); 127 | } 128 | 129 | /** 130 | * 去掉指定前缀 131 | * @param str 字符串 132 | * @param prefix 前缀 133 | * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 134 | */ 135 | public static String removePrefix(String str, String prefix) { 136 | if(str != null && str.startsWith(prefix)) { 137 | return str.substring(prefix.length()); 138 | } 139 | return str; 140 | } 141 | 142 | /** 143 | * 忽略大小写去掉指定前缀 144 | * @param str 字符串 145 | * @param prefix 前缀 146 | * @return 切掉后的字符串,若前缀不是 prefix, 返回原字符串 147 | */ 148 | public static String removePrefixIgnoreCase(String str, String prefix) { 149 | if (str != null && str.toLowerCase().startsWith(prefix.toLowerCase())) { 150 | return str.substring(prefix.length()); 151 | } 152 | return str; 153 | } 154 | 155 | /** 156 | * 去掉指定后缀 157 | * @param str 字符串 158 | * @param suffix 后缀 159 | * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 160 | */ 161 | public static String removeSuffix(String str, String suffix) { 162 | if (str != null && str.endsWith(suffix)) { 163 | return str.substring(0, str.length() - suffix.length()); 164 | } 165 | return str; 166 | } 167 | 168 | /** 169 | * 忽略大小写去掉指定后缀 170 | * @param str 字符串 171 | * @param suffix 后缀 172 | * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 173 | */ 174 | public static String removeSuffixIgnoreCase(String str, String suffix) { 175 | if (str != null && str.toLowerCase().endsWith(suffix.toLowerCase())) { 176 | return str.substring(0, str.length() - suffix.length()); 177 | } 178 | return str; 179 | } 180 | 181 | /** 182 | * 切分字符串
183 | * a#b#c -> [a,b,c] 184 | * a##b#c -> [a,"",b,c] 185 | * @param str 被切分的字符串 186 | * @param separator 分隔符字符 187 | * @return 切分后的集合 188 | */ 189 | public static List split(String str, char separator) { 190 | return split(str, separator, 0); 191 | } 192 | 193 | /** 194 | * 切分字符串 195 | * @param str 被切分的字符串 196 | * @param separator 分隔符字符 197 | * @param limit 限制分片数 198 | * @return 切分后的集合 199 | */ 200 | public static List split(String str, char separator, int limit){ 201 | if(str == null) { 202 | return null; 203 | } 204 | List list = new ArrayList(limit == 0 ? 16 : limit); 205 | if(limit == 1) { 206 | list.add(str); 207 | return list; 208 | } 209 | 210 | boolean isNotEnd = true; //未结束切分的标志 211 | int strLen = str.length(); 212 | StringBuilder sb = new StringBuilder(strLen); 213 | for(int i=0; i < strLen; i++) { 214 | char c = str.charAt(i); 215 | if(isNotEnd && c == separator) { 216 | list.add(sb.toString()); 217 | //清空StringBuilder 218 | sb.delete(0, sb.length()); 219 | 220 | //当达到切分上限-1的量时,将所剩字符全部作为最后一个串 221 | if(limit !=0 && list.size() == limit-1) { 222 | isNotEnd = false; 223 | } 224 | }else { 225 | sb.append(c); 226 | } 227 | } 228 | list.add(sb.toString()); 229 | return list; 230 | } 231 | 232 | /** 233 | * 切分字符串
234 | * from jodd 235 | * @param str 被切分的字符串 236 | * @param delimiter 分隔符 237 | * @return 238 | */ 239 | public static String[] split(String str, String delimiter) { 240 | if(str == null) { 241 | return null; 242 | } 243 | if(str.trim().length() == 0) { 244 | return new String[]{str}; 245 | } 246 | 247 | int dellen = delimiter.length(); //del length 248 | int maxparts = (str.length() / dellen) + 2; // one more for the last 249 | int[] positions = new int[maxparts]; 250 | 251 | int i, j = 0; 252 | int count = 0; 253 | positions[0] = - dellen; 254 | while ((i = str.indexOf(delimiter, j)) != -1) { 255 | count++; 256 | positions[count] = i; 257 | j = i + dellen; 258 | } 259 | count++; 260 | positions[count] = str.length(); 261 | 262 | String[] result = new String[count]; 263 | 264 | for (i = 0; i < count; i++) { 265 | result[i] = str.substring(positions[i] + dellen, positions[i + 1]); 266 | } 267 | return result; 268 | } 269 | 270 | /** 271 | * 重复某个字符 272 | * @param c 被重复的字符 273 | * @param count 重复的数目 274 | * @return 重复字符字符串 275 | */ 276 | public static String repeat(char c, int count) { 277 | char[] result = new char[count]; 278 | for (int i = 0; i < count; i++) { 279 | result[i] = c; 280 | } 281 | return new String(result); 282 | } 283 | 284 | /** 285 | * 给定字符串转换字符编码
286 | * 如果参数为空,则返回原字符串,不报错。 287 | * @param str 被转码的字符串 288 | * @param sourceCharset 原字符集 289 | * @param destCharset 目标字符集 290 | * @return 转换后的字符串 291 | */ 292 | public static String convertCharset(String str, String sourceCharset, String destCharset) { 293 | if(isBlank(str) || isBlank(sourceCharset) || isBlank(destCharset)) { 294 | return str; 295 | } 296 | try { 297 | return new String(str.getBytes(sourceCharset), destCharset); 298 | } catch (UnsupportedEncodingException e) { 299 | return str; 300 | } 301 | } 302 | 303 | /** 304 | * 比较两个字符串是否相同,如果为null或者空串则算不同 305 | * @param str1 字符串1 306 | * @param str2 字符串2 307 | * @return 是否非空相同 308 | */ 309 | public static boolean equalsNotEmpty(String str1, String str2) { 310 | if(isEmpty(str1)) { 311 | return false; 312 | } 313 | return str1.equals(str2); 314 | } 315 | 316 | /** 317 | * 格式化文本 318 | * @param template 文本模板,被替换的部分用 {} 表示 319 | * @param values 参数值 320 | * @return 格式化后的文本 321 | */ 322 | public static String format(String template, Object... values) { 323 | return String.format(template.replace("{}", "%s"), values); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/Util/ClassUtil.java: -------------------------------------------------------------------------------- 1 | package com.wks.wsIm.Util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.File; 7 | import java.io.FileFilter; 8 | import java.io.IOException; 9 | import java.lang.reflect.Method; 10 | import java.net.URL; 11 | import java.util.Collections; 12 | import java.util.Enumeration; 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | import java.util.jar.JarEntry; 16 | import java.util.jar.JarFile; 17 | 18 | /** 19 | * 类工具类 20 | * 1、扫描指定包下的所有类 21 | * @see http://www.oschina.net/code/snippet_234657_22722 22 | * @author seaside_hi, xiaoleilu 23 | * 24 | */ 25 | public class ClassUtil { 26 | private static Logger log = LoggerFactory.getLogger(ClassUtil.class); 27 | 28 | /** Class文件扩展名 */ 29 | private static final String CLASS_EXT = ".class"; 30 | /** Jar文件扩展名 */ 31 | private static final String JAR_FILE_EXT = ".jar"; 32 | /** 在Jar中的路径jar的扩展名形式 */ 33 | private static final String JAR_PATH_EXT = ".jar!"; 34 | /** 当Path为文件形式时, path会加入一个表示文件的前缀 */ 35 | private static final String PATH_FILE_PRE = "file:"; 36 | /** 扫描Class文件时空的过滤器,表示不过滤 */ 37 | private static final ClassFilter NULL_CLASS_FILTER = null; 38 | 39 | 40 | 41 | private ClassUtil() { 42 | } 43 | 44 | /** 45 | * 扫面改包路径下所有class文件 46 | * 47 | * @param packageName 包路径 com | com. | com.abs | com.abs. 48 | * @return 49 | */ 50 | public static Set> scanPackage(String packageName) { 51 | log.debug("Scan classes from package [{}]...", packageName); 52 | return scanPackage(packageName, NULL_CLASS_FILTER); 53 | } 54 | 55 | /** 56 | * 扫面包路径下满足class过滤器条件的所有class文件,
57 | * 如果包路径为 com.abs + A.class 但是输入 abs会产生classNotFoundException
58 | * 因为className 应该为 com.abs.A 现在却成为abs.A,此工具类对该异常进行忽略处理,有可能是一个不完善的地方,以后需要进行修改
59 | * 60 | * @param packageName 包路径 com | com. | com.abs | com.abs. 61 | * @param classFilter class过滤器,过滤掉不需要的class 62 | * @return 63 | */ 64 | public static Set> scanPackage(String packageName, ClassFilter classFilter) { 65 | if(StrUtil.isBlank(packageName)) throw new NullPointerException("packageName can't be blank!"); 66 | packageName = getWellFormedPackageName(packageName); 67 | 68 | final Set> classes = new HashSet>(); 69 | for (String classPath : getClassPaths(packageName)) { 70 | log.debug("Scan classpath: [{}]", classPath); 71 | // 填充 classes 72 | fillClasses(classPath, packageName, classFilter, classes); 73 | } 74 | 75 | //如果在项目的ClassPath中未找到,去系统定义的ClassPath里找 76 | if(classes.isEmpty()) { 77 | for (String classPath : getJavaClassPaths()) { 78 | log.debug("Scan java classpath: [{}]", classPath); 79 | // 填充 classes 80 | fillClasses(new File(classPath), packageName, classFilter, classes); 81 | } 82 | } 83 | return classes; 84 | } 85 | 86 | /** 87 | * 获得指定类中的Public方法名
88 | * 去重重载的方法 89 | * @param clazz 类 90 | */ 91 | public final static Set getMethods(Class clazz) { 92 | HashSet methodSet = new HashSet(); 93 | Method[] methodArray = clazz.getMethods(); 94 | for (Method method : methodArray) { 95 | String methodName = method.getName(); 96 | methodSet.add(methodName); 97 | } 98 | return methodSet; 99 | } 100 | 101 | /** 102 | * 获得ClassPath 103 | * @return 104 | */ 105 | public static Set getClassPathResources(){ 106 | return getClassPaths(StrUtil.EMPTY); 107 | } 108 | 109 | /** 110 | * 获得ClassPath 111 | * @param packageName 包名称 112 | * @return 113 | */ 114 | public static Set getClassPaths(String packageName){ 115 | String packagePath = packageName.replace(StrUtil.DOT, StrUtil.SLASH); 116 | Enumeration resources; 117 | try { 118 | resources = Thread.currentThread().getContextClassLoader().getResources(packagePath); 119 | } catch (IOException e) { 120 | log.error("Error when load classPath!", e); 121 | return null; 122 | } 123 | Set paths = new HashSet(); 124 | while(resources.hasMoreElements()) { 125 | paths.add(resources.nextElement().getPath()); 126 | } 127 | return paths; 128 | } 129 | 130 | /** 131 | * 获得Java ClassPath路径,不包括 jre
132 | * @return 133 | */ 134 | public static String[] getJavaClassPaths() { 135 | String[] classPaths = System.getProperty("java.class.path").split(System.getProperty("path.separator")); 136 | return classPaths; 137 | } 138 | 139 | 140 | //--------------------------------------------------------------------------------------------------- Private method start 141 | /** 142 | * 文件过滤器,过滤掉不需要的文件
143 | * 只保留Class文件、目录和Jar 144 | */ 145 | private static FileFilter fileFilter = new FileFilter(){ 146 | @Override 147 | public boolean accept(File pathname) { 148 | return isClass(pathname.getName()) || pathname.isDirectory() || isJarFile(pathname); 149 | } 150 | }; 151 | 152 | /** 153 | * 改变 com -> com. 避免在比较的时候把比如 completeTestSuite.class类扫描进去,如果没有"." 154 | *
那class里面com开头的class类也会被扫描进去,其实名称后面或前面需要一个 ".",来添加包的特征 155 | * 156 | * @param packageName 157 | * @return 158 | */ 159 | private static String getWellFormedPackageName(String packageName) { 160 | return packageName.lastIndexOf(StrUtil.DOT) != packageName.length() - 1 ? packageName + StrUtil.DOT : packageName; 161 | } 162 | 163 | /** 164 | * 填充满足条件的class 填充到 classes
165 | * 同时会判断给定的路径是否为Jar包内的路径,如果是,则扫描此Jar包 166 | * 167 | * @param path Class文件路径或者所在目录Jar包路径 168 | * @param packageName 需要扫面的包名 169 | * @param classFilter class过滤器 170 | * @param classes List 集合 171 | */ 172 | private static void fillClasses(String path, String packageName, ClassFilter classFilter, Set> classes) { 173 | //判定给定的路径是否为 174 | int index = path.lastIndexOf(JAR_PATH_EXT); 175 | if(index != -1) { 176 | path = path.substring(0, index + JAR_FILE_EXT.length()); 177 | path = StrUtil.removePrefix(path, PATH_FILE_PRE); 178 | processJarFile(new File(path), packageName, classFilter, classes); 179 | }else { 180 | fillClasses(new File(path), packageName, classFilter, classes); 181 | } 182 | } 183 | 184 | /** 185 | * 填充满足条件的class 填充到 classes 186 | * 187 | * @param file Class文件或者所在目录Jar包文件 188 | * @param packageName 需要扫面的包名 189 | * @param classFilter class过滤器 190 | * @param classes List 集合 191 | */ 192 | private static void fillClasses(File file, String packageName, ClassFilter classFilter, Set> classes) { 193 | if (file.isDirectory()) { 194 | processDirectory(file, packageName, classFilter, classes); 195 | } else if (isClassFile(file)) { 196 | processClassFile(file, packageName, classFilter, classes); 197 | } else if (isJarFile(file)) { 198 | processJarFile(file, packageName, classFilter, classes); 199 | } 200 | } 201 | 202 | /** 203 | * 处理如果为目录的情况,需要递归调用 fillClasses方法 204 | * 205 | * @param directory 206 | * @param packageName 207 | * @param classFilter 208 | * @param classes 209 | */ 210 | private static void processDirectory(File directory, String packageName, ClassFilter classFilter, Set> classes) { 211 | for (File file : directory.listFiles(fileFilter)) { 212 | fillClasses(file, packageName, classFilter, classes); 213 | } 214 | } 215 | 216 | /** 217 | * 处理为class文件的情况,填充满足条件的class 到 classes 218 | * 219 | * @param file 220 | * @param packageName 221 | * @param classFilter 222 | * @param classes 223 | */ 224 | private static void processClassFile(File file, String packageName, ClassFilter classFilter, Set> classes) { 225 | final String filePathWithDot = file.getAbsolutePath().replace(File.separator, StrUtil.DOT); 226 | int subIndex = -1; 227 | if ((subIndex = filePathWithDot.indexOf(packageName)) != -1) { 228 | final String className = filePathWithDot.substring(subIndex).replace(CLASS_EXT, StrUtil.EMPTY); 229 | fillClass(className, packageName, classes, classFilter); 230 | } 231 | } 232 | 233 | /** 234 | * 处理为jar文件的情况,填充满足条件的class 到 classes 235 | * 236 | * @param file 237 | * @param packageName 238 | * @param classFilter 239 | * @param classes 240 | */ 241 | private static void processJarFile(File file, String packageName, ClassFilter classFilter, Set> classes) { 242 | try { 243 | for (JarEntry entry : Collections.list(new JarFile(file).entries())) { 244 | if (isClass(entry.getName())) { 245 | final String className = entry.getName().replace(StrUtil.SLASH, StrUtil.DOT).replace(CLASS_EXT, StrUtil.EMPTY); 246 | fillClass(className, packageName, classes, classFilter); 247 | } 248 | } 249 | } catch (Throwable ex) { 250 | log.error(ex.getMessage(), ex); 251 | } 252 | } 253 | 254 | /** 255 | * 填充class 到 classes 256 | * 257 | * @param className 258 | * @param packageName 259 | * @param classes 260 | * @param classFilter 261 | */ 262 | private static void fillClass(String className, String packageName, Set> classes, ClassFilter classFilter) { 263 | if (className.startsWith(packageName)) { 264 | try { 265 | final Class clazz = Class.forName(className, false, Thread.currentThread().getContextClassLoader()); 266 | if (classFilter == NULL_CLASS_FILTER || classFilter.accept(clazz)) { 267 | classes.add(clazz); 268 | } 269 | } catch (Throwable ex) { 270 | log.error("", ex); 271 | } 272 | } 273 | } 274 | 275 | private static boolean isClassFile(File file) { 276 | return isClass(file.getName()); 277 | } 278 | 279 | private static boolean isClass(String fileName) { 280 | return fileName.endsWith(CLASS_EXT); 281 | } 282 | 283 | private static boolean isJarFile(File file) { 284 | return file.getName().contains(JAR_FILE_EXT); 285 | } 286 | //--------------------------------------------------------------------------------------------------- Private method end 287 | 288 | /** 289 | * 类过滤器,用于过滤不需要加载的类 290 | * @see http://www.oschina.net/code/snippet_234657_22722 291 | * @author seaside_hi 292 | */ 293 | @FunctionalInterface 294 | public interface ClassFilter { 295 | boolean accept(Class clazz); 296 | } 297 | } -------------------------------------------------------------------------------- /src/main/java/com/wks/wsIm/server/HttpStaticFileServerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.wks.wsIm.server; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.channel.ChannelFuture; 21 | import io.netty.channel.ChannelFutureListener; 22 | import io.netty.channel.ChannelHandlerContext; 23 | import io.netty.channel.ChannelProgressiveFuture; 24 | import io.netty.channel.ChannelProgressiveFutureListener; 25 | import io.netty.channel.DefaultFileRegion; 26 | import io.netty.channel.SimpleChannelInboundHandler; 27 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 28 | import io.netty.handler.codec.http.DefaultHttpResponse; 29 | import io.netty.handler.codec.http.FullHttpRequest; 30 | import io.netty.handler.codec.http.FullHttpResponse; 31 | import io.netty.handler.codec.http.HttpChunkedInput; 32 | import io.netty.handler.codec.http.HttpHeaderNames; 33 | import io.netty.handler.codec.http.HttpUtil; 34 | import io.netty.handler.codec.http.HttpHeaderValues; 35 | import io.netty.handler.codec.http.HttpResponse; 36 | import io.netty.handler.codec.http.HttpResponseStatus; 37 | import io.netty.handler.codec.http.LastHttpContent; 38 | import io.netty.handler.ssl.SslHandler; 39 | import io.netty.handler.stream.ChunkedFile; 40 | import io.netty.util.CharsetUtil; 41 | import io.netty.util.internal.SystemPropertyUtil; 42 | 43 | import javax.activation.MimetypesFileTypeMap; 44 | import java.io.File; 45 | import java.io.FileNotFoundException; 46 | import java.io.RandomAccessFile; 47 | import java.io.UnsupportedEncodingException; 48 | import java.net.URLDecoder; 49 | import java.text.SimpleDateFormat; 50 | import java.util.Calendar; 51 | import java.util.Date; 52 | import java.util.GregorianCalendar; 53 | import java.util.Locale; 54 | import java.util.TimeZone; 55 | import java.util.regex.Pattern; 56 | 57 | import static io.netty.handler.codec.http.HttpMethod.*; 58 | import static io.netty.handler.codec.http.HttpResponseStatus.*; 59 | import static io.netty.handler.codec.http.HttpVersion.*; 60 | 61 | /** 62 | * A simple handler that serves incoming HTTP requests to send their respective 63 | * HTTP responses. It also implements {@code 'If-Modified-Since'} header to 64 | * take advantage of browser cache, as described in 65 | * RFC 2616. 66 | * 67 | *

How Browser Caching Works

68 | * 69 | * Web browser caching works with HTTP headers as illustrated by the following 70 | * sample: 71 | *
    72 | *
  1. Request #1 returns the content of {@code /file1.txt}.
  2. 73 | *
  3. Contents of {@code /file1.txt} is cached by the browser.
  4. 74 | *
  5. Request #2 for {@code /file1.txt} does not return the contents of the 75 | * file again. Rather, a 304 Not Modified is returned. This tells the 76 | * browser to use the contents stored in its cache.
  6. 77 | *
  7. The server knows the file has not been modified because the 78 | * {@code If-Modified-Since} date is the same as the file's last 79 | * modified date.
  8. 80 | *
81 | * 82 | *
 83 |  * Request #1 Headers
 84 |  * ===================
 85 |  * GET /file1.txt HTTP/1.1
 86 |  *
 87 |  * Response #1 Headers
 88 |  * ===================
 89 |  * HTTP/1.1 200 OK
 90 |  * Date:               Tue, 01 Mar 2011 22:44:26 GMT
 91 |  * Last-Modified:      Wed, 30 Jun 2010 21:36:48 GMT
 92 |  * Expires:            Tue, 01 Mar 2012 22:44:26 GMT
 93 |  * Cache-Control:      private, max-age=31536000
 94 |  *
 95 |  * Request #2 Headers
 96 |  * ===================
 97 |  * GET /file1.txt HTTP/1.1
 98 |  * If-Modified-Since:  Wed, 30 Jun 2010 21:36:48 GMT
 99 |  *
100 |  * Response #2 Headers
101 |  * ===================
102 |  * HTTP/1.1 304 Not Modified
103 |  * Date:               Tue, 01 Mar 2011 22:44:28 GMT
104 |  *
105 |  * 
106 | */ 107 | public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler { 108 | 109 | public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; 110 | public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; 111 | public static final int HTTP_CACHE_SECONDS = 60; 112 | 113 | private FullHttpRequest request; 114 | 115 | @Override 116 | public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { 117 | this.request = request; 118 | if (!request.decoderResult().isSuccess()) { 119 | sendError(ctx, BAD_REQUEST); 120 | return; 121 | } 122 | 123 | if (!GET.equals(request.method())) { 124 | this.sendError(ctx, METHOD_NOT_ALLOWED); 125 | return; 126 | } 127 | 128 | final boolean keepAlive = HttpUtil.isKeepAlive(request); 129 | final String uri = request.uri(); 130 | final String path = sanitizeUri(uri); 131 | if (path == null) { 132 | this.sendError(ctx, FORBIDDEN); 133 | return; 134 | } 135 | 136 | File file = new File(path); 137 | if (file.isHidden() || !file.exists()) { 138 | this.sendError(ctx, NOT_FOUND); 139 | return; 140 | } 141 | 142 | if (file.isDirectory()) { 143 | if (uri.endsWith("/")) { 144 | this.sendListing(ctx, file, uri); 145 | } else { 146 | this.sendRedirect(ctx, uri + '/'); 147 | } 148 | return; 149 | } 150 | 151 | if (!file.isFile()) { 152 | sendError(ctx, FORBIDDEN); 153 | return; 154 | } 155 | 156 | // Cache Validation 157 | String ifModifiedSince =request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE); 158 | if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) { 159 | SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); 160 | Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); 161 | 162 | // Only compare up to the second because the datetime format we send to the client 163 | // does not have milliseconds 164 | long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; 165 | long fileLastModifiedSeconds = file.lastModified() / 1000; 166 | if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { 167 | this.sendNotModified(ctx); 168 | return; 169 | } 170 | } 171 | 172 | RandomAccessFile raf; 173 | try { 174 | raf = new RandomAccessFile(file, "r"); 175 | } catch (FileNotFoundException ignore) { 176 | sendError(ctx, NOT_FOUND); 177 | return; 178 | } 179 | long fileLength = raf.length(); 180 | 181 | HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); 182 | HttpUtil.setContentLength(response, fileLength); 183 | setContentTypeHeader(response, file); 184 | setDateAndCacheHeaders(response, file); 185 | 186 | if (!keepAlive) { 187 | response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); 188 | } else if (request.protocolVersion().equals(HTTP_1_0)) { 189 | response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 190 | } 191 | 192 | // Write the initial line and the header. 193 | ctx.write(response); 194 | 195 | // Write the content. 196 | ChannelFuture sendFileFuture; 197 | ChannelFuture lastContentFuture; 198 | if (ctx.pipeline().get(SslHandler.class) == null) { 199 | sendFileFuture = 200 | ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise()); 201 | // Write the end marker. 202 | lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 203 | } else { 204 | sendFileFuture = 205 | ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)), 206 | ctx.newProgressivePromise()); 207 | // HttpChunkedInput will write the end marker (LastHttpContent) for us. 208 | lastContentFuture = sendFileFuture; 209 | } 210 | 211 | sendFileFuture.addListener(new ChannelProgressiveFutureListener() { 212 | @Override 213 | public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { 214 | if (total < 0) { // total unknown 215 | System.err.println(future.channel() + " Transfer progress: " + progress); 216 | } else { 217 | System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total); 218 | } 219 | } 220 | 221 | @Override 222 | public void operationComplete(ChannelProgressiveFuture future) { 223 | System.err.println(future.channel() + " Transfer complete."); 224 | } 225 | }); 226 | 227 | // Decide whether to close the connection or not. 228 | if (!keepAlive) { 229 | // Close the connection when the whole content is written out. 230 | lastContentFuture.addListener(ChannelFutureListener.CLOSE); 231 | } 232 | } 233 | 234 | @Override 235 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 236 | cause.printStackTrace(); 237 | if (ctx.channel().isActive()) { 238 | sendError(ctx, INTERNAL_SERVER_ERROR); 239 | } 240 | } 241 | 242 | private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); 243 | 244 | private static String sanitizeUri(String uri) { 245 | if(uri.equals("/")){ 246 | uri="/index.html"; 247 | } 248 | 249 | // Decode the path. 250 | try { 251 | uri = URLDecoder.decode(uri, "UTF-8"); 252 | } catch (UnsupportedEncodingException e) { 253 | throw new Error(e); 254 | } 255 | 256 | if (uri.isEmpty() || uri.charAt(0) != '/') { 257 | return null; 258 | } 259 | 260 | // Convert file separators. 261 | uri = uri.replace('/', File.separatorChar); 262 | 263 | // Simplistic dumb security check. 264 | // You will have to do something serious in the production environment. 265 | if (uri.contains(File.separator + '.') || 266 | uri.contains('.' + File.separator) || 267 | uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' || 268 | INSECURE_URI.matcher(uri).matches()) { 269 | return null; 270 | } 271 | 272 | // Convert to absolute path. 273 | return System.getProperty("front_dir") + File.separator + uri; 274 | } 275 | 276 | private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*"); 277 | 278 | private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) { 279 | StringBuilder buf = new StringBuilder() 280 | .append("\r\n") 281 | .append("") 282 | .append("Listing of: ") 283 | .append(dirPath) 284 | .append("\r\n") 285 | 286 | .append("

Listing of: ") 287 | .append(dirPath) 288 | .append("

\r\n") 289 | 290 | .append("
    ") 291 | .append("
  • ..
  • \r\n"); 292 | 293 | for (File f: dir.listFiles()) { 294 | if (f.isHidden() || !f.canRead()) { 295 | continue; 296 | } 297 | 298 | String name = f.getName(); 299 | if (!ALLOWED_FILE_NAME.matcher(name).matches()) { 300 | continue; 301 | } 302 | 303 | buf.append("
  • ") 306 | .append(name) 307 | .append("
  • \r\n"); 308 | } 309 | 310 | buf.append("
\r\n"); 311 | 312 | ByteBuf buffer = ctx.alloc().buffer(buf.length()); 313 | buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8); 314 | 315 | FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer); 316 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); 317 | 318 | this.sendAndCleanupConnection(ctx, response); 319 | } 320 | 321 | private void sendRedirect(ChannelHandlerContext ctx, String newUri) { 322 | FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER); 323 | response.headers().set(HttpHeaderNames.LOCATION, newUri); 324 | 325 | this.sendAndCleanupConnection(ctx, response); 326 | } 327 | 328 | private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { 329 | FullHttpResponse response = new DefaultFullHttpResponse( 330 | HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8)); 331 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); 332 | 333 | this.sendAndCleanupConnection(ctx, response); 334 | } 335 | 336 | /** 337 | * When file timestamp is the same as what the browser is sending up, send a "304 Not Modified" 338 | * 339 | * @param ctx 340 | * Context 341 | */ 342 | private void sendNotModified(ChannelHandlerContext ctx) { 343 | FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER); 344 | setDateHeader(response); 345 | 346 | this.sendAndCleanupConnection(ctx, response); 347 | } 348 | 349 | /** 350 | * If Keep-Alive is disabled, attaches "Connection: close" header to the response 351 | * and closes the connection after the response being sent. 352 | */ 353 | private void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response) { 354 | final FullHttpRequest request = this.request; 355 | final boolean keepAlive = HttpUtil.isKeepAlive(request); 356 | HttpUtil.setContentLength(response, response.content().readableBytes()); 357 | if (!keepAlive) { 358 | // We're going to close the connection as soon as the response is sent, 359 | // so we should also make it clear for the client. 360 | response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); 361 | } else if (request.protocolVersion().equals(HTTP_1_0)) { 362 | response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 363 | } 364 | 365 | ChannelFuture flushPromise = ctx.writeAndFlush(response); 366 | 367 | if (!keepAlive) { 368 | // Close the connection as soon as the response is sent. 369 | flushPromise.addListener(ChannelFutureListener.CLOSE); 370 | } 371 | } 372 | 373 | /** 374 | * Sets the Date header for the HTTP response 375 | * 376 | * @param response 377 | * HTTP response 378 | */ 379 | private static void setDateHeader(FullHttpResponse response) { 380 | SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); 381 | dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); 382 | 383 | Calendar time = new GregorianCalendar(); 384 | response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); 385 | } 386 | 387 | /** 388 | * Sets the Date and Cache headers for the HTTP Response 389 | * 390 | * @param response 391 | * HTTP response 392 | * @param fileToCache 393 | * file to extract content type 394 | */ 395 | private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { 396 | SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); 397 | dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); 398 | 399 | // Date header 400 | Calendar time = new GregorianCalendar(); 401 | response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); 402 | 403 | // Add cache headers 404 | time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); 405 | response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime())); 406 | response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); 407 | response.headers().set( 408 | HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); 409 | } 410 | 411 | /** 412 | * Sets the content type header for the HTTP Response 413 | * 414 | * @param response 415 | * HTTP response 416 | * @param file 417 | * file to extract content type 418 | */ 419 | private static void setContentTypeHeader(HttpResponse response, File file) { 420 | MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); 421 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); 422 | } 423 | } 424 | --------------------------------------------------------------------------------