近日研究如何脱离开unity自带的网络组件实现网络功能,找到了Java里很不错的框架--Netty,他可以高效的实现多并发访问等功能。为了简单尝试下这两者之间的结合,在网上查了很多零碎的资料,于是打算做一个unity为前端的聊天室系统来验证效果。

服务器端

首先为了使用netty以及作为前后端沟通的json,这里需要先把netty和json转换工具载入项目,这里用的json转换工具是阿里粑粑的fastjson,因为我用的是maven,所以在pom.xml里面加入以下注解:

<dependencies>
  	<dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>5.0.0.Alpha2</version>
    </dependency>
    <dependency>
	    <groupId>com.alibaba</groupId>
	    <artifactId>fastjson</artifactId>
	    <version>1.2.47</version>
	</dependency>
  </dependencies>

然后简单说明下netty的运作机制,netty一般由三个类组成,分别是start,Initialize,Handler。即启动类挂载初始化类,初始化类挂载处理类。下面是对应三个类我的代码:

//package assembly:single
public class ChatServer {
	private int port;
    public ChatServer(int port) {
        this.port = port;
    }

    public void start(){
        //配置服务端的NIO线程组
        //两个Reactor一个用于服务端接收客户端的连接,另一个用于进行SocketChannel的网络读写
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
        //ServerBootstrap对象是Netty用于启动NIO服务端的辅助启动类,目的是降低服务端开发的复杂度
        ServerBootstrap bootstrap = new ServerBootstrap();
        //Set the EventLoopGroup for the parent (acceptor) and the child (client).
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                //回调请求
                .childHandler(new ChatServerInitialize())
                //.localAddress(new InetSocketAddress(port))
                //配置NioServerSocketChannel的TCP参数
                .option(ChannelOption.SO_BACKLOG, 1024)
                .option(ChannelOption.SO_KEEPALIVE,true);

            ChannelFuture future = bootstrap.bind(port).sync();

            System.out.println("服务器开始监听:");
            future.channel().closeFuture().sync();
            System.out.println("服务器结束监听:");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new ChatServer(8888).start();
	}

}
public class ChatServerInitialize extends ChannelInitializer<SocketChannel> {
	protected void initChannel(SocketChannel socketChannel) throws Exception {
        System.out.println("客户端连接:" + socketChannel.remoteAddress());
        //ChannelPipeline类似于一个管道,管道中存放的是一系列对读取数据进行业务操作的ChannelHandler。
        ChannelPipeline pipeline = socketChannel.pipeline();
        
        pipeline.addLast("frame",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        pipeline.addLast("decode",new StringDecoder());//解码器
        pipeline.addLast("encode",new StringEncoder());
        pipeline.addLast("handler",new ChatServerHandler());
    }
}
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
	public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /**
     * 当有客户端连接时,handlerAdded会执行,就把该客户端的通道记录下来,加入队列
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel inComing = ctx.channel();//获得客户端通道
        //通知其他客户端有新人进入
        for (Channel channel : channels){
            if (channel != inComing)
                channel.writeAndFlush(JSON.toJSONString(new Request(RequestType.Say,"[欢迎: " + inComing.remoteAddress() + "] 进入聊天室!\n")));
        }
        ChatRecord.chatRecord.add("[欢迎: " + inComing.remoteAddress() + "] 进入聊天室!\n");
        channels.add(inComing);//加入队列
    }

    /**
     * 断开连接
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel outComing = ctx.channel();//获得客户端通道
        //通知其他客户端有人离开
        for (Channel channel : channels){
            if (channel != outComing)
                channel.writeAndFlush(JSON.toJSONString(new Request(RequestType.Say,"[再见: ]" + outComing.remoteAddress() + " 离开聊天室!\n")));
        }
        ChatRecord.chatRecord.add(outComing.remoteAddress() + " 离开聊天室!\n");
        channels.remove(outComing);
    }


    /**
     * 当服务器监听到客户端活动时
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel inComing = ctx.channel();
        System.out.println("[" + inComing.remoteAddress() + "]: 在线");
    }

    /**
     * 离线
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel inComing = ctx.channel();
        System.out.println("[" + inComing.remoteAddress() + "]: 离线");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel inComing = ctx.channel();
        System.out.println(inComing.remoteAddress() + "通讯异常!");
        ctx.close();
    }

	@Override
	protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
		// TODO Auto-generated method stub
		Channel inComing = ctx.channel();
		Request request = JSON.parseObject(msg, Request.class);
		System.out.println("[用户" + inComing.remoteAddress() + request.GetRequestType()+"]" + request.getValue());
		switch (request.GetRequestType()) {
		//请求获取聊天记录
		case GetChatRecord:
			System.out.println("开始传输聊天记录");
			ChatRecord.Say();
			String value ="";
			boolean get =true;
			for(String Record: ChatRecord.chatRecord) {
				if(get) {
					value += Record+"\n";
					get=false;
				}else {
					get=true;
				}
			}
			inComing.writeAndFlush(JSON.toJSONString(new Request(RequestType.Say,value+"..更早记录请翻看聊天档案..\n")));
			break;
		//说话
		case Say:
		default:
			for (Channel channel : channels){
	            if (channel != inComing){
	                channel.writeAndFlush(JSON.toJSONString(new Request(RequestType.Say,inComing.remoteAddress() + ":" + request.getValue())));
	            }else {
	            	channel.writeAndFlush(JSON.toJSONString(new Request(RequestType.Say,"我:" + request.getValue())));
	            	ChatRecord.chatRecord.add(inComing.remoteAddress() + ":" + request.getValue());
	            }
	        }
			break;
		}
	}
}

同时为了实现json传输和业务需求,还创建了三个工具类,分别如下:

public class Request {
	private RequestType type;
	private String value;
	public int getType() {
		return type.ordinal();
	}
	public RequestType GetRequestType() {
		return type;
	}
	public void SetRequestType(RequestType type) {
		this.type = type;
	}
	public void setType(int type) {
		this.type = RequestType.value(type);
	}
	public String getValue() {
		return value;
	}
	public void setValue(String value) {
		this.value = value;
	}
	public Request(int type, String value) {
		super();
		setType(type);
		setValue(value);
	}
	public Request(RequestType type, String value) {
		super();
		this.type = type;
		setValue(value);
	}
	public Request() {
		super();
		// TODO Auto-generated constructor stub
	}
	@Override
	public String toString() {
		return "Request [type=" + type + ", value=" + value + "]";
	}
	
}
public enum RequestType {
	GetChatRecord,Say;
	public static RequestType value(int key) {
		RequestType[] keyTypes = RequestType.values();
		return keyTypes[key];
	}
}
public class ChatRecord {
	public static List<String> chatRecord = new ArrayList<String>();
	public static void Say() {
		System.out.println("list长度:"+chatRecord.size());
		for(String Record: chatRecord) {
			System.out.println(Record);
		}
	}
}

这三个类中ChatRecord是为了保存记录,RequestType是用于表明当前消息的类型,Request是请求的主题类。

客户端

客户端使用的是很低级的多线程死循环监听,还是很欠缺的,目前还在寻找更好的解决方案。具体如下:

前端unity后端Java 前端 unity_前端unity后端Java

首先搞了个简单的对话框,然后在SocketComponent上挂上脚本

前端unity后端Java 前端 unity_unity_02

具体脚本内容如下:

public class nettyComponent : MonoBehaviour {

    public string IP = "127.0.0.1";
    public int Port = 8888;
    public Text text;
    public MyLinkList<string> ceshi = new MyLinkList<string>();
    nettyClient client;

    // Use this for initialization
    void Start()
    {
        //获得nettyClient实例
        client = nettyClient.GetInstance(IP, Port);
    }


    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
            client.Closed();

        if (client.myLink.GetLength() > 0) {
            Request _key = client.myLink.GetElem(1);
            client.myLink.Delete(1);
            switch (_key.type) {
                case RequestType.Say:
                    ceshi.Append(_key.value);
                    if (ceshi.GetLength() > 6) ceshi.Delete(1);
                    string value = "";
                    for (int _i = 0; _i < ceshi.GetLength(); _i++)
                    {
                        value += ceshi.GetElem(_i + 1) + "\n";
                    }
                    text.text = value;
                    break;
            }
        }
    }
    private void OnDestroy()
    {
        client.Closed();
    }
    private void OnDisable()
    {
        client.Closed();
    }
    public void Send() {
        string xinxi = GameObject.Find("InputField").GetComponent<InputField>().text;
        GameObject.Find("InputField").GetComponent<InputField>().text = "";
        client.SendText(xinxi);
    }
}
public class nettyClient{

    public string IP = "127.0.0.1";
    public int Port = 7397;
    public MyLinkList<Request> myLink = new MyLinkList<Request>();
    public bool isConnected;

    //信息接收进程
    private Thread _ReceiveThread = null;
    //网络检测进程
    private Thread _connectionDetectorThread = null;

    private Socket clientSocket = null;
    private static byte[] result = new byte[1024];

    //单例模式
    private static nettyClient instance;

    public static nettyClient GetInstance()
    {
        if (instance == null)
        {
            instance = new nettyClient();
        }
        return instance;
    }

    public static nettyClient GetInstance(string ip, int port)
    {
        if (instance == null)
        {
            instance = new nettyClient(ip, port);
        }
        return instance;
    }

    //默认服务器IP地址构造函数
    public nettyClient()
    {
        startConnect();

        //初始化网络检测线程
        _connectionDetectorThread = new Thread(new ThreadStart(connectionDetector));
        //开启网络检测线程[用于检测是否正在连接,否则重新连接]
        _connectionDetectorThread.Start();
        
    }

    //自定义服务器IP地址构造函数
    public nettyClient(string ip, int port)
    {
        IP = ip;
        Port = port;

        startConnect();

        //初始化网络检测线程
        _connectionDetectorThread = new Thread(new ThreadStart(connectionDetector));
        //开启网络检测线程[用于检测是否正在连接,否则重新连接]
        _connectionDetectorThread.Start();

    }

    private void startConnect()
    {

        //创建Socket对象, 这里我的连接类型是TCP
        clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        //服务器IP地址
        IPAddress ipAddress = IPAddress.Parse(IP);
        //服务器端口
        IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, Port);
        //这是一个异步的建立连接,当连接建立成功时调用connectCallback方法
        IAsyncResult result = clientSocket.BeginConnect(ipEndpoint, new AsyncCallback(connectCallback), clientSocket);
        //这里做一个超时的监测,当连接超过5秒还没成功表示超时
        bool success = result.AsyncWaitHandle.WaitOne(5000, true);
        if (!success)
        {
            //超时
            clientSocket.Close();
            Debug.Log("connect Time Out");

            if (_ReceiveThread != null)
                _ReceiveThread.Abort();
            //          Closed();
        }
        else
        {
            //如果连接成功则开启接受进程,发送信息
            if (clientSocket.Connected)
            {

                this.isConnected = true;

                //初始化线程
                _ReceiveThread = new Thread(new ThreadStart(Receive));
                //开启线程[用于接收数据]
                _ReceiveThread.Start();

                //发送数据
                Send();
            }
        }


    }

    /// <summary>
    /// 发送数据
    /// </summary>
    public void Send()
    {
        string key = JsonConvert.SerializeObject(new Request(RequestType.GetChatRecord, ""));
        Debug.Log(key);
        for (int i = 0; i < 2; i++)
        {
            //UTF8编码
            clientSocket.Send(System.Text.Encoding.UTF8.GetBytes(key+"\n"));
        }
    }
    /// 发送数据
    /// </summary>
    public void SendText(string str)
    {
        string key = JsonConvert.SerializeObject(new Request(RequestType.Say, str));
        Debug.Log(key);
        for (int i = 0; i < 2; i++)
        {
            clientSocket.Send(System.Text.Encoding.UTF8.GetBytes(key + "\n"));
        }
    }
    //向服务端发送一条字符串
    //一般不会发送字符串 应该是发送数据包
    public void SendMessage(string str)
    {

        byte[] msg = System.Text.Encoding.UTF8.GetBytes(str);

        if (!clientSocket.Connected)
        {
            clientSocket.Close();
            return;
        }
        try
        {
            IAsyncResult asyncSend = clientSocket.BeginSend(msg, 0, msg.Length, SocketFlags.None, new AsyncCallback(sendCallback), clientSocket);
            bool success = asyncSend.AsyncWaitHandle.WaitOne(5000, true);

            if (!success)
            {
                clientSocket.Close();
                Debug.Log("Failed to SendMessage server.");
            }
            else
                Debug.Log("Message has been sent!");
        }
        catch
        {
            Debug.Log("send message error");
        }
    }

    /// <summary>
    /// 接收数据线程
    /// </summary>
    public void Receive()
    {
        int receiveLength = 0;

        try
        {
            while (true)
            {
                if (!clientSocket.Connected)
                {
                    //与服务器断开连接跳出循环
                    Debug.Log("Failed to clientSocket server.");
                    clientSocket.Close();
                    break;
                }

                try
                {
                    //Receive方法中会一直等待服务端回发消息
                    //如果没有回发会一直在这里等着。
                    int i = clientSocket.Receive(result);
                    if (i <= 0)
                    {
                        clientSocket.Close();

                        _ReceiveThread.Abort();

                        Debug.Log("断开连接");
                        break;
                    }

                    if ((receiveLength = clientSocket.Receive(result)) > 0)
                    {
                        //UTF8解码
                        Console.WriteLine("接收服务器消息:{0}", Encoding.UTF8.GetString(result, 0, receiveLength));
                        myLink.Append(JsonConvert.DeserializeObject<Request>(Encoding.UTF8.GetString(result, 0, receiveLength)));
                        Debug.Log(Encoding.UTF8.GetString(result, 0, receiveLength));
                    }
                }
                catch (Exception ex)
                {
                    Debug.Log("Failed to clientSocket error." + ex);
                    clientSocket.Close();
                }
            }
        }
        catch (Exception)
        {

            throw;
        }
    }

    /// <summary>
    /// 重新连接线程
    /// </summary>
    public void connectionDetector()
    {
        try
        {
            int connectTime = 0;

            while (true)
            {
                try
                {

                    if (clientSocket.Connected)
                    {
                        Debug.Log("网络检测中,连接状态为:" + clientSocket.Connected);

                        connectTime = 0;
                    }
                    else if (!clientSocket.Connected)
                    {
                        Debug.Log("网络检测中,连接状态为:False");

                        this.isConnected = false;

                        //尝试重连
                        Debug.Log("正在尝试第" + connectTime.ToString() + "次重连");
                        //连接
                        startConnect();
                        //每5秒执行一次重连
                        Thread.Sleep(5000);

                        connectTime += 1;
                    }
                }
                catch (Exception )
                {

                }
            }
        }
        catch (Exception)
        {

            throw;
        }
    }

    static void Main(string[] args)
    {
        new nettyClient();
    }

    //发送信息-回调
    private void sendCallback(IAsyncResult asyncSend)
    {
        Debug.Log(asyncSend.AsyncState);
    }

    //连接-回调
    private void connectCallback(IAsyncResult asyncConnect)
    {

    }

    //关闭Socket
    public void Closed()
    {
        try
        {
            if (clientSocket != null && clientSocket.Connected)
            {
                clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();

            }
            clientSocket = null;

            //关闭线程
            _ReceiveThread.Abort();
            _connectionDetectorThread.Abort();

            Debug.Log("已关闭Socket");
        }
        catch (Exception e)
        {
            throw e;
        }
    }
}

除此之外为了json传输,额外建立个C#类:

[System.Serializable]
public class Request{
    public RequestType type;
    public string value;
    public Request(RequestType _type, string _value) {
        type = _type;
        value = _value;
    }
    public override string ToString() {
        return "Request [type:" + type + ",value:"+value;
    }
}
public enum RequestType {
    GetChatRecord, Say
}

以及一个链表实现类,具体代码来自另外一个博主,链接如下:

http://www.manew.com/blog-11763-7490.html

效果

unity前台:

前端unity后端Java 前端 unity_network_03

前端unity后端Java 前端 unity_客户端_04

java后台:

前端unity后端Java 前端 unity_客户端_05

总结

可以看出因为前端代码的问题,需要访问后台两次才能返回正确的信息,造成后端重复访问,此外为了适应这种问题被迫改动后端代码适应前端。还会继续研究,看有没有办法优化前端的代码。