游戏套接字

该库已发布早期访问版,目前不稳定,因为它与其他解决方案并行开发。英语不是我的母语,所以没有注释。在这个阶段,该库适合那些想理解源代码并获得解决方案起点或帮助我的人:)

功能

  • 一个库包含服务器端和客户端端。
  • API通信库类似于Socket.io,但不兼容该解决方案。
  • 包含内置的二进制协议,因此您无需进行字节级别操作。
  • 传输层使用TCP。为了发送游戏消息,计划并行实现UDP
  • 它实现了多路复用等概念——通过单个通道与多个空间进行交互。

当前不计划支持WebSocket(但一切都可能随着社区的支持而改变)

示例

示例

用法

创建客户端

import 'package:game_socket/client.dart';

void main() {
  var client = GameClientExample();
  client.connect('localhost', 3103);
}

class GameClientExample extends GameSocketClient {
  GameClientExample() {
    on(Event.handshake, (packet) => _onHandshake(packet));
    on(Event.roomPacket, (packet) => _onRoomPacket(packet));
  }

  void _onHandshake(Packet packet) {
    if (packet.namespace == '/') {
      sendMessage(ConnectRequest('/home'));
    } else if (packet.namespace == '/home') {
      sendMessage(JoinRoomRequest('lobby', namespace: '/home'));
    }
  }

  void _onRoomPacket(RoomPacket packet) {
    var roomName = packet.roomName;
    if (packet.joinRoom && roomName == 'lobby') {
      var msg = RoomEvent(roomName!, namespace: '/home', event: 'hello', message: 'hello all');
      sendMessage(msg);
    }
  }
}

此客户端连接到服务器上的主/命名空间,然后连接到/home命名空间。然后,它发送一个进入lobby房间的请求,之后它分派一个包含消息文本hello allhello事件。

创建服务器

import 'package:game_socket/server.dart';

void main() {
  var service = SocketServiceExample();
  service.listen();
}

class SocketServiceExample {
  late GameSocketServer server;
  late Namespace home;

  SocketServiceExample() {
    server = GameSocketServer(options: ServerOptions.byDefault()..supportRawData = true);
    home = server.of('/home');
    home.on(ServerEvent.connect, (data) => _onHomeConnect(data));
    home.on('hello', (packet) => _onHomeData(packet));
    //
    server.on(ServerEvent.connection, (socket) {
      print('/: connection $socket');
      socket.on(ServerEvent.connect, (data) => _onConnect(data[0], data[1]));
      socket.on(Event.disconnecting, (data) => _onDisconnecting(data));
      socket.on(Event.disconnect, (data) => _onDisconnect(data[0], data[1]));
      socket.on(Event.error, (data) => _onError(data));
      socket.on(Event.data, (data) => _onData(data));
      socket.on(Event.close, (data) => {_onClose(data)});
    });
    server.on(ServerEvent.error, (data) => {print('/: eventError $data')});
    server.on(ServerEvent.close, (data) => {print('/: serverClose $data')});
    server.on(ServerEvent.raw, (data) => {print('/: raw $data')});
    server.on(ServerEvent.createRoom, (data) => {print('/: createRoom $data')});
    server.on(ServerEvent.joinRoom, (data) => {print('/: joinRoom $data')});
    server.on(ServerEvent.leaveRoom, (data) => {print('/: leaveRoom $data')});
    server.on(ServerEvent.deleteRoom, (data) => {print('/: deleteRoom $data')});
  }

  void listen() {
    server.listen();
  }

  void _onHomeConnect(dynamic data) {
    print('/home: connect $data');
  }

  void _onHomeData(dynamic data) {
    print('/home: $data');
    if (data is RoomPacket && data.roomName != null) {
      home.broadcast(data, rooms: {data.roomName!});
    }
  }

  void _onConnect(String namespace, String socketId) {
    print('/: connect $socketId');
  }

  void _onDisconnecting(dynamic data) {
    print('/: disconnecting $data');
  }

  void _onDisconnect(String namespace, String reason) {
    print('$namespace: disconnect reason:$reason');
  }

  void _onError(dynamic data) {
    print('/: error $data');
  }

  void _onData(dynamic data) {
    print('/: $data');
  }

  void _onClose(dynamic data) {
    print('/: close $data');
  }
}

服务器日志
listen null Options{ port:3103 raw:true closeOnError:false }
/: connection GameClient{ 15466abe2006464e99b6c8b36f7f4ed8 ReadyState.open [137545126]}
/: createRoom 15466abe2006464e99b6c8b36f7f4ed8
/: joinRoom [15466abe2006464e99b6c8b36f7f4ed8, 15466abe2006464e99b6c8b36f7f4ed8]
Home: connect [/home, 15466abe2006464e99b6c8b36f7f4ed8]
客户端日志
open InternetAddress('127.0.0.1', IPv4) ReadyState.open
handshake Packet{[0.0 /], bit:516, bool:16, int:[0, 0, 60, 0, 0, 0], string:{3: 15466abe2006464e99b6c8b36f7f4ed8}}
>> Message{[/home] boolMask:4, int:[0, 0, 0, 0, 0, 0], string:{} null}
handshake Packet{[0.0 /home], bit:516, bool:16, int:[0, 0, 60, 0, 0, 0], string:{3: 15466abe2006464e99b6c8b36f7f4ed8}}
>> Message{[/home] boolMask:16, int:[0, 0, 0], string:{0: lobby} null}
>> Message{[/home] boolMask:512, int:[0, 0, 0], string:{0: lobby, 5: hello, 1: hello all} null}

协议

协议是基于模式的。这种方法允许您节省传输的数据量,因为数据类型不随消息一起传输,并且数字的长度也不被序列化。

模式中使用的数据类型

类型 Size 范围
布尔值 1 位 真或假
int8 1 字节 0 到 255
int16 2 字节 0 到 65535
int32 4 字节 0 到 4294967295
string 1 + 值 0 到 255 个字符
bytes 2 + 值 0 到 65535 字节

创建模式

import 'package:game_socket/protocol.dart';
typedef PS = PlayerStateSchema;
class PlayerStateSchema extends SimpleSchema {
  @override
  int get code => 10; // unique schema code 10..255 
  @override
  int get version => 1; // version 0..255 to support game clients with different versions
  
  // bool
  static const int reserved = 0; // reserved
  @override
  int get boolCount => 1;

  // int8
  static const int speed = 0; // 0.000..1.000
  static const int health = 1; // max(100)
  @override
  int get int8Count => 2;
  // int16
  static const int uid = 2; // max(65535)
  static const int angle = 3; // radians
  static const int score = 4; // max(65535)
  @override
  int get int16Count => 3;
  // int32
  static const int elapsedTime = 5; // time for internal synchronization
  static const int x = 6; // x-coordinate
  static const int y = 7; // y-coordinate
  @override
  int get int32Count => 3;

  // strings
  static const int name = 0; // player name
  @override
  int get stringsCount => 1;
}

创建模式时,您要执行两项操作:获取数组的命名单元格编号,并将数组长度确定为五个模式数据类型之一。

创建消息类

class PlayerStateMessage extends Message {
  PlayerStateMessage(Player player, {required double elapsedTime}) : super(PS()) {
    putUInt(PS.uid, player.uid);
    putInt(PS.x, (player.positionBody.x * 1000).toInt()); // ~ -2000000.0000..+2000000.0000
    putInt(PS.y, (player.positionBody.y * 1000).toInt());
    putInt(PS.score, player.score);
    putSingle(PS.speed, player.speed);
    putRadians(PS.angle, player.rotationBody);
    putUInt(PS.elapsedTime, (elapsedTime * 1000).toInt()); // double ms
  }
}

读写消息时的数据类型

操作 模式类型 Dart 类型 范围
putBool 布尔值 布尔值 真或假
putInt int8 整数 -128 到 127
putUInt int8 整数 0 到 255
putInt int16 整数 -32768 到 32767
putUInt int16 整数 0 到 65535
putInt int32 整数 -2147483648 到 2147483647
putUInt int32 整数 0 到 4294967295
putString string 字符串 0 到 255 个字符
putSingle ~int8~ 双精度 0 到 1 步长 ~0.004
putRadians ~int16~ 双精度 步长 ~0.0002
putPayload bytes Uint8List 0 到 65535 字节

计划

  • 初始化以发送UDP图。
  • 自动连接和重新连接。
  • 扩展与房间交互的可能性。
  • 进行压力测试。

新手提示

  • 如果您正在开发浏览器游戏,那么您需要一个WebSocket解决方案。
  • 在设计实时通信游戏时,UDP是首选,因为TCP在数据包丢失时会导致延迟。

GitHub

https://github.com/QiXi/game_socket