Netty - 一个简单的聊天室小项目

经过一段时间对Netty的学习,我们对Netty各版本以及像ProtocolBuffers等技术应用都有了不少相关的了解, 我们就用这段时间学到的只是做一个简单的聊天室的小项目来练习自己学到的技术。
做这个小项目之前我们先大致了解下我们需要用到的技术点,netty3.x/4.x/5.x、swingbuilder、protobuf。这里我们先以Netty3为例构建项目,各位可以通过git上查找Netty5以及结合了ProtocolBuffers版本的聊天室项目。
Netty_Demo[git]
我们先稍微比较下Netty几个版本之间的应用差异

netty3.x netty4.x/5.x
ChannelBuffer ByteBuf
ChannelBuffers PooledByteBufAllocator(要注意使用完后释放buffer)或UnpooledByteBufAllocator或Unpooled
FrameDecoder ByteToMessageDecoder
OneToOneEncoder MessageToByteEncoder
messageReceive channelRead0(netty5中messageReceive)
Common部分
项目目录结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

首先我们需要导入依赖,这里结合spring、mybatis、netty、log4j

pom.xml
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>Netty_Demo</artifactId>
        <groupId>com.ithzk</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>Netty3</artifactId>

    <name>Netty3</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <!-- spring版本号 -->
        <spring.version>4.3.13.RELEASE</spring.version>
        <!-- mybatis版本号 -->
        <mybatis.version>3.2.6</mybatis.version>
        <!-- log4j日志文件管理包版本 -->
        <slf4j.version>1.7.7</slf4j.version>
        <log4j.version>1.2.17</log4j.version>
        <jedis.version>2.7.3</jedis.version>
        <druid.version>1.1.6</druid.version>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/io.netty/netty -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty</artifactId>
            <version>3.10.5.Final</version>
        </dependency>
        <!-- log start -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>2.4.1</version>
        </dependency>
        <!-- mybatis核心包 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.2.6</version>
        </dependency>
        <!-- mybatis/spring包 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.2.2</version>
        </dependency>
        <!-- 导入dbcp的jar包,用来在applicationContext.xml中配置数据库 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

        <!-- spring核心包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-oxm</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- 导入java ee jar 包 -->
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
        </dependency>
        <!-- 导入Mysql数据库链接jar包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.30</version>
        </dependency>
    </dependencies>
</project>

这里两个自定义注解主要作用是为了之后Hadnler处理调用业务代码有序管理使用

SocketCommand.java
package com.chat.common.core.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 请求命令
 * @author hzk
 * @date 2018/10/22
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SocketCommand {

    /**
     * 请求命令号
     */
    short cmd();
}

SocketModule .java
package com.chat.common.core.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 请求模块
 * @author hzk
 * @date 2018/10/22
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SocketModule {

    /**
     * 请求模块号
     */
    short module();
}

这里请求和响应的自定义编码器和解码器和我们之前一起学习时候是大体一致的,基本没有改变

RequestDecoder.java
package com.chat.common.core.coder;

import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Request;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

/**
 * 请求解码器
 * 数据包格式(根据需求定义)
 * 包头 模块号 命令号 长度 数据 
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 * @author hzk
 * @date 2018/9/29
 */
public class RequestDecoder extends FrameDecoder{

    @Override
    protected Object decode(ChannelHandlerContext channelHandlerContext, Channel channel, ChannelBuffer channelBuffer) throws Exception {
        //可读长度必须大于基本长度
        if(channelBuffer.readableBytes() >= Constants.AbstractDataStructure.DATA_STRUCTURE_LENGTH){
            //防止socket字节流攻击
            if(channelBuffer.readableBytes() > 2048){
                channelBuffer.skipBytes(channelBuffer.readableBytes());
            }

            //记录包头开始偏移Index,即第一个可读数据的起始位置
            int beginIndex;

            while (true){
                beginIndex = channelBuffer.readerIndex();
                //标记读索引位置
                channelBuffer.markReaderIndex();
                int packHead = channelBuffer.readInt();
                if(Constants.AbstractDataStructure.PACKAGE_HEAD == packHead){
                    break;
                }

                //未读取到包头,还原读索引位置,略过一个字节
                channelBuffer.resetReaderIndex();
                channelBuffer.readByte();

                if(channelBuffer.readableBytes() < Constants.AbstractDataStructure.DATA_STRUCTURE_LENGTH){
                    //数据包不完整,需要等待后面的数据包
                    return null;
                }
            }
            //模块号
            short module = channelBuffer.readShort();
            //命令号
            short cmd = channelBuffer.readShort();
            //数据长度
            int length = channelBuffer.readInt();
            if(length <0){
                channel.close();
            }

            //判断请求数据包 数据是否完整
            if(channelBuffer.readableBytes() < length){
                //还原读指针
                channelBuffer.readerIndex(beginIndex);
                return null;
            }

            //读取data数据
            byte[] data = new byte[length];
            channelBuffer.readBytes(data);

            //解析出消息对象,往下传递(handler)
            return new Request(module,cmd,data);
        }
        //数据包不完整,需要等待后面的数据包
        return null;
    }
}

RequestEncoder.java
package com.chat.common.core.coder;

import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Request;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;

/**
 * 请求编码器
 * 数据包格式(根据需求定义)
 * 包头 模块号 命令号 长度 数据 
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 * @author hzk
 * @date 2018/9/29
 */
public class RequestEncoder extends OneToOneEncoder{

    @Override
    protected Object encode(ChannelHandlerContext channelHandlerContext, Channel channel, Object rs) throws Exception {
        Request request = (Request) rs;
        ChannelBuffer channelBuffer = ChannelBuffers.dynamicBuffer();
        //包头
        channelBuffer.writeInt(Constants.AbstractDataStructure.PACKAGE_HEAD);
        //模块Module
        channelBuffer.writeShort(request.getModule());
        //命令号cmd
        channelBuffer.writeShort(request.getCmd());
        //数据长度
        channelBuffer.writeInt(request.getDataLength());
        //数据
        if(null != request.getData()){
            channelBuffer.writeBytes(request.getData());
        }
        return channelBuffer;
    }
}

ResponseDecoder.java
package com.chat.common.core.coder;

import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Response;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

/**
 * 响应解码器
 * 数据包格式(根据需求定义)
 * 包头 模块号 命令号 长度 数据 
 * 包头4字节int
 * 模块号2字节short
 * 命令号2字节short
 * 响应码4字节int
 * 长度4字节(描述数据部分字节长度)
 * @author hzk
 * @date 2018/9/29
 */
public class ResponseDecoder extends FrameDecoder{

    @Override
    protected Object decode(ChannelHandlerContext channelHandlerContext, Channel channel, ChannelBuffer channelBuffer) throws Exception {
        //可读长度必须大于基本长度
        if(channelBuffer.readableBytes() >= Constants.AbstractDataStructure.DATA_STRUCTURE_LENGTH){
            //防止socket字节流攻击
            if(channelBuffer.readableBytes() > 2048){
                channelBuffer.skipBytes(channelBuffer.readableBytes());
            }

            //记录包头开始偏移Index
            int beginIndex = channelBuffer.readerIndex();

            while (true){
                beginIndex = channelBuffer.readerIndex();
                //标记读索引位置
                channelBuffer.markReaderIndex();
                int packHead = channelBuffer.readInt();
                if(Constants.AbstractDataStructure.PACKAGE_HEAD == packHead){
                    break;
                }

                //未读取到包头,还原读索引位置,略过一个字节
                channelBuffer.resetReaderIndex();
                channelBuffer.readByte();

                if(channelBuffer.readableBytes() < Constants.AbstractDataStructure.DATA_RESPONSE_STRUCTURE_LENGTH){
                    //数据包不完整,需要等待后面的数据包
                    return null;
                }

            }

            //模块号
            short module = channelBuffer.readShort();
            //命令号
            short cmd = channelBuffer.readShort();
            //状态码
            int code = channelBuffer.readInt();
            //数据长度
            int length = channelBuffer.readInt();
            if(length < 0){
                channel.close();
            }

            //判断请求数据包 数据是否完整
            if(channelBuffer.readableBytes() < length){
                //还原读指针
                channelBuffer.readerIndex(beginIndex);
                return null;
            }

            //读取data数据
            byte[] data = new byte[length];
            channelBuffer.readBytes(data);

            //解析出消息对象,往下传递(handler)
            return new Response(module,cmd,data,code);
        }

        //数据包不完整,需要等待后面的数据包
        return null;
    }

}

ResponseEncoder.java
package com.chat.common.core.coder;

import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Response;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;

/**
 * 响应编码器
 * 数据包格式(根据需求定义)
 * 包头 模块号 命令号 长度 数据 
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 * @author hzk
 * @date 2018/9/29
 */
public class ResponseEncoder extends OneToOneEncoder{

    @Override
    protected Object encode(ChannelHandlerContext channelHandlerContext, Channel channel, Object rs) throws Exception {
        Response response = (Response) rs;
        System.out.println("ResponseEncoder response:" + rs.toString());

        ChannelBuffer channelBuffer = ChannelBuffers.dynamicBuffer();
        //包头
        channelBuffer.writeInt(Constants.AbstractDataStructure.PACKAGE_HEAD);
        //模块Module
        channelBuffer.writeShort(response.getModule());
        //命令号cmd
        channelBuffer.writeShort(response.getCmd());
        //状态
        channelBuffer.writeInt(response.getCode());
        //数据长度
        channelBuffer.writeInt(response.getDataLength());
        //数据
        if(null != response.getData()){
            channelBuffer.writeBytes(response.getData());
        }
        return channelBuffer;
    }
}

下面这几个是我们整个项目中需要用到的常量数据以及响应数据用到的枚举

Constants.java
package com.chat.common.core.constants;

/**
 * 常量
 * @author hzk
 * @date 2018/9/29
 */
public class Constants {

    /**
     * netty配置相关
     */
    public abstract static class AbstractNettyConfig{
        /**
         * 端口
         */
        public static final int PORT = 8888;
        /**
         * IP
         */
        public static final String ADDRESS = "127.0.0.1";
    }

    /**
     * 自定义数据结构相关
     */
    public abstract static class AbstractDataStructure{
        /**
         * 包头
         */
        public static final int PACKAGE_HEAD = -37593513;
        /**
         * 数据包基本长度(Request)
         * 4 + 2 + 2 + 4
         */
        public static final int DATA_STRUCTURE_LENGTH = 12;
        /**
         * 数据包基本长度(Response) 比request多一个响应码(int) 之前在ResponseDecoder内部写死传递,现通过接收上一级传递解(译)码
         * 4 + 2 + 4 + 2 + 4
         */
        public static final int DATA_RESPONSE_STRUCTURE_LENGTH = 16;
    }

    /**
     * 模块相关
     */
    public abstract static class AbstractModule{
        /**
         * 用户/玩家模块
         */
        public static final short PLAYER = 1;
        /**
         * 聊天模块
         */
        public static final short CHAT = 2;
    }

    /**
     * 用户/玩家命令相关
     */
    public abstract static class AbstractCmdPlayer{
        /**
         * 创建并登陆
         */
        public static final short REGISTER_AND_LOGIN = 1;
        /**
         * 登陆
         */
        public static final short LOGIN = 2;
    }

    /**
     * 聊天命令相关
     */
    public abstract static class AbstractCmdChat{

        /**
         * 广播消息
         */
        public static final short PUBLIC_CHAT = 1;
        /**
         * 私人消息
         */
        public static final short PRIVATE_CHAT = 2;
        /**
         * 推送消息
         */
        public static final short PUSH_CHAT = 101;
    }

    /**
     * 聊天类型相关
     */
    public abstract static class AbstractChatType{
        /**
         * 广播消息
         */
        public static final byte PUBLIC_CHAT = 0;
        /**
         * 私人消息
         */
        public static final byte PRIVATE_CHAT = 1;
    }

}

ResultCodeEnum.java
package com.chat.common.core.constants;

import org.apache.log4j.Logger;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;

/**
 * 响应码枚举类
 * @author hzk
 */
public enum ResultCodeEnum {

	/**
	 * 成功
	 */
	SUCCESS(1000,"success"),

	/**
	 * 找不到命令
	 */
	NO_INVOKER(2000,"no_invoker"),

	/**
	 * 参数异常
	 */
	PARAM_ERROR(2001,"param_error"),

	/**
	 * 未知异常
	 */
	UNKNOWN_EXCEPTION(2002,"unknown_exception"),

	/**
	 * 用户名或密码不能为空
	 */
	PLAYER_NAME_NULL(2003,"player_name_null"),

	/**
	 * 用户名已使用
	 */
	PLAYER_EXIST(2004,"player_exist"),

	/**
	 * 用户不存在
	 */
	PLAYER_NO_EXIST(2005,"player_no_exist"),

	/**
	 * 密码错误
	 */
	PASSWORD_ERROR(2006,"password_error"),

	/**
	 * 已经登录
	 */
	ALREADY_LOGIN(2007,"already_login"),

	/**
	 * 登录失败
	 */
	LOGIN_FAIL(2008,"login_fail"),

	/**
	 * 用户不在线
	 */
	PLAYER_NO_ONLINE(2009,"player_not_online"),

	/**
	 * 未登录
	 */
	NOT_LOGIN(2010,"not_login"),

	/**
	 * 不能以自己为私聊对象
	 */
	CANNOT_CHAT_YOURSELF(2011,"can't chat with yourself"),

	/**
	 * 参数检验失败
	 */
	PARAMETER_AUTHORIZATION_FAILED(4001,"parameter_authorization_failed"),
	/**
	 * 运行时异常
	 */
	RuntimeException(4002,"runtime_exception"),
	/**
	 * 空指针异常
	 */
	NullPointerException(4003,"null_pointer_exception"),
	/**
	 * 类型转换异常
	 */
	ClassCastException(4004,"class_cast_exception"),
	/**
	 * IO异常
	 */
	IOException(4005,"io_exception"),
	/**
	 * 未知方法异常
	 */
	NoSuchMethodException(4006,"no_such_method_exception"),
	/**
	 * 数组越界异常
	 */
	IndexOutOfBoundsException(4007,"index_out_of_bounds_exception"),
	/**
	 * 400错误
	 */
	HttpMessageNotReadableException(4008,"http_message_not_readable_exception"),
	/**
	 * 400错误
	 */
	MissingServletRequestParameterException(4009,"miss_servlet_request_parameter_exception"),
	/**
	 * 405错误
	 */
	HttpRequestMethodNotSupportedException(4010,"http_request_method_not_supported_exception"),
	/**
	 * 406错误
	 */
	HttpMediaTypeNotAcceptableException(4011,"http_media_type_not_acceptable_exception"),
	/**
	 * 500错误
	 */
	ConversionNotSupportedException(4012,"conversion_not_supported_exception"),
	/**
	 * 响应码未定义
	 */
	NO_CODE(4013,"no_code"),
    ;

	private static Logger log = Logger.getLogger(ResultCodeEnum.class);
	private int resultCode;
	private String resultInfo;
	private static Map< Integer, ResultCodeEnum> resultCodeMap = new HashMap<Integer, ResultCodeEnum>();
	private static Semaphore semaphore = new Semaphore(1);

	public int getResultCode() {
		return resultCode;
	}

	public String getResultInfo() {
		return resultInfo;
	}

	private ResultCodeEnum(int resultCode, String resultInfo){
		this.resultCode = resultCode;
		this.resultInfo = resultInfo;
	}
	
	public static String getResultInfo(int resultCode){
		if(resultCodeMap.isEmpty()){
			try {
				semaphore.acquire();
				initCodeInfoMap();
			} catch (InterruptedException e) {
				log.error("getResultInfo,err:"+e.getMessage());
			}finally{
				semaphore.release();
			}
		}
		ResultCodeEnum resultCodeEnum = resultCodeMap.get(new Integer(resultCode));
		return resultCodeEnum!=null?resultCodeEnum.getResultInfo():NO_CODE.getResultInfo();
	}
	
	private static void initCodeInfoMap(){
		if(resultCodeMap.isEmpty()){
			for(ResultCodeEnum resultCodeEnum:ResultCodeEnum.values()){
				resultCodeMap.put(resultCodeEnum.getResultCode(), resultCodeEnum);
			}
		}
	}
	
	public static void main(String args[]){
		System.out.println(ResultCodeEnum.PLAYER_EXIST.getResultCode());
		System.out.println(ResultCodeEnum.PLAYER_EXIST.getResultInfo());
		for(ResultCodeEnum resultCodeEnum : ResultCodeEnum.values()){
			System.out.println(resultCodeEnum.getResultCode()+" "+resultCodeEnum.getResultInfo());
		}
		
	}
	
}

SwingConstants.java
package com.chat.common.core.constants;

/**
 * Swing常量
 * @author hzk
 * @date 2018/10/25
 */
public class SwingConstants {

    /**
     * 按键命令
     */
    public abstract static class AbstractButtonCommand{
        /**
         * 注册
         */
        public static final String REGISTER = "REGISTER";

        /**
         * 登录
         */
        public static final String LOGIN = "LOGIN";

        /**
         * 发送
         */
        public static final String SEND = "SEND";
    }


}

用于包装以及抛出错误码的异常

ErrorCodeException.java
package com.chat.common.core.exception;

/**
 * @author hzk
 * @date 2018/10/22
 */
public class ErrorCodeException extends RuntimeException{

    private final int errorCode;

    public int getErrorCode(){
        return errorCode;
    }

    public ErrorCodeException(int errorCode){
        this.errorCode = errorCode;
    }
}

Client客户端和Server服务端交互时用来传递数据的请求、响应结果对象

Request.java
package com.chat.common.core.model;

/**
 * 请求对象
 * @author hzk
 * @date 2018/9/29
 */
public class Request {

    /**
     * 请求模块号
     */
    private short module;

    /**
     * 请求命令号
     */
    private short cmd;

    /**
     * 数据部分
     */
    private byte[] data;

    public Request(short module, short cmd, byte[] data) {
        this.module = module;
        this.cmd = cmd;
        this.data = data;
    }

    public short getModule() {
        return module;
    }

    public void setModule(short module) {
        this.module = module;
    }

    public short getCmd() {
        return cmd;
    }

    public void setCmd(short cmd) {
        this.cmd = cmd;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

    public int getDataLength(){
        if(null == data){
            return 0;
        }
        return data.length;
    }

    public static Request valueOf(short module,short cmd,byte[] data){
        return new Request(module,cmd,data);
    }

}

Response.java
package com.chat.common.core.model;

import java.util.Arrays;

/**
 * 响应对象
 * @author hzk
 * @date 2018/9/29
 */
public class Response {

    /**
     * 请求模块
     */
    private short module;

    /**
     * 请求命令号
     */
    private short cmd;

    /**
     * 数据部分
     */
    private byte[] data;

    /**
     * 状态码
     */
    private int code;

    public Response(short module, short cmd, byte[] data, int code) {
        this.module = module;
        this.cmd = cmd;
        this.data = data;
        this.code = code;
    }

    public Response(Request request){
        this.module = request.getModule();
        this.cmd = request.getCmd();
    }

    public short getModule() {
        return module;
    }

    public void setModule(short module) {
        this.module = module;
    }

    public short getCmd() {
        return cmd;
    }

    public void setCmd(short cmd) {
        this.cmd = cmd;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public int getDataLength(){
        if(null == data){
            return 0;
        }
        return data.length;
    }

    @Override
    public String toString() {
        return "Response{" +
                "module=" + module +
                ", cmd=" + cmd +
                ", data=" + Arrays.toString(data) +
                ", code=" + code +
                '}';
    }
}

Result.java
package com.chat.common.core.model;

import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.serializable.Serializable;

/**
 * 结果对象
 * @author hzk
 * @date 2018/10/23
 */
public class Result<T extends Serializable>{

    /**
     * 结果码
     */
    private int resultCode;

    /**
     * 结果内容
     */
    private T content;

    public static <T extends Serializable> Result<T> success(T content){
        Result<T> tResult = new Result<>();
        tResult.resultCode = ResultCodeEnum.SUCCESS.getResultCode();
        tResult.content = content;
        return tResult;
    }

    public static <T extends Serializable> Result<T> success(){
        Result<T> tResult = new Result<>();
        tResult.resultCode = ResultCodeEnum.SUCCESS.getResultCode();
        return tResult;
    }

    public static <T extends Serializable> Result<T> error(int resultCode){
        Result<T> tResult = new Result<>();
        tResult.setResultCode(resultCode);
        return tResult;
    }

    public static <T extends Serializable> Result<T> valueOf(int resultCode,T content){
        Result<T> tResult = new Result<>();
        tResult.resultCode = resultCode;
        tResult.content = content;
        return tResult;
    }

    public int getResultCode() {
        return resultCode;
    }

    public void setResultCode(int resultCode) {
        this.resultCode = resultCode;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    /**
     * 判断状态码是否成功
     * @return
     */
    public boolean isSuccess(){
        return this.resultCode == ResultCodeEnum.SUCCESS.getResultCode();
    }
}

下面这几个有助于我们通过扫描自定义注解更加有效地管理不同模块不同命令号的handler

Invoker.java
package com.chat.common.core.scanner;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 命令执行器/调用器
 * @author hzk
 * @date 2018/10/25
 */
public class Invoker {

    /**
     * 方法
     */
    private Method method;

    /**
     * 目标对象
     */
    private Object target;

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Object getTarget() {
        return target;
    }

    public void setTarget(Object target) {
        this.target = target;
    }

    public Invoker(Method method, Object target) {
        this.method = method;
        this.target = target;
    }

    /**
     * 执行
     * @param paramValues
     * @return
     */
    public Object invoke(Object... paramValues){
        try {
            return method.invoke(target,paramValues);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e){
            e.printStackTrace();
        }
        return null;
    }

    public static Invoker valueOf(Method method,Object target){
        return new Invoker(method,target);
    }

}

InvokerHolder.java
package com.chat.common.core.scanner;

import java.util.HashMap;
import java.util.Map;

/**
 * 命令执行器管理者
 * @author hzk
 * @date 2018/10/25
 */
public class InvokerHolder {

    /**
     * 命令调用器
     */
    private static Map<Short,Map<Short,Invoker>> invokers = new HashMap<>();

    /**
     * 添加命令调用器
     * @param module 模块号
     * @param cmd 命令号
     * @param invoker 调用器
     */
    public static void addInvoker(short module,short cmd,Invoker invoker){
        Map<Short, Invoker> invokerMap = invokers.get(module);
        if(null == invokerMap){
            invokerMap = new HashMap<>();
            invokers.put(module,invokerMap);
        }
        invokerMap.put(cmd,invoker);
    }

    /**
     * 获取命令调用器
     * @param module 模块号
     * @param cmd 命令号
     * @return {@link Invoker}
     */
    public static Invoker getInvoker(short module,short cmd){
        Map<Short, Invoker> invokerMap = invokers.get(module);
        if(null == invokerMap){
            return null;
        }
        return invokerMap.get(cmd);
    }
}

HandlerScanner.java
package com.chat.common.core.scanner;

import com.chat.common.core.annotation.SocketCommand;
import com.chat.common.core.annotation.SocketModule;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * Handler扫描器
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class HandlerScanner implements BeanPostProcessor{

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<? extends Object> clazz = bean.getClass();

        Class<?>[] interfaces = clazz.getInterfaces();
        if(null != interfaces && interfaces.length >0){
            //扫描类的所有接口父类
            for (Class<?> anInterface : interfaces) {
                //判断是否为Handler接口类
                SocketModule moduleAnnotation = anInterface.getAnnotation(SocketModule.class);
                if(null == moduleAnnotation){
                    continue;
                }

                //找出命令方法
                Method[] methods = anInterface.getMethods();
                if(null != methods && methods.length > 0){
                    for (Method method : methods) {
                        SocketCommand cmdAnnotation = method.getAnnotation(SocketCommand.class);
                        if(null == cmdAnnotation){
                            continue;
                        }

                        short module = moduleAnnotation.module();
                        short cmd = cmdAnnotation.cmd();
                        
                        if(null == InvokerHolder.getInvoker(module,cmd)){
                            InvokerHolder.addInvoker(module,cmd,Invoker.valueOf(method,bean));
                        }else{
                            System.out.println("Repeat Module:" + module + ",Cmd:" + cmd);
                        }
                    }
                }
            }
        }

        return bean;
    }
}

当然少不了我们传输数据过程中自定义序列化的工具,也是在之前大家就熟悉的

BufferFactory.java
package com.chat.common.core.serializable;


import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;

import java.nio.ByteOrder;

/**
 * ChannelBuffers工具类
 * @author hzk
 * @date 2018/9/26
 */
public class BufferFactory{

    public static ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN;

    /**
     * 获取一个ChannelBuffer
     * @return
     */
    public static ChannelBuffer getBuffer(){
        ChannelBuffer channelBuffer = ChannelBuffers.dynamicBuffer();
        return channelBuffer;
    }

    /**
     * 获取一个ChannelBuffer 并写入数据
     * @param bytes
     * @return
     */
    public static ChannelBuffer getBuffer(byte[] bytes){
        ChannelBuffer channelBuffer = ChannelBuffers.copiedBuffer(bytes);
        return channelBuffer;
    }


}

Serializable.java
package com.chat.common.core.serializable;

import org.jboss.netty.buffer.ChannelBuffer;

import java.nio.charset.Charset;
import java.util.*;

/**
 * 自定义序列化
 * @author hzk
 * @date 2018/9/26
 */
public abstract class Serializable {

    public static final Charset CHARSET = Charset.forName("UTF-8");

    protected ChannelBuffer writeBuffer;

    protected ChannelBuffer readBuffer;

    /**
     * 反序列化具体实现
     */
    protected abstract void read();

    /**
     * 序列化具体实现
     */
    protected abstract void write();

    /**
     * 从bytes数组读取数据
     * @param bytes
     * @return
     */
    public Serializable readFromBytes(byte[] bytes){
        readBuffer = BufferFactory.getBuffer(bytes);
        read();
        readBuffer.clear();
        return this;
    }

    /**
     * 从channelBuffer读取数据
     * @param channelBuffer
     */
    public void readFromBuffer(ChannelBuffer channelBuffer){
        this.readBuffer = channelBuffer;
        read();
    }

    /**
     * 写入到本地channelBuffer
     * @return
     */
    public ChannelBuffer writeToLocalBuffer(){
        this.writeBuffer = BufferFactory.getBuffer();
        write();
        return writeBuffer;
    }

    /**
     * 写入到目标channelBuffer
     * @param channelBuffer
     * @return
     */
    public ChannelBuffer writeToTargetBuffer(ChannelBuffer channelBuffer){
        this.writeBuffer = channelBuffer;
        write();
        return writeBuffer;
    }

    public Serializable writeByte(Byte value){
        writeBuffer.writeByte(value);
        return this;
    }

    public Serializable writeInt(int value){
        writeBuffer.writeInt(value);
        return this;
    }

    public Serializable writeShort(short value){
        writeBuffer.writeShort(value);
        return this;
    }

    public Serializable writeLong(long value){
        writeBuffer.writeLong(value);
        return this;
    }

    public Serializable writeFloat(float value){
        writeBuffer.writeFloat(value);
        return this;
    }

    public Serializable writeDouble(double value){
        writeBuffer.writeDouble(value);
        return this;
    }

    public Serializable writeString(String value){
        if(null == value || value.isEmpty()){
            writeShort((short)0);
            return this;
        }
        byte[] bytes = value.getBytes(CHARSET);
        short size = (short) bytes.length;
        writeBuffer.writeShort(size);
        writeBuffer.writeBytes(bytes);
        return this;
    }

    public Serializable writeObject(Object object){
        if(null == object){
            writeByte((byte)0);
        }else{
            if(object instanceof Integer){
                writeInt((int)object);
            }else if(object instanceof Short){
                writeShort((short)object);
            }else if(object instanceof Byte){
                writeByte((byte)object);
            }else if(object instanceof Long){
                writeLong((long)object);
            }else if(object instanceof Float){
                writeFloat((float)object);
            }else if(object instanceof Double){
                writeDouble((double)object);
            }else if(object instanceof String){
                writeString((String) object);
            }else if(object instanceof Serializable){
                writeByte((byte)1);
                Serializable serializable = (Serializable) object;
                serializable.writeToTargetBuffer(writeBuffer);
            }else{
                throw new RuntimeException("不可序列化类型:[%s]"+object.getClass());
            }
        }
        return this;
    }

    public <T> Serializable writeList(List<T> list){
        if(isEmpty(list)){
            writeBuffer.writeShort((short)0);
            return this;
        }
        writeBuffer.writeShort((short)list.size());
        for(T t:list){
            writeObject(t);
        }
        return this;
    }

    public <K,V> Serializable writeMap(Map<K,V> map){
        if(isEmpty(map)){
            writeBuffer.writeShort((short)0);
            return this;
        }
        writeBuffer.writeShort((short)map.size());
        for (Map.Entry<K,V> entry:map.entrySet()) {
            writeObject(entry.getKey());
            writeObject(entry.getValue());
        }
        return this;
    }


    /**
     * 返回byte数组
     * @return
     */
    public byte[] getBytes(){
        writeToLocalBuffer();
        byte[] bytes = null;
        if(writeBuffer.writerIndex() == 0){
            bytes = new byte[0];
        }else{
            bytes = new byte[writeBuffer.writerIndex()];
            writeBuffer.readBytes(bytes);
        }
        writeBuffer.clear();
        return bytes;
    }

    public byte readByte(){
        return readBuffer.readByte();
    }

    public short readShort(){
        return readBuffer.readShort();
    }

    public int readInt(){
        return readBuffer.readInt();
    }

    public long readLong(){
        return readBuffer.readLong();
    }

    public float readFloat(){
        return readBuffer.readFloat();
    }

    public double readDouble(){
        return readBuffer.readDouble();
    }

    public String readString(){
        short size = readBuffer.readShort();
        if(size <= 0){
            return "";
        }
        byte[] bytes = new byte[size];
        readBuffer.readBytes(bytes);
        return new String(bytes,CHARSET);
    }

    public <K> K readObject(Class<K> clz){
        Object k = null;
        if(clz == int.class || clz == Integer.class){
            k = readInt();
        }else if(clz == byte.class || clz == Byte.class){
            k = readByte();
        }else if(clz == short.class || clz == Short.class){
            k = readShort();
        }else if(clz == long.class || clz == Long.class){
            k = readLong();
        }else if(clz == float.class || clz == Float.class){
            k = readFloat();
        }else if(clz == double.class || clz == Double.class){
            k = readDouble();
        }else if(clz == String.class){
            k = readString();
        }else if(Serializable.class.isAssignableFrom(clz)){
            try {
                byte hasObject = readBuffer.readByte();
                if(hasObject == 1){
                    Serializable temp = (Serializable) clz.newInstance();
                    temp.readFromBuffer(readBuffer);
                    k = temp;
                }else{
                    k = null;
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }else{
            throw new RuntimeException(String.format("不支持类型:[%s]",clz));
        }
        return (K)k;
    }

    public <T> List<T> readList(Class<T> clz){
        ArrayList<T> list = new ArrayList<>();
        short size = readBuffer.readShort();
        for(int i=0;i<size;i++){
            list.add(readObject(clz));
        }
        return list;
    }

    public <K,V> Map<K,V> readMap(Class<K> keyClz,Class<V> valueClz){
        HashMap<K, V> map = new HashMap<>();
        short size = readBuffer.readShort();
        for (int i =0;i<size;i++){
            K key = readObject(keyClz);
            V value = readObject(valueClz);
            map.put(key,value);
        }
        return map;
    }

    private <T> boolean isEmpty(Collection<T> c) {
        return c == null || c.isEmpty();
    }
    public <K,V> boolean isEmpty(Map<K,V> c) {
        return c == null || c.isEmpty();
    }
}

每一个连接之间都会建立一个独有的channel,我们这里用自定义会话将每个用户和其对应的channel绑定并且记录起来,以供发送消息的时候可以更方便定位其他用户

Session.java
package com.chat.common.core.session;

/**
 * 会话抽象接口
 * @author hzk
 * @date 2018/10/23
 */
public interface Session {

    /**
     * 会话绑定对象
     * @return
     */
    Object getAttachment();

    /**
     * 绑定会话对象
     */
    void setAttachment(Object attachment);

    /**
     * 移除会话对象
     */
    void removeAttachment();

    /**
     * 写入消息到会话
     *  @param msg 消息
     */
    void write(Object msg);

    /**
     * 判断会话是否在连接中
     * @return
     */
    boolean isConnected();

    /**
     * 关闭会话
     */
    void close();

}

SessionImpl.java
package com.chat.common.core.session;


import org.jboss.netty.channel.Channel;

/**
 * 会话实现封装类
 * @author hzk
 * @date 2018/10/23
 */
public class SessionImpl implements Session{

    /**
     * 实际会话对象
     */
    private Channel channel;

    public SessionImpl(Channel channel) {
        this.channel = channel;
    }

    @Override
    public Object getAttachment() {
        return channel.getAttachment();
    }

    @Override
    public void setAttachment(Object attachment) {
        channel.setAttachment(attachment);
    }

    @Override
    public void removeAttachment() {
        channel.setAttachment(null);
    }

    @Override
    public void write(Object msg) {
        channel.write(msg);
    }

    @Override
    public boolean isConnected() {
        return channel.isConnected();
    }

    @Override
    public void close() {
        channel.close();
    }
}

SessionManager.java
package com.chat.common.core.session;

import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.model.Response;
import com.chat.common.core.serializable.Serializable;
import com.google.protobuf.GeneratedMessage;
import org.jboss.netty.util.internal.ConcurrentHashMap;

import java.util.Collections;
import java.util.Set;

/**
 * 会话管理者
 * @author hzk
 * @date 2018/10/23
 */
public class SessionManager {

    /**
     * 在线会话组
     */
    private static final ConcurrentHashMap<Long,Session> onlineSessions = new ConcurrentHashMap<>();

    /**
     * 将当前用户加入在线会话组
     * @param playerId 用户ID
     * @param session 当前用户会话
     * @return 是否加入成功
     */
    public static boolean putSession(long playerId,Session session){
        if(!onlineSessions.contains(playerId)){
            //如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null
            return onlineSessions.putIfAbsent(playerId, session) == null ? true : false;
        }
        return false;
    }

    /**
     * 移除当前用户
     * @param playerId 用户ID
     * @return 是否移除成功
     */
    public static Session removeSession(long playerId){
        return onlineSessions.remove(playerId);
    }

    /**
     * 发送消息[自定义协议]
     * @param playerId 用户ID
     * @param module 模块号
     * @param cmd 命令号
     * @param message 发送消息
     * @param <T> 发送消息泛型
     */
    public static <T extends Serializable> void sendMessage(long playerId,short module,short cmd,T message){
        Session session = onlineSessions.get(playerId);
        if(null != session && session.isConnected()){
            Response response = new Response(module, cmd, message.getBytes(), ResultCodeEnum.SUCCESS.getResultCode());
            session.write(response);
        }
    }

    /**
     * 发送消息[Protocol Buffers协议]
     * @param playerId 用户ID
     * @param module 模块号
     * @param cmd 命令号
     * @param message 发送消息
     * @param <T> 发送消息泛型
     */
    public static <T extends GeneratedMessage> void sendMessage(long playerId,short module,short cmd,T message){
        Session session = onlineSessions.get(playerId);
        if(null != session && session.isConnected()){
            Response response = new Response(module, cmd, message.toByteArray(), ResultCodeEnum.SUCCESS.getResultCode());
            session.write(response);
        }
    }

    /**
     * 检测用户是否在线
     * @param playerId 用户ID
     * @return
     */
    public static boolean isOnlinePlayer(long playerId){
        return onlineSessions.containsKey(playerId);
    }

    /**
     * 获取所有在线用户ID
     * @return
     */
    public static Set<Long> getOnlinePlayers(){
        return Collections.unmodifiableSet(onlineSessions.keySet());
    }
}

接下来几个是和数据库交互相关的工具类以及操作类,这里我们建立的用户表仅仅也只有简单的几个字段数据,只为了走通整个流程,利用MybatisGenerator生成的部分就不贴出来了。

MapperBeanNameGenerator.java
package com.chat.common.core.utils;


import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;

public class MapperBeanNameGenerator implements BeanNameGenerator {

    @Override
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
        String beanClass = definition.getBeanClassName();
        int w = beanClass.indexOf(".write.");

        int r = beanClass.indexOf(".read.");
        return (w > 0) ? beanClass.substring(w + 1) : beanClass.substring(r + 1);
    }
}

Player.java
package com.chat.common.entity;

public class Player {
    /**
     * This field was generated by MyBatis Generator.
     * This field corresponds to the database column player.id
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    private Long id;

    /**
     * This field was generated by MyBatis Generator.
     * This field corresponds to the database column player.name
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    private String name;

    /**
     * This field was generated by MyBatis Generator.
     * This field corresponds to the database column player.password
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    private String password;

    /**
     * This field was generated by MyBatis Generator.
     * This field corresponds to the database column player.level
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    private Integer level;

    /**
     * This field was generated by MyBatis Generator.
     * This field corresponds to the database column player.exp
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    private Integer exp;

    /**
     * This method was generated by MyBatis Generator.
     * This method returns the value of the database column player.id
     *
     * @return the value of player.id
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public Long getId() {
        return id;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method sets the value of the database column player.id
     *
     * @param id the value for player.id
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method returns the value of the database column player.name
     *
     * @return the value of player.name
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public String getName() {
        return name;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method sets the value of the database column player.name
     *
     * @param name the value for player.name
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method returns the value of the database column player.password
     *
     * @return the value of player.password
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public String getPassword() {
        return password;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method sets the value of the database column player.password
     *
     * @param password the value for player.password
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public void setPassword(String password) {
        this.password = password == null ? null : password.trim();
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method returns the value of the database column player.level
     *
     * @return the value of player.level
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public Integer getLevel() {
        return level;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method sets the value of the database column player.level
     *
     * @param level the value for player.level
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public void setLevel(Integer level) {
        this.level = level;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method returns the value of the database column player.exp
     *
     * @return the value of player.exp
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public Integer getExp() {
        return exp;
    }

    /**
     * This method was generated by MyBatis Generator.
     * This method sets the value of the database column player.exp
     *
     * @param exp the value for player.exp
     *
     * @mbggenerated Tue Oct 23 17:55:52 CST 2018
     */
    public void setExp(Integer exp) {
        this.exp = exp;
    }
}
PlayerMapper.java
package com.chat.common.dao.read;

import com.chat.common.entity.Player;
import com.chat.common.entity.PlayerExample;

import java.util.List;

public interface PlayerMapper {

    int countByExample(PlayerExample example);

    List<Player> selectByExample(PlayerExample example);

    Player selectByPrimaryKey(Long id);

}
PlayerMapper.java
package com.chat.common.dao.write;

import com.chat.common.entity.Player;
import com.chat.common.entity.PlayerExample;
import org.apache.ibatis.annotations.Param;

public interface PlayerMapper {

    int deleteByExample(PlayerExample example);

    int deleteByPrimaryKey(Long id);

    int insert(Player record);

    int insertSelective(Player record);

    int updateByExampleSelective(@Param("record") Player record, @Param("example") PlayerExample example);

    int updateByExample(@Param("record") Player record, @Param("example") PlayerExample example);

    int updateByPrimaryKeySelective(Player record);

    int updateByPrimaryKey(Player record);
}

上面贴出了请求响应结果数据的封装类,这里几个是我们执行特定模块号命令号所需要用到的具体数据

PrivateChatRequest.java
package com.chat.common.module.chat.request;

import com.chat.common.core.serializable.Serializable;

/**
 * 私聊消息请求
 * @author hzk
 * @date 2018/10/23
 */
public class PrivateChatRequest extends Serializable{

    /**
     * 发送目标用户ID
     */
    private long targetPlayerId;

    /**
     * 内容
     */
    private String content;

    public long getTargetPlayerId() {
        return targetPlayerId;
    }

    public void setTargetPlayerId(long targetPlayerId) {
        this.targetPlayerId = targetPlayerId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    protected void read() {
        this.targetPlayerId = readLong();
        this.content = readString();
    }

    @Override
    protected void write() {
        writeLong(targetPlayerId);
        writeString(content);
    }
}

PublicChatRequest.java
package com.chat.common.module.chat.request;

import com.chat.common.core.serializable.Serializable;

/**
 * 广播信息请求
 * @author hzk
 * @date 2018/10/23
 */
public class PublicChatRequest extends Serializable{

    /**
     * 内容
     */
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    protected void read() {
        this.content = readString();
    }

    @Override
    protected void write() {
        writeString(content);
    }
}

ChatResponse.java
package com.chat.common.module.chat.response;

import com.chat.common.core.serializable.Serializable;

/**
 * 聊天消息响应
 * @author hzk
 * @date 2018/10/23
 */
public class ChatResponse extends Serializable{

    /**
     * 发送用户ID
     */
    private long sendPlayerId;

    /**
     * 发送用户名称
     */
    private String sendPlayerName;

    /**
     * 目标用户名称
     */
    private String targetPlayerName;

    /**
     * 消息类型
     * 0 广播类型
     * 1 私聊
     * {@link com.chat.common.core.constants.Constants.AbstractChatType}
     */
    private byte chatType;

    /**
     * 消息
     */
    private String message;

    public long getSendPlayerId() {
        return sendPlayerId;
    }

    public void setSendPlayerId(long sendPlayerId) {
        this.sendPlayerId = sendPlayerId;
    }

    public String getSendPlayerName() {
        return sendPlayerName;
    }

    public void setSendPlayerName(String sendPlayerName) {
        this.sendPlayerName = sendPlayerName;
    }

    public String getTargetPlayerName() {
        return targetPlayerName;
    }

    public void setTargetPlayerName(String targetPlayerName) {
        this.targetPlayerName = targetPlayerName;
    }

    public byte getChatType() {
        return chatType;
    }

    public void setChatType(byte chatType) {
        this.chatType = chatType;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    protected void read() {
        this.sendPlayerId = readLong();
        this.sendPlayerName = readString();
        this.targetPlayerName = readString();
        this.chatType = readByte();
        this.message = readString();
    }

    @Override
    protected void write() {
        writeLong(sendPlayerId);
        writeString(sendPlayerName);
        writeString(targetPlayerName);
        writeByte(chatType);
        writeString(message);
    }
}

LoginRequest.java
package com.chat.common.module.player.request;

import com.chat.common.core.serializable.Serializable;

/**
 * 登录请求
 * @author hzk
 * @date 2018/10/23
 */
public class LoginRequest extends Serializable{

    /**
     * 用户名
     */
    private String playerName;

    /**
     * 密码
     */
    private String password;

    public String getPlayerName() {
        return playerName;
    }

    public void setPlayerName(String playerName) {
        this.playerName = playerName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    protected void read() {
        this.playerName = readString();
        this.password = readString();
    }

    @Override
    protected void write() {
        writeString(playerName);
        writeString(password);
    }
}

RegisterRequest.java
package com.chat.common.module.player.request;

import com.chat.common.core.serializable.Serializable;

/**
 * 注册请求
 * @author hzk
 * @date 2018/10/23
 */
public class RegisterRequest extends Serializable {

    /**
     * 用户名
     */
    private String playerName;

    /**
     * 密码
     */
    private String password;

    public String getPlayerName() {
        return playerName;
    }

    public void setPlayerName(String playerName) {
        this.playerName = playerName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    protected void read() {
        this.playerName = readString();
        this.password = readString();
    }

    @Override
    protected void write() {
        writeString(playerName);
        writeString(password);
    }
}

PlayerResponse.java
package com.chat.common.module.player.response;

import com.chat.common.core.serializable.Serializable;

/**
 * 用户信息响应
 * @author hzk
 * @date 2018/10/23
 */
public class PlayerResponse extends Serializable{

    /**
     * 用户ID
     */
    private Long playerId;

    /**
     * 用户名
     */
    private String playerName;

    /**
     * 等级
     */
    private Integer level;

    /**
     * 经验值
     */
    private Integer exp;

    public Long getPlayerId() {
        return playerId;
    }

    public void setPlayerId(Long playerId) {
        this.playerId = playerId;
    }

    public String getPlayerName() {
        return playerName;
    }

    public void setPlayerName(String playerName) {
        this.playerName = playerName;
    }

    public Integer getLevel() {
        return level;
    }

    public void setLevel(Integer level) {
        this.level = level;
    }

    public Integer getExp() {
        return exp;
    }

    public void setExp(Integer exp) {
        this.exp = exp;
    }

    @Override
    protected void read() {
        this.playerId = readLong();
        this.playerName = readString();
        this.level = readInt();
        this.exp = readInt();
    }

    @Override
    protected void write() {
        writeLong(playerId);
        writeString(playerName);
        writeInt(level);
        writeInt(exp);
    }
}

聊天以及用户行为操作的业务处理类

ChatService.java
package com.chat.common.service;

/**
 * 聊天服务
 * @author hzk
 * @date 2018/10/24
 */
public interface ChatService {

    /**
     * 广播消息(群发)
     * @param playerId 用户ID
     * @param content 消息内容
     */
    public void publicChat(long playerId,String content);

    /**
     * 私聊
     * @param playerId 用户ID
     * @param targetPlayerId 目标用户ID
     * @param content 消息内容
     */
    public void privateChat(long playerId,long targetPlayerId,String content);

}

ChatServiceImpl.java
package com.chat.common.service.impl;

import com.chat.common.core.constants.Constants;
import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.exception.ErrorCodeException;
import com.chat.common.core.session.SessionManager;
import com.chat.common.entity.Player;
import com.chat.common.module.chat.response.ChatResponse;
import com.chat.common.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.Set;

/**
 * 聊天服务
 * @author hzk
 * @date 2018/10/24
 */
@Component
public class ChatServiceImpl implements ChatService{

    @Autowired
    private com.chat.common.dao.read.PlayerMapper playerMapper_r;

    @Override
    public void publicChat(long playerId, String content) {
        Player player = playerMapper_r.selectByPrimaryKey(playerId);

        //获取所有在线玩家
        Set<Long> onlinePlayers = SessionManager.getOnlinePlayers();

        //创建消息对象
        ChatResponse chatResponse = new ChatResponse();
        chatResponse.setSendPlayerId(playerId);
        chatResponse.setSendPlayerName(player.getName());
        chatResponse.setMessage(content);
        chatResponse.setChatType(Constants.AbstractChatType.PUBLIC_CHAT);

        for (Long onlinePlayer : onlinePlayers) {
            SessionManager.sendMessage(onlinePlayer,Constants.AbstractModule.CHAT,Constants.AbstractCmdChat.PUSH_CHAT,chatResponse);
        }
    }

    @Override
    public void privateChat(long playerId, long targetPlayerId, String content) {
        //检测是否私聊对象为自己
        if(playerId == targetPlayerId){
            throw new ErrorCodeException(ResultCodeEnum.CANNOT_CHAT_YOURSELF.getResultCode());
        }

        Player player = playerMapper_r.selectByPrimaryKey(playerId);

        Player targetPlayer = playerMapper_r.selectByPrimaryKey(targetPlayerId);
        //检测目标用户是否存在
        if(null == targetPlayer){
            throw new ErrorCodeException(ResultCodeEnum.PLAYER_NO_EXIST.getResultCode());
        }

        //检测目标用户是否在线
        if(!SessionManager.isOnlinePlayer(targetPlayerId)){
            throw new ErrorCodeException(ResultCodeEnum.PLAYER_NO_ONLINE.getResultCode());
        }

        //创建消息对象
        ChatResponse chatResponse = new ChatResponse();
        chatResponse.setSendPlayerId(playerId);
        chatResponse.setSendPlayerName(player.getName());
        chatResponse.setTargetPlayerName(targetPlayer.getName());
        chatResponse.setMessage(content);
        chatResponse.setChatType(Constants.AbstractChatType.PRIVATE_CHAT);

        //给目标用户发送信息
        SessionManager.sendMessage(targetPlayerId,Constants.AbstractModule.CHAT,Constants.AbstractCmdChat.PUSH_CHAT,chatResponse);
        //给自己回一个信息
        SessionManager.sendMessage(playerId,Constants.AbstractModule.CHAT,Constants.AbstractCmdChat.PUSH_CHAT,chatResponse);
    }
}

PlayerService.java
package com.chat.common.service;

import com.chat.common.core.session.Session;
import com.chat.common.module.player.response.PlayerResponse;

/**
 * 玩家服务
 * @author hzk
 * @date 2018/10/24
 */
public interface PlayerService {

    /**
     * 注册并登录
     * @param session
     * @param playerName 用户名
     * @param password 密码
     * @return
     */
    public PlayerResponse registerAndLogin(Session session,String playerName,String password);

    /**
     * 登录
     * @param session
     * @param playerName 用户名
     * @param password 密码
     * @return
     */
    public PlayerResponse login(Session session,String playerName,String password);
}

PlayerServiceImpl.java
package com.chat.common.service.impl;

import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.exception.ErrorCodeException;
import com.chat.common.core.session.Session;
import com.chat.common.core.session.SessionManager;
import com.chat.common.entity.Player;
import com.chat.common.entity.PlayerExample;
import com.chat.common.module.player.response.PlayerResponse;
import com.chat.common.service.PlayerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 用户服务
 * @author hzk
 * @date 2018/10/24
 */
@Component
public class PlayerServiceImpl implements PlayerService{

    @Autowired
    private com.chat.common.dao.read.PlayerMapper playerMapper_r;

    @Autowired
    private com.chat.common.dao.write.PlayerMapper playerMapper_w;

    @Override
    public PlayerResponse registerAndLogin(Session session, String playerName, String password) {
        PlayerExample playerExample = new PlayerExample();
        playerExample.createCriteria().andNameEqualTo(playerName);
        int exist = playerMapper_r.countByExample(playerExample);
        if(exist > 0){
            //注册用户名已存在
            throw new ErrorCodeException(ResultCodeEnum.PLAYER_EXIST.getResultCode());
        }

        Player player = new Player();
        player.setName(playerName);
        player.setPassword(password);
        player.setLevel(1);
        player.setExp(0);
        int registerFlag = playerMapper_w.insertSelective(player);
        if(registerFlag <= 0){
            //注册失败
            throw new ErrorCodeException(ResultCodeEnum.UNKNOWN_EXCEPTION.getResultCode());
        }

        return login(session,playerName,password);
    }

    @Override
    public PlayerResponse login(Session session, String playerName, String password) {
        
        //检测当前会话是否已经登录
        if(session.getAttachment() != null){
            throw new ErrorCodeException(ResultCodeEnum.ALREADY_LOGIN.getResultCode());
        }

        //检测用户是否存在
        PlayerExample playerExample = new PlayerExample();
        playerExample.createCriteria().andNameEqualTo(playerName);
        List<Player> players = playerMapper_r.selectByExample(playerExample);
        if(null == players || players.isEmpty()){
            throw new ErrorCodeException(ResultCodeEnum.PLAYER_NO_EXIST.getResultCode());
        }

        //检测账户密码匹配
        Player player = players.get(0);
        if(!password.equals(player.getPassword())){
            throw new ErrorCodeException(ResultCodeEnum.PASSWORD_ERROR.getResultCode());
        }

        //检测是否在其他地方已经登录
        boolean onlinePlayer = SessionManager.isOnlinePlayer(player.getId());
        if(onlinePlayer){
            Session oldSession = SessionManager.removeSession(player.getId());
            oldSession.removeAttachment();
            //踢下线
            oldSession.close();
        }

        //加入在线玩家会话
        if(SessionManager.putSession(player.getId(),session)){
            session.setAttachment(player);
        }else{
            throw new ErrorCodeException(ResultCodeEnum.LOGIN_FAIL.getResultCode());
        }

        //创建Response传输对象返回
        PlayerResponse playerResponse = new PlayerResponse();
        playerResponse.setPlayerId(player.getId());
        playerResponse.setPlayerName(player.getName());
        playerResponse.setLevel(player.getLevel());
        playerResponse.setExp(player.getExp());
        return playerResponse;
    }
}
Server部分

首先是我们定义了模块号命令号用于业务的handler

ChatHandler.java
package com.chat.server.chat.handler;

import com.chat.common.core.annotation.SocketCommand;
import com.chat.common.core.annotation.SocketModule;
import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Result;

/**
 * 聊天处理
 * @author hzk
 * @date 2018/10/25
 */
@SocketModule(module = Constants.AbstractModule.CHAT)
public interface ChatHandler {

    /**
     *  广播消息(群发)
     * @param playerId 发送用户ID
     * @param data {@link com.chat.common.module.chat.request.PublicChatRequest}
     * @return
     */
    @SocketCommand(cmd = Constants.AbstractCmdChat.PUBLIC_CHAT)
    public Result<?> publicChat(long playerId,byte[] data);

    /**
     * 私聊消息
     * @param playerId 发送用户ID
     * @param data {@link com.chat.common.module.chat.request.PublicChatRequest}
     * @return
     */
    @SocketCommand(cmd = Constants.AbstractCmdChat.PRIVATE_CHAT)
    public Result<?> privateChat(long playerId,byte[] data);
}

ChatHandlerImpl.java
package com.chat.server.chat.handler.impl;

import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.exception.ErrorCodeException;
import com.chat.common.core.model.Result;
import com.chat.common.module.chat.request.PrivateChatRequest;
import com.chat.common.module.chat.request.PublicChatRequest;
import com.chat.common.service.ChatService;
import com.chat.server.chat.handler.ChatHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 消息处理实现类
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class ChatHandlerImpl implements ChatHandler{

    @Autowired
    private ChatService chatService;

    @Override
    public Result<?> publicChat(long playerId, byte[] data) {
        try {
            //反序列化
            PublicChatRequest publicChatRequest = new PublicChatRequest();
            publicChatRequest.readFromBytes(data);

            //参数校验
            if(StringUtils.isEmpty(publicChatRequest.getContent())){
                return Result.error(ResultCodeEnum.PARAM_ERROR.getResultCode());
            }

            //执行业务
            chatService.publicChat(playerId,publicChatRequest.getContent());
        }catch (ErrorCodeException e){
            return Result.error(e.getErrorCode());
        }
        catch (Exception e) {
            e.printStackTrace();
            return Result.error(ResultCodeEnum.UNKNOWN_EXCEPTION.getResultCode());
        }
        return Result.success();
    }

    @Override
    public Result<?> privateChat(long playerId, byte[] data) {
        try {
            //反序列化
            PrivateChatRequest privateChatRequest = new PrivateChatRequest();
            privateChatRequest.readFromBytes(data);

            //参数校验
            if(StringUtils.isEmpty(privateChatRequest.getContent()) || privateChatRequest.getTargetPlayerId() <= 0){
                return Result.error(ResultCodeEnum.PARAM_ERROR.getResultCode());
            }

            //执行业务
            chatService.privateChat(playerId,privateChatRequest.getTargetPlayerId(),privateChatRequest.getContent());
        }catch (ErrorCodeException e){
            return Result.error(e.getErrorCode());
        }
        catch (Exception e) {
            e.printStackTrace();
            return Result.error(ResultCodeEnum.UNKNOWN_EXCEPTION.getResultCode());
        }
        return Result.success();
    }
}

PlayerHandler.java
package com.chat.server.player.handler;

import com.chat.common.core.annotation.SocketCommand;
import com.chat.common.core.annotation.SocketModule;
import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Result;
import com.chat.common.core.session.Session;
import com.chat.common.module.player.response.PlayerResponse;

/**
 * 用户处理
 * @author hzk
 * @date 2018/10/25
 */
@SocketModule(module = Constants.AbstractModule.PLAYER)
public interface PlayerHandler {

    /**
     *  创建并登录账号
     * @param session 会话
     * @param data {@link com.chat.common.module.player.request.RegisterRequest}
     * @return
     */
    @SocketCommand(cmd = Constants.AbstractCmdPlayer.REGISTER_AND_LOGIN)
    public Result<PlayerResponse> registerAndLogin(Session session,byte[] data);

    /**
     * 登录账号
     * @param session 会话
     * @param data {@link com.chat.common.module.player.request.RegisterRequest}
     * @return
     */
    @SocketCommand(cmd = Constants.AbstractCmdPlayer.LOGIN)
    public Result<PlayerResponse> login(Session session,byte[] data);
}

PlayerHandlerImpl.java
package com.chat.server.player.handler.impl;

import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.exception.ErrorCodeException;
import com.chat.common.core.model.Result;
import com.chat.common.core.session.Session;
import com.chat.common.module.player.request.LoginRequest;
import com.chat.common.module.player.request.RegisterRequest;
import com.chat.common.module.player.response.PlayerResponse;
import com.chat.common.service.PlayerService;
import com.chat.server.player.handler.PlayerHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class PlayerHandlerImpl implements PlayerHandler{

    @Autowired
    private PlayerService playerService;

    @Override
    public Result<PlayerResponse> registerAndLogin(Session session, byte[] data) {
        PlayerResponse playerResponse = null;
        try {
            //反序列化
            RegisterRequest registerRequest = new RegisterRequest();
            registerRequest.readFromBytes(data);

            ///参数校验
            if(StringUtils.isEmpty(registerRequest.getPlayerName()) || StringUtils.isEmpty(registerRequest.getPassword())){
                return Result.error(ResultCodeEnum.PARAM_ERROR.getResultCode());
            }

            //执行业务
            playerResponse = playerService.registerAndLogin(session, registerRequest.getPlayerName(), registerRequest.getPassword());
        }catch (ErrorCodeException e){
            return Result.error(e.getErrorCode());
        }catch (Exception e){
            return Result.error(ResultCodeEnum.UNKNOWN_EXCEPTION.getResultCode());
        }

        return Result.success(playerResponse);
    }

    @Override
    public Result<PlayerResponse> login(Session session, byte[] data) {
        PlayerResponse playerResponse = null;
        try {
            //反序列化
            LoginRequest loginRequest = new LoginRequest();
            loginRequest.readFromBytes(data);

            ///参数校验
            if(StringUtils.isEmpty(loginRequest.getPlayerName()) || StringUtils.isEmpty(loginRequest.getPassword())){
                return Result.error(ResultCodeEnum.PARAM_ERROR.getResultCode());
            }

            //执行业务
            playerResponse = playerService.login(session, loginRequest.getPlayerName(), loginRequest.getPassword());
        }catch (ErrorCodeException e){
            return Result.error(e.getErrorCode());
        }catch (Exception e){
            e.printStackTrace();
            return Result.error(ResultCodeEnum.UNKNOWN_EXCEPTION.getResultCode());
        }

        return Result.success(playerResponse);
    }
}

最后只需要确定命令调用器的serverhandler以及server启动类就完成了服务端部分

ServerHandler.java
package com.chat.server.boot;

import com.chat.common.core.constants.Constants;
import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.core.model.Request;
import com.chat.common.core.model.Response;
import com.chat.common.core.model.Result;
import com.chat.common.core.serializable.Serializable;
import com.chat.common.core.session.Session;
import com.chat.common.core.session.SessionImpl;
import com.chat.common.core.session.SessionManager;
import com.chat.common.entity.Player;
import com.chat.common.core.scanner.Invoker;
import com.chat.common.core.scanner.InvokerHolder;
import com.google.protobuf.GeneratedMessage;
import org.jboss.netty.channel.*;

/**
 * 消息接收处理类
 * @author hzk
 * @date 2018/10/25
 */
public class ServerHandler extends SimpleChannelHandler{

    /**
     * 接收消息
     * @param ctx
     * @param e
     * @throws Exception
     */
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        Request request = (Request) e.getMessage();
        handleMessage(new SessionImpl(ctx.getChannel()),request);
    }

    private void handleMessage(Session session,Request request) throws Exception {
        Response response = new Response(request);

        System.out.println("ServerHandler->handleMessage Module:" + response.getModule() + ",Cmd:" + response.getCmd());

        //获取命令调用器
        Invoker invoker = InvokerHolder.getInvoker(request.getModule(), request.getCmd());
        if(null != invoker){
            try {
                Result result = null;
                //检测模块 若为用户模块 传入session channel参数,否则为聊天模块传入用户ID
                if(Constants.AbstractModule.PLAYER == request.getModule()){
                    result = (Result) invoker.invoke(session,request.getData());
                }else{
                    Object attachment = session.getAttachment();
                    if(null != attachment){
                        Player player = (Player) attachment;
                        result = (Result) invoker.invoke(player.getId(),request.getData());
                    }else{
                        //会话未登录 拒绝请求
                        response.setCode(ResultCodeEnum.NOT_LOGIN.getResultCode());
                        session.write(response);
                        return;
                    }
                }

                //检测请求状态
                if(ResultCodeEnum.SUCCESS.getResultCode() == result.getResultCode()){
                    //回写数据
                    Object content = result.getContent();
                    if(null != content){
                        if(content instanceof Serializable){
                            Serializable contentT = (Serializable) content;
                            response.setData(contentT.getBytes());
                        }else if(content instanceof GeneratedMessage){
                            GeneratedMessage contentT = (GeneratedMessage) content;
                            response.setData(contentT.toByteArray());
                        }else{
                            System.out.println(String.format("无法识别的传输对象:",content));
                        }
                    }
                    response.setCode(result.getResultCode());
                    session.write(response);
                }else{
                    //返回错误码
                    response.setCode(result.getResultCode());
                    session.write(response);
                    return;
                }
            }catch (Exception e){
                e.printStackTrace();
                //系统异常
                response.setCode(ResultCodeEnum.UNKNOWN_EXCEPTION.getResultCode());
                session.write(response);
            }
        }else{
            //未找到执行者
            response.setCode(ResultCodeEnum.NO_INVOKER.getResultCode());
            session.write(response);
            return;
        }
    }

    /**
     * 断线移除会话
     * @param ctx
     * @param e
     * @throws Exception
     */
    @Override
    public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
        SessionImpl session = new SessionImpl(ctx.getChannel());
        Object attachment = session.getAttachment();
        if(null != attachment){
            Player player = (Player) attachment;
            SessionManager.removeSession(player.getId());
        }
    }
}

Server.java
package com.chat.server.boot;

import com.chat.common.core.coder.RequestDecoder;
import com.chat.common.core.coder.ResponseEncoder;
import com.chat.common.core.constants.Constants;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * netty服务端启动类
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class Server {

    /**
     * 服务启动
     */
    public void start(){
        //服务引导程序
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        //boss线程监听端口,worker线程负责数据读写
        ExecutorService boss = Executors.newCachedThreadPool();
        ExecutorService worker = Executors.newCachedThreadPool();

        //设置NioSocket工厂
        serverBootstrap.setFactory(new NioServerSocketChannelFactory(boss,worker));

        //设置管道
        serverBootstrap.setPipelineFactory(new ChannelPipelineFactory() {

            @Override
            public ChannelPipeline getPipeline() throws Exception {
                ChannelPipeline pipeline = Channels.pipeline();
                pipeline.addLast("decoder",new RequestDecoder());
                pipeline.addLast("encoder",new ResponseEncoder());
                pipeline.addLast("serverHandler",new ServerHandler());
                return pipeline;
            }
        });

        serverBootstrap.setOption("backlog",1024);
        serverBootstrap.bind(new InetSocketAddress(Constants.AbstractNettyConfig.PORT));
        System.out.println("Server Start Success...");

    }

}

ServerMain.java
package com.chat.server.boot;

import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Netty服务端启动类
 * @author hzk
 * @date 2018/10/25
 */
public class ServerMain {
    
    public static void main(String[] args){
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("server/application.xml");
        Server server = classPathXmlApplicationContext.getBean(Server.class);
        server.start();
    }
}

Client部分

首先是和Server部分一样定义了模块号命令号用于业务的handler

ChatHandler.java
package com.chat.client.chat.handler;

import com.chat.common.core.annotation.SocketCommand;
import com.chat.common.core.annotation.SocketModule;
import com.chat.common.core.constants.Constants;

/**
 * 聊天处理
 * @author hzk
 * @date 2018/10/25
 */
@SocketModule(module = Constants.AbstractModule.CHAT)
public interface ChatHandler {

    /**
     * 发送广播消息回调
     * @param resultCode 响应码
     * @param data
     */
    @SocketCommand(cmd =  Constants.AbstractCmdChat.PUBLIC_CHAT)
    public void publicChat(int resultCode,byte[] data);

    /**
     * 私发消息
     * @param resultCode 响应码
     * @param data
     */
    @SocketCommand(cmd = Constants.AbstractCmdChat.PRIVATE_CHAT)
    public void privateChat(int resultCode,byte[] data);

    /**
     * 接收推送聊天消息
     * @param resultCode 响应码
     * @param data {@link com.chat.common.module.chat.response.ChatResponse}
     */
    @SocketCommand(cmd = Constants.AbstractCmdChat.PUSH_CHAT)
    public void receiveMessage(int resultCode,byte[] data);
}

ChatHandlerImpl.java
package com.chat.client.chat.handler.impl;

import com.chat.client.chat.handler.ChatHandler;
import com.chat.client.swing.ResultCodeHint;
import com.chat.client.swing.SwingClient;
import com.chat.common.core.constants.Constants;
import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.module.chat.response.ChatResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class ChatHandlerImpl implements ChatHandler{

    @Autowired
    private SwingClient swingClient;

    @Autowired
    private ResultCodeHint resultCodeHint;

    @Override
    public void publicChat(int resultCode, byte[] data) {
        if(ResultCodeEnum.SUCCESS.getResultCode() == resultCode){
            swingClient.getHints().setText("发送成功!");
        }else{
            swingClient.getHints().setText(resultCodeHint.getHintContent(resultCode));
        }
    }

    @Override
    public void privateChat(int resultCode, byte[] data) {
        if(ResultCodeEnum.SUCCESS.getResultCode() == resultCode){
            swingClient.getHints().setText("发送成功!");
        }else{
            swingClient.getHints().setText(resultCodeHint.getHintContent(resultCode));
        }
    }

    @Override
    public void receiveMessage(int resultCode, byte[] data) {
        ChatResponse chatResponse = new ChatResponse();
        chatResponse.readFromBytes(data);

        StringBuilder stringBuilder = new StringBuilder();
        if(Constants.AbstractChatType.PUBLIC_CHAT == chatResponse.getChatType()){
            stringBuilder.append(chatResponse.getSendPlayerName());
            stringBuilder.append("[");
            stringBuilder.append(chatResponse.getSendPlayerId());
            stringBuilder.append("]");
            stringBuilder.append(" 说:\n\t");
            stringBuilder.append(chatResponse.getMessage());
            stringBuilder.append("\n\n");
        }else if(Constants.AbstractChatType.PRIVATE_CHAT == chatResponse.getChatType()){
            if(swingClient.getPlayerResponse().getPlayerId() == chatResponse.getSendPlayerId()){
                stringBuilder.append("您悄悄对 ");
                stringBuilder.append("[");
                stringBuilder.append(chatResponse.getTargetPlayerName());
                stringBuilder.append("]");
                stringBuilder.append(" 说:\n\t");
                stringBuilder.append(chatResponse.getMessage());
                stringBuilder.append("\n\n");
            }else{
                stringBuilder.append(chatResponse.getSendPlayerName());
                stringBuilder.append("[");
                stringBuilder.append(chatResponse.getSendPlayerId());
                stringBuilder.append("]");
                stringBuilder.append(" 悄悄对你说:\n\t");
                stringBuilder.append(chatResponse.getMessage());
                stringBuilder.append("\n\n");
            }
        }
        swingClient.getChatContent().append(stringBuilder.toString());
    }
}

PlayerHandler.java
package com.chat.client.player.handler;

import com.chat.common.core.annotation.SocketCommand;
import com.chat.common.core.annotation.SocketModule;
import com.chat.common.core.constants.Constants;

/**
 * 用户处理
 * @author hzk
 * @date 2018/10/26
 */
@SocketModule(module = Constants.AbstractModule.PLAYER)
public interface PlayerHandler {

    /**
     * 注册并登录
     * @param resultCode
     * @param data
     */
    @SocketCommand(cmd = Constants.AbstractCmdPlayer.REGISTER_AND_LOGIN)
    public void registerAndLogin(int resultCode,byte[] data);


    /**
     * 登录账号
     * @param resultCode
     * @param data
     */
    @SocketCommand(cmd = Constants.AbstractCmdPlayer.LOGIN)
    public void login(int resultCode,byte[] data);
}

PlayerHandlerImpl.java
package com.chat.client.player.handler.impl;

import com.chat.client.player.handler.PlayerHandler;
import com.chat.client.swing.ResultCodeHint;
import com.chat.client.swing.SwingClient;
import com.chat.common.core.constants.ResultCodeEnum;
import com.chat.common.module.player.response.PlayerResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author hzk
 * @date 2018/10/26
 */
@Component
public class PlayerHandlerImpl implements PlayerHandler {

    @Autowired
    private SwingClient swingClient;

    @Autowired
    private ResultCodeHint resultCodeHint;

    @Override
    public void registerAndLogin(int resultCode, byte[] data) {
        if(ResultCodeEnum.SUCCESS.getResultCode() == resultCode){
            PlayerResponse playerResponse = new PlayerResponse();
            playerResponse.readFromBytes(data);

            swingClient.setPlayerResponse(playerResponse);
            swingClient.getHints().setText("注册并登录成功!");
        }else{
            swingClient.getHints().setText(resultCodeHint.getHintContent(resultCode));
        }
    }

    @Override
    public void login(int resultCode, byte[] data) {
        if(ResultCodeEnum.SUCCESS.getResultCode() == resultCode){
            PlayerResponse playerResponse = new PlayerResponse();
            playerResponse.readFromBytes(data);

            swingClient.setPlayerResponse(playerResponse);
            swingClient.getHints().setText("登录成功!");
        }else{
            swingClient.getHints().setText(resultCodeHint.getHintContent(resultCode));
        }
    }
}

最后也是只需要确定命令调用器的clienthandler以及client启动类就完成了客户端部分,这里不一样的是我们利用了swing去生成可视化界面便于操作

ResultCodeHint.java
package com.chat.client.swing;

import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;

/**
 * 消息提示
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class ResultCodeHint {

    private Properties properties= new Properties();

    /**
     * 初始化读取配置文件
     * @throws IOException
     */
    @PostConstruct
    public void init() throws IOException {
        InputStream in = getClass().getResourceAsStream("/client/code.properties");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
        properties.load(bufferedReader);
    }

    /**
     * 错误内容提示
     * @param code 错误码
     * @return
     */
    public String getHintContent(int code){
        Object object = properties.get(code+"");
        if(null == object){
            return "错误码:" + code;
        }
        return object.toString();
    }

}

SwingClient.java
package com.chat.client.swing;

import com.chat.client.boot.Client;
import com.chat.common.core.constants.*;
import com.chat.common.core.constants.SwingConstants;
import com.chat.common.core.model.Request;
import com.chat.common.module.chat.request.PrivateChatRequest;
import com.chat.common.module.chat.request.PublicChatRequest;
import com.chat.common.module.player.request.LoginRequest;
import com.chat.common.module.player.request.RegisterRequest;
import com.chat.common.module.player.response.PlayerResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;

/**
 * swing客户端
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class SwingClient extends JFrame implements ActionListener{

    @Autowired
    private Client client;

    /**
     * 用户信息
     */
    private PlayerResponse playerResponse;

    /**
     * 用户名
     */
    private JTextField playerName;

    /**
     * 密码
     */
    private JTextField password;

    /**
     * 登录按钮
     */
    private JButton loginButton;

    /**
     * 注册按钮
     */
    private JButton registerButton;

    /**
     * 聊天内容
     */
    private JTextArea chatContent;

    /**
     * 发送消息
     */
    private JTextField message;

    /**
     * 目标用户
     */
    private JTextField targetPlayer;

    /**
     * 发送按钮
     */
    private JButton sendButton;

    /**
     * 操作提示
     */
    private JLabel hints;

    public SwingClient(){
        getContentPane().setLayout(null);
        //登录部分
        JLabel playerNameLab = new JLabel("用户名");
        playerNameLab.setFont(new Font("宋体",Font.PLAIN,12));
        playerNameLab.setBounds(76,40,54,15);
        getContentPane().add(playerNameLab);

        playerName = new JTextField();
        playerName.setBounds(139,37,154,21);
        getContentPane().add(playerName);
        playerName.setColumns(10);

        JLabel passwordLab = new JLabel("密  码");
        passwordLab.setFont(new Font("宋体", Font.PLAIN, 12));
        passwordLab.setBounds(76, 71, 54, 15);
        getContentPane().add(passwordLab);

        password = new JTextField();
        password.setColumns(10);
        password.setBounds(139, 68, 154, 21);
        getContentPane().add(password);

        //登录
        loginButton = new JButton("登录");
        loginButton.setFont(new Font("宋体", Font.PLAIN, 12));
        loginButton.setActionCommand(SwingConstants.AbstractButtonCommand.LOGIN);
        loginButton.addActionListener(this);
        loginButton.setBounds(315, 37, 93, 23);
        getContentPane().add(loginButton);

        //注册
        registerButton = new JButton("注册");
        registerButton.setFont(new Font("宋体", Font.PLAIN, 12));
        registerButton.setActionCommand(SwingConstants.AbstractButtonCommand.REGISTER);
        registerButton.addActionListener(this);
        registerButton.setBounds(315, 67, 93, 23);
        getContentPane().add(registerButton);

        //聊天内容框
        chatContent = new JTextArea();
        chatContent.setLineWrap(true);

        JScrollPane scrollBar = new JScrollPane(chatContent);
        scrollBar.setBounds(76, 96, 93, 403);
        scrollBar.setSize(336, 300);
        getContentPane().add(scrollBar);


        //发送消息部分
        JLabel targetLab = new JLabel("私聊用户");
        targetLab.setFont(new Font("宋体", Font.PLAIN, 12));
        targetLab.setBounds(76, 436, 63, 24);
        getContentPane().add(targetLab);

        targetPlayer = new JTextField();
        targetPlayer.setBounds(139, 438, 133, 21);
        getContentPane().add(targetPlayer);
        targetPlayer.setColumns(10);

        JLabel messageLab = new JLabel("消息");
        messageLab.setFont(new Font("宋体", Font.PLAIN, 12));
        messageLab.setBounds(76, 411, 54, 15);
        getContentPane().add(messageLab);

        message = new JTextField();
        message.setBounds(139, 408, 222, 21);
        getContentPane().add(message);
        message.setColumns(10);

        sendButton = new JButton("发送");
        sendButton.setFont(new Font("宋体", Font.PLAIN, 12));
        sendButton.setBounds(382, 407, 67, 23);
        sendButton.setActionCommand(SwingConstants.AbstractButtonCommand.SEND);
        sendButton.addActionListener(this);
        getContentPane().add(sendButton);

        //错误提示区域
        hints = new JLabel();
        hints.setForeground(Color.red);
        hints.setFont(new Font("宋体", Font.PLAIN, 14));
        hints.setBounds(76, 488, 200, 15);
        getContentPane().add(hints);


        int weigh = 500;
        int high = 600;
        int w = (Toolkit.getDefaultToolkit().getScreenSize().width - weigh) / 2;
        int h = (Toolkit.getDefaultToolkit().getScreenSize().height - high) / 2;
        this.setLocation(w, h);
        this.setTitle("Try Chat");
        this.setSize(weigh, high);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setResizable(true);
    }

    @Override
    public void actionPerformed(ActionEvent event) {
        switch (event.getActionCommand()){
            //登录
            case SwingConstants.AbstractButtonCommand.LOGIN:
                try {
                    LoginRequest loginRequest = new LoginRequest();
                    loginRequest.setPlayerName(playerName.getText());
                    loginRequest.setPassword(password.getText());
                    //构建请求
                    Request request = Request.valueOf(Constants.AbstractModule.PLAYER,Constants.AbstractCmdPlayer.LOGIN,loginRequest.getBytes());
                    client.sendMessage(request);
                }catch (Exception e){
                    e.printStackTrace();
                    hints.setText("无法连接服务器");
                }
                break;
            //注册
            case SwingConstants.AbstractButtonCommand.REGISTER:
                try {
                    RegisterRequest registerRequest = new RegisterRequest();
                    registerRequest.setPlayerName(playerName.getText());
                    registerRequest.setPassword(password.getText());
                    //构建请求
                    Request request = Request.valueOf(Constants.AbstractModule.PLAYER,Constants.AbstractCmdPlayer.REGISTER_AND_LOGIN,registerRequest.getBytes());
                    client.sendMessage(request);
                }catch (Exception e){
                    e.printStackTrace();
                    hints.setText("无法连接服务器");
                }
                break;
            //发送消息
            case SwingConstants.AbstractButtonCommand.SEND:
                try {
                    if(StringUtils.isEmpty(targetPlayer.getText()) && !StringUtils.isEmpty(message.getText())){
                        PublicChatRequest publicChatRequest = new PublicChatRequest();
                        publicChatRequest.setContent(message.getText());
                        //构建请求
                        Request request = Request.valueOf(Constants.AbstractModule.CHAT,Constants.AbstractCmdChat.PUBLIC_CHAT,publicChatRequest.getBytes());
                        client.sendMessage(request);
                    }else{
                        if(StringUtils.isEmpty(message.getText())){
                            hints.setText("发送内容不能为空!");
                            return;
                        }

                        long playerId = 0;
                        try {
                            playerId = Long.parseLong(targetPlayer.getText());
                        }catch (NumberFormatException e){
                            e.printStackTrace();
                            hints.setText("用户ID为数字!");
                            return;
                        }

                        PrivateChatRequest privateChatRequest = new PrivateChatRequest();
                        privateChatRequest.setContent(message.getText());
                        privateChatRequest.setTargetPlayerId(playerId);
                        //构建请求
                        Request request = Request.valueOf(Constants.AbstractModule.CHAT,Constants.AbstractCmdChat.PRIVATE_CHAT,privateChatRequest.getBytes());
                        client.sendMessage(request);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                    hints.setText("无法连接服务器");
                }
            default:
                break;
        }
    }

    @Override
    protected void processWindowStateEvent(WindowEvent e) {
        if(WindowEvent.WINDOW_CLOSING == e.getID()){
            client.shutdown();
        }
        super.processWindowStateEvent(e);
    }

    public PlayerResponse getPlayerResponse() {
        return playerResponse;
    }

    public void setPlayerResponse(PlayerResponse playerResponse) {
        this.playerResponse = playerResponse;
    }

    public JTextArea getChatContent() {
        return chatContent;
    }

    public JLabel getHints() {
        return hints;
    }

}

ClientHandler.java
package com.chat.client.boot;

import com.chat.client.swing.SwingClient;
import com.chat.common.core.model.Response;
import com.chat.common.core.scanner.Invoker;
import com.chat.common.core.scanner.InvokerHolder;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

/**
 * 消息接收处理类
 * @author hzk
 * @date 2018/10/25
 */
public class ClientHandler extends SimpleChannelHandler{

    /**
     * 界面客户端
     */
    private SwingClient swingClient;

    public ClientHandler(SwingClient swingClient) {
        this.swingClient = swingClient;
    }

    /**
     * 接收消息
     * @param ctx
     * @param e
     * @throws Exception
     */
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        Response response = (Response) e.getMessage();
        handleMessage(response);
    }

    /**
     * 消息处理
     * @param response {@link Response}
     */
    private void handleMessage(Response response){
        System.out.println("ServerHandler->handleMessage Module:" + response.getModule() + ",Cmd:" + response.getCmd());
        //获取命令执行器
        Invoker invoker = InvokerHolder.getInvoker(response.getModule(), response.getCmd());
        if(null != invoker){
            try {
                invoker.invoke(response.getCode(),response.getData());
            }catch (Exception e){
                e.printStackTrace();
            }
        }else{
            //未找到执行器
            System.out.println("未找到执行器!");
        }

    }


    /**
     * 断开连接
     * @param ctx
     * @param e
     * @throws Exception
     */
    @Override
    public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
        swingClient.getHints().setText("与服务器断开连接!");
    }
}

Client.java
package com.chat.client.boot;

import com.chat.client.swing.SwingClient;
import com.chat.common.core.coder.RequestEncoder;
import com.chat.common.core.coder.ResponseDecoder;
import com.chat.common.core.constants.Constants;
import com.chat.common.core.model.Request;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


/**
 * netty客户端
 * @author hzk
 * @date 2018/10/25
 */
@Component
public class Client {

    /**
     * 界面客户端
     */
    @Autowired
    private SwingClient swingClient;

    /**
     * 客户端引导程序
     */
    ClientBootstrap clientBootstrap = new ClientBootstrap();

    /**
     * 会话通道
     */
    private Channel channel;

    /**
     * 线程池
     */
    private ExecutorService boss = Executors.newCachedThreadPool();
    private ExecutorService worker = Executors.newCachedThreadPool();

    /**
     * 初始化客户端
     * 服务器加载Servlet的时候运行,并且只会被服务器执行一次
     */
    @PostConstruct
    public void init(){
        //设置ClientSocket工厂
        clientBootstrap.setFactory(new NioClientSocketChannelFactory(boss,worker));
        //设置管道
        clientBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            @Override
            public ChannelPipeline getPipeline() throws Exception {
                ChannelPipeline pipeline = Channels.pipeline();
                pipeline.addLast("decoder",new ResponseDecoder());
                pipeline.addLast("encoder",new RequestEncoder());
                pipeline.addLast("clientHandler",new ClientHandler(swingClient));
                return pipeline;
            }
        });
    }

    /**
     * 连接服务端
     * @throws InterruptedException
     */
    public void connect() throws InterruptedException {
        //连接服务器
        ChannelFuture connect = clientBootstrap.connect(new InetSocketAddress(Constants.AbstractNettyConfig.ADDRESS, Constants.AbstractNettyConfig.PORT));
        connect.sync();
        channel = connect.getChannel();
    }

    /**
     * 关闭连接
     */
    public void shutdown(){
        channel.close();
    }

    /**
     * 获取会话通道
     * @return {@link Channel}
     */
    public Channel getChannel(){
        return channel;
    }

    /**
     * 发送消息
     * @param request {@link Request}
     * @throws InterruptedException
     */
    public void sendMessage(Request request) throws InterruptedException {
        if(null == channel || !channel.isConnected()){
            connect();
        }
        channel.write(request);
    }
}

ClientMain.java
package com.chat.client.boot;

import com.chat.client.swing.SwingClient;
import com.chat.server.boot.Server;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Netty服务端启动类
 * @author hzk
 * @date 2018/10/25
 */
public class ClientMain {
    
    public static void main(String[] args){
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("client/application.xml");
        SwingClient swingClient = classPathXmlApplicationContext.getBean(SwingClient.class);
        swingClient.setVisible(true);
    }
}

配置文件部分

上面这些就是我们这个简单的聊天室小项目的所有Java代码了,但是我们还有一些不能遗漏的东西,就是配置文件。

目录部分

在这里插入图片描述

client.applicatiion.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 引入配置 -->
    <!-- 配置文件 -->
    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:server/jdbc.properties</value>
                <value>classpath:server/log4j.properties</value>
            </list>
        </property>
    </bean>
    <!-- 包扫描 -->
    <context:annotation-config />
    <context:component-scan base-package="com.chat.client"/>
    <context:component-scan base-package="com.chat.common"/>

    <bean id="mapperBeanNameGenerator" class="com.chat.common.core.utils.MapperBeanNameGenerator"/>

    <!-- ==========================netty_chat数据的连接 Begin========================== -->

    <bean id="dataSourceWrite_netty" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性driverClassName、 url、user、password -->
        <property name="driverClassName" value="${db.driver}" />
        <property name="url" value="${db.netty.master.url}" />
        <property name="username" value="${db.netty.master.username}" />
        <property name="password" value="${db.netty.master.password}" />
        <!-- 配置初始化大小、最小、最大 -->
        <!-- 通常来说,只需要修改initialSize、minIdle、maxActive -->
        <property name="initialSize" value="${db.master.initialSize}" />
        <property name="minIdle" value="${db.master.minIdle}" />
        <property name="maxActive" value="${db.master.maxActive}" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${db.master.maxWait}" />
        <!-- 默认值是 true ,当从连接池取连接时,验证这个连接是否有效 -->
        <property name="testOnBorrow" value="true" />
        <!-- 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
        注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串 -->
        <property name="testWhileIdle" value="true" />
        <!-- 默认值是 flase, 当从把该连接放回到连接池的时,验证这个连接是否有效 -->
        <property name="testOnReturn" value="false" />
        <!--用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录-->
        <property name="validationQuery" value="SELECT 'x'" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="30000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="180" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="${db.master.logAbandoned}" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="50" />
        <property name="filters" value="stat" />
        <property name="proxyFilters">
            <list>
                <ref bean="logFilter" />
            </list>
        </property>
    </bean>

    <bean id="dataSourceRead_netty" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性driverClassName、 url、user、password -->
        <property name="driverClassName" value="${db.driver}" />
        <property name="url" value="${db.netty.slave.url}" />
        <property name="username" value="${db.netty.slave.username}" />
        <property name="password" value="${db.netty.slave.password}" />
        <!-- 配置初始化大小、最小、最大 -->
        <!-- 通常来说,只需要修改initialSize、minIdle、maxActive -->
        <property name="initialSize" value="${db.slave.initialSize}" />
        <property name="minIdle" value="${db.slave.minIdle}" />
        <property name="maxActive" value="${db.slave.maxActive}" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${db.slave.maxWait}" />
        <!-- 默认值是 true ,当从连接池取连接时,验证这个连接是否有效 -->
        <property name="testOnBorrow" value="true" />
        <!-- 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
        注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串 -->
        <property name="testWhileIdle" value="true" />
        <!-- 默认值是 flase, 当从把该连接放回到连接池的时,验证这个连接是否有效 -->
        <property name="testOnReturn" value="false" />
        <!--用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录-->
        <property name="validationQuery" value="SELECT 1 " />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="30000" />
        <!-- 超过时间限制是否回收 -->
        <property name="removeAbandoned" value="true" />
        <!-- 超时时间;单位为秒。180秒=3分钟 -->
        <property name="removeAbandonedTimeout" value="180" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="${db.slave.logAbandoned}" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="50" />
        <property name="filters" value="stat" />
        <property name="proxyFilters">
            <list>
                <ref bean="logFilter" />
            </list>
        </property>
    </bean>

    <bean id="logFilter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter">
        <property name="statementExecutableSqlLogEnable" value="false" />
    </bean>

    <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
    <bean id="sqlSessionFactoryWrite_netty" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSourceWrite_netty" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:_mapper/write/**/*.xml"/>
    </bean>

    <bean id="sqlSessionFactoryRead_netty" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSourceRead_netty" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:_mapper/read/**/*.xml"/>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.chat.common.dao.write" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryWrite_netty"/>
        <property name="nameGenerator" ref="mapperBeanNameGenerator"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.chat.common.dao.read" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryRead_netty"/>
        <property name="nameGenerator" ref="mapperBeanNameGenerator"/>
    </bean>
    <!-- ==========================netty_chat数据的连接 Finish========================== -->
</beans>

client.code.properties
2000=找不到命令
2001=参数异常
2002=未知异常
2003=用户名或密码不能为空
2004=用户名已使用
2005=用户不存在
2006=密码错误
2007=已经登录
2008=登录失败
2009=用户不在线
2010=未登录
2011=不能以自己为私聊对象
server.application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 引入配置 -->
    <!-- 配置文件 -->
    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:server/jdbc.properties</value>
                <value>classpath:server/log4j.properties</value>
            </list>
        </property>
    </bean>
    <!-- 包扫描 -->
    <context:annotation-config />
    <context:component-scan base-package="com.chat.server"/>
    <context:component-scan base-package="com.chat.common"/>

    <bean id="mapperBeanNameGenerator" class="com.chat.common.core.utils.MapperBeanNameGenerator"/>

    <!-- ==========================netty_chat数据的连接 Begin========================== -->

    <bean id="dataSourceWrite_netty" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性driverClassName、 url、user、password -->
        <property name="driverClassName" value="${db.driver}" />
        <property name="url" value="${db.netty.master.url}" />
        <property name="username" value="${db.netty.master.username}" />
        <property name="password" value="${db.netty.master.password}" />
        <!-- 配置初始化大小、最小、最大 -->
        <!-- 通常来说,只需要修改initialSize、minIdle、maxActive -->
        <property name="initialSize" value="${db.master.initialSize}" />
        <property name="minIdle" value="${db.master.minIdle}" />
        <property name="maxActive" value="${db.master.maxActive}" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${db.master.maxWait}" />
        <!-- 默认值是 true ,当从连接池取连接时,验证这个连接是否有效 -->
        <property name="testOnBorrow" value="true" />
        <!-- 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
        注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串 -->
        <property name="testWhileIdle" value="true" />
        <!-- 默认值是 flase, 当从把该连接放回到连接池的时,验证这个连接是否有效 -->
        <property name="testOnReturn" value="false" />
        <!--用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录-->
        <property name="validationQuery" value="SELECT 'x'" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="30000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="180" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="${db.master.logAbandoned}" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="50" />
        <property name="filters" value="stat" />
        <property name="proxyFilters">
            <list>
                <ref bean="logFilter" />
            </list>
        </property>
    </bean>

    <bean id="dataSourceRead_netty" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性driverClassName、 url、user、password -->
        <property name="driverClassName" value="${db.driver}" />
        <property name="url" value="${db.netty.slave.url}" />
        <property name="username" value="${db.netty.slave.username}" />
        <property name="password" value="${db.netty.slave.password}" />
        <!-- 配置初始化大小、最小、最大 -->
        <!-- 通常来说,只需要修改initialSize、minIdle、maxActive -->
        <property name="initialSize" value="${db.slave.initialSize}" />
        <property name="minIdle" value="${db.slave.minIdle}" />
        <property name="maxActive" value="${db.slave.maxActive}" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${db.slave.maxWait}" />
        <!-- 默认值是 true ,当从连接池取连接时,验证这个连接是否有效 -->
        <property name="testOnBorrow" value="true" />
        <!-- 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
        注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串 -->
        <property name="testWhileIdle" value="true" />
        <!-- 默认值是 flase, 当从把该连接放回到连接池的时,验证这个连接是否有效 -->
        <property name="testOnReturn" value="false" />
        <!--用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录-->
        <property name="validationQuery" value="SELECT 1 " />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="30000" />
        <!-- 超过时间限制是否回收 -->
        <property name="removeAbandoned" value="true" />
        <!-- 超时时间;单位为秒。180秒=3分钟 -->
        <property name="removeAbandonedTimeout" value="180" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="${db.slave.logAbandoned}" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="50" />
        <property name="filters" value="stat" />
        <property name="proxyFilters">
            <list>
                <ref bean="logFilter" />
            </list>
        </property>
    </bean>

    <bean id="logFilter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter">
        <property name="statementExecutableSqlLogEnable" value="false" />
    </bean>

    <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
    <bean id="sqlSessionFactoryWrite_netty" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSourceWrite_netty" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:_mapper/write/**/*.xml"/>
    </bean>

    <bean id="sqlSessionFactoryRead_netty" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSourceRead_netty" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:_mapper/read/**/*.xml"/>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.chat.common.dao.write" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryWrite_netty"/>
        <property name="nameGenerator" ref="mapperBeanNameGenerator"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.chat.common.dao.read" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryRead_netty"/>
        <property name="nameGenerator" ref="mapperBeanNameGenerator"/>
    </bean>
    <!-- ==========================netty_chat数据的连接 Finish========================== -->
</beans>

server.jdbc.properties
db.driver=com.mysql.jdbc.Driver


###################################netty_chat####################################
db.netty.master.url=jdbc:mysql://127.0.0.1:3306/netty_chat?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
db.netty.master.username=root
db.netty.master.password=123

db.netty.slave.url=jdbc:mysql://127.0.0.1:3306/netty_chat?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
db.netty.slave.username=root
db.netty.slave.password=123

###################################common####################################
#initialSize
db.master.initialSize=0
#maxActive
db.master.maxActive=30
#minIdle
db.master.minIdle=0
#maxWait
db.master.maxWait=5000
db.master.logAbandoned=true

#initialSize
db.slave.initialSize=1
#maxActive
db.slave.maxActive=100
#minIdle
db.slave.minIdle=10
#maxWait
db.slave.maxWait=5000
db.slave.logAbandoned=true

client.log4j.properties
#\u5b9a\u4e49LOG\u8f93\u51fa\u7ea7\u522b
log4j.rootLogger=info,dailyFile,debug,Console
#log4j.rootLogger=WARN,dailyFile,Console
#\u5b9a\u4e49\u65e5\u5fd7\u8f93\u51fa\u76ee\u7684\u5730\u4e3a\u63a7\u5236\u53f0
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out
#\u53ef\u4ee5\u7075\u6d3b\u5730\u6307\u5b9a\u65e5\u5fd7\u8f93\u51fa\u683c\u5f0f\uff0c\u4e0b\u9762\u4e00\u884c\u662f\u6307\u5b9a\u5177\u4f53\u7684\u683c\u5f0f
log4j.appender.Console.layout = org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%-5p] [%d] [%t] [%C.%M:%L] - %m%n

#\u6587\u4ef6\u5927\u5c0f\u5230\u8fbe\u6307\u5b9a\u5c3a\u5bf8\u7684\u65f6\u5019\u4ea7\u751f\u4e00\u4e2a\u65b0\u7684\u6587\u4ef6
log4j.appender.dailyFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyFile.Threshold=debug
log4j.appender.dailyFile.ImmediateFlush=true
log4j.appender.dailyFile.Append=true
log4j.appender.dailyFile.File=/home/logs/apps/netty/netty-server.log
log4j.appender.dailyFile.DatePattern='.'yyyy-MM-dd'.log'
log4j.appender.dailyFile.layout=org.apache.log4j.PatternLayout
log4j.appender.dailyFile.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n
log4j.appender.dailyFile.encoding=UTF-8

log4j.logger.com.pptv.ott.epglive.dao=debug
log4j.logger.org.apache.http=WARN
log4j.logger.org.apache.zookeeper=WARN

所有东西都准备好了之后,我们来走一遍流程,先启动服务端,然后开启多个客户端来验证下是否和我们设计的一样。
在这里插入图片描述
我们注册三个用户同时登陆,去模拟了群发以及私聊等功能,虽然项目很简洁但是功能基本都完全实现了,通过这段时间大家一起学习,对netty这个框架应该有了一个大致的了解,并且还能去了解一些序列化和socket相关的知识,博客中会涉及一些其他途径的内容,在这里借鉴也是希望大家可以坚持学习,对大家有一点点帮助心里也很高兴,因为也没有标明出处所以不好标明引用,希望涉及到的同学能理解。

猜你喜欢

转载自blog.csdn.net/u013985664/article/details/83862432
今日推荐