近日研究如何脱离开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是请求的主题类。
客户端
客户端使用的是很低级的多线程死循环监听,还是很欠缺的,目前还在寻找更好的解决方案。具体如下:
首先搞了个简单的对话框,然后在SocketComponent上挂上脚本
具体脚本内容如下:
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前台:
java后台:
总结
可以看出因为前端代码的问题,需要访问后台两次才能返回正确的信息,造成后端重复访问,此外为了适应这种问题被迫改动后端代码适应前端。还会继续研究,看有没有办法优化前端的代码。