用Netty实现自定义的网络协议,实现于Unity的通信

545 阅读3分钟

前言

在游戏开发中,网络通信是不可避免的,为了更好地掌控网络通信,通常需要自定义网络协议。在这篇文章中,我们将会介绍如何使用Netty实现自定义协议,并将其应用到Unity的网络通信中。

准备工作

在开始之前,需要先准备好Java和C#的开发环境。同时,需要安装好Netty库和Unity库。

实现

自定义协议

我们将自定义一种协议,该协议用于Unity客户端与Java服务器之间的通信。 协议格式如下:

+——+————+——————+——————+ 
| 协议头 | 消息长度 | 消息体 |
+——+————+——————+——————+ 
|2 | 4 | n(n<=0) | 
+——+————+——————+——————+ 

协议头固定为2个字节,消息长度固定为4个字节,用于存储消息体的长度。消息体长度没有固定,可以根据具体数据而变化。

服务器端Java代码

我们使用Netty库来实现服务器端代码。

public class Server { 
     private int port; 
     public Server(int port) { 
          this.port = port; 
     } 
     public void run() { 
         EventLoopGroup bossGroup = new NioEventLoopGroup(); 
         EventLoopGroup workerGroup = new NioEventLoopGroup(); 
         try { 
              ServerBootstrap b = new ServerBootstrap(); 
              b.group(bossGroup, workerGroup) 
              .channel(NioServerSocketChannel.class) 
              .childHandler(new ChannelInitializer<SocketChannel>() { 
                   @Override 
                   public void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline(); 
                        pipeline.addLast(new MessageDecoder()); 
                        pipeline.addLast(new MessageEncoder()); 
                        pipeline.addLast(new ServerHandler()); 
                   } 
               }) 
               .option(ChannelOption.SO_BACKLOG, 128)
               .childOption(ChannelOption.SO_KEEPALIVE, true); 
               ChannelFuture f = b.bind(port).sync(); 
               System.out.println("Server started on port " + port);
               f.channel().closeFuture().sync(); 
         } catch (InterruptedException e) { 
              e.printStackTrace(); 
         } finally { 
              workerGroup.shutdownGracefully(); 
              bossGroup.shutdownGracefully(); 
         } 
   } 
   public static void main(String[] args) { 
        ew Server(8888).run(); 
   } 
} 

在代码中,我们首先定义了一个Server类,用于启动Netty服务器。在该类中,我们使用EventLoopGroup来创建boss和worker线程组。然后创建ServerBootstrap实例,并设置服务器的一些参数。我们需要设置端口号、Channel类型、ChannelHandler以及一些ChannelOption。最后,通过bind方法来启动服务器。如果绑定成功,则输出相应的信息。

public class ServerHandler extends ChannelInboundHandlerAdapter { 
     @Override 
     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 
          Message message = (Message) msg; 
          System.out.println("Server received message: " + message.getBody()); // 处理接收到的消息 
          Message response = new Message(); 
          response.setBody("Hello from server!"); 
          ctx.writeAndFlush(response); 
     } 
     @Override 
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 
           cause.printStackTrace(); ctx.close(); 
     } 
} 

在上述代码中,我们使用了ChannelHandler来处理接收到的消息。在服务器收到消息后,我们会将消息转化为Message类,并输出到控制台上。然后处理相应业务逻辑,并将处理结果作为回传信息进行返回。

public class MessageDecoder extends ByteToMessageDecoder { 
      @Override 
      protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 
           if (in.readableBytes() < 6) { 
                return; 
           } 
           in.markReaderIndex(); 
           short magic = in.readShort(); 
           if (magic != 0x7EF2) { 
               throw new CorruptedFrameException("Invalid magic number: " + magic); 
           } 
           int length = in.readInt(); 
           if (in.readableBytes() < length) { 
               in.resetReaderIndex(); 
               return; 
           } 
           byte[] body = new byte[length]; 
           in.readBytes(body); 
           Message message = new Message(); 
           message.setBody(new String(body, StandardCharsets.UTF_8)); 
           out.add(message); 
      } 
 } 

在上述代码中,我们使用了MessageDecoder来接收并解码消息。首先我们读取2个字节的协议头,判断是否为0x7EF2。然后读取4个字节的消息体长度,再读取相应长度的数据,并将其转化为Message对象。

public class MessageEncoder extends MessageToByteEncoder<Message> { 
     @Override 
     protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { 
          byte[] body = msg.getBody().getBytes(StandardCharsets.UTF_8);
          out.writeShort(0x7EF2); 
          out.writeInt(body.length); 
          out.writeBytes(body); 
    } 
} 

在上述代码中,我们使用了MessageEncoder来编码和发送消息。首先我们写入协议头和消息体长度,然后写入相应长度的数据。

客户端C#代码

我们使用Unity库来实现客户端代码。

public class Client : MonoBehaviour { 
     private TcpClient client; 
     private NetworkStream stream; 
     public void Connect(string ip, int port) { 
         client = new TcpClient(ip, port); 
         stream = client.GetStream(); 
     } 
     public void Send(string message) { 
        byte[] body = Encoding.UTF8.GetBytes(message); 
        int length = body.Length; 
        byte[] buffer = new byte[6 + length]; 
        buffer[0] = 0x7E; 
        buffer[1] = 0xF2; 
        buffer[2] = (byte)((length << 24) & 0xFF); 
        buffer[3] = (byte)((length << 16) & 0xFF); 
        buffer[4] = (byte)((length << 8) & 0xFF); 
        buffer[5] = (byte)(length & 0xFF); 
        Array.Copy(body, 0, buffer, 6, length); 
        stream.Write(buffer, 0, buffer.Length); 
        byte[] recvBuffer = new byte[1024]; 
        int recvLength = stream.Read(recvBuffer, 0, recvBuffer.Length); 
        string response = Encoding.UTF8.GetString(recvBuffer, 0, recvLength); 
        Debug.Log("Client received message: " + response); 
     } 
     public void Disconnect() { 
        stream.Close(); 
        client.Close(); 
     } 
 } 

在客户端代码中,我们首先定义了一个Client类,用于处理客户端与服务器的通信。其中Connect方法用于连接服务器,Send方法用于发送消息,Disconnect方法用于断开连接。 在Send方法中,我们首先将消息体转化为byte数组,并获取其长度。然后根据协议格式,计算出整个消息的长度,并将其写