文章目錄
  1. 1. 前言
  2. 2. 服务器
    1. 2.1. 数据存储
      1. 2.1.1. 关系型数据库
        1. 2.1.1.1. SQLite
      2. 2.1.2. 非关系型数据库
      3. 2.1.3. 数据库选择
    2. 2.2. 消息通信
      1. 2.2.1. 联网方式
      2. 2.2.2. 数据格式定义
      3. 2.2.3. 数据收发
      4. 2.2.4. 数据解析
    3. 2.3. 服务器验证
  3. 3. 服务器从零开发
    1. 3.1. 服务器语言选择
    2. 3.2. 服务器开源框架
    3. 3.3. 网络通信
      1. 3.3.1. 联网通信实现
        1. 3.3.1.1. 二进制抽象类
        2. 3.3.1.2. 网络通信
        3. 3.3.1.3. 数据格式解析
        4. 3.3.1.4. 服务器
    4. 3.4. 数据库Redis
  4. 4. Reference
    1. 4.1. 进阶
    2. 4.2. 基础知识
    3. 4.3. 相关资料
    4. 4.4. 实战讲解
    5. 4.5. 其他

前言

本章节是为了记录学习游戏开发过程中的服务器搭建以及前后端网络通信相关的知识点,游戏开发中联网是必不可少的,无论是从游戏热更新(服务器通知版本以及热更相关信息),还是游戏社交(微信分享,联机组队),还是防破解(服务器校验)都离不开网络通信和服务器。

本人是前端程序,对后端服务器开发几乎一窍不通,本篇文章也算是为了开阔自己的眼界,同时也是为了未来自己的游戏有网络这一块的支持需要对服务器搭建以及网络通信要有所了解。本文着重点不在于高效和优美的框架设计,着眼于基础的网络通信和服务器搭建,而这正是此篇博客的初衷。

  • 目标
  1. 理解游戏服务器工作原理
  2. 理解不同服务器框架的优劣,选择合适的服务器框架
  3. 学会搭建基础的游戏服务器,具备基础的数据存储功能(本文重点)
  4. 掌握网络通信知识,搭建前后端通信框架,支持前后端通信(本文重点)

服务器

那么什么是服务器了?

个人简单的理解,可以把服务器理解成运行在远程电脑上,通过网络对客户端提供服务(数据处理,数据存储读取,消息转发等)的机器或者软件。

了解服务器架构:

游戏服务器架构的演进简史

从上面可以看出服务器有以下几个基础功能:

  1. 对于游戏数据和玩家数据的存储(数据存储)
  2. 对玩家数据进行数据广播和同步(消息通信)
  3. 把一部分游戏逻辑在服务器上运算,做好验证,防止外挂(服务器验证)
  4. 服务器架构设计(容灾,性能,扩容等)

第三点涉及到前后端框架设计(帧同步 or 状态同步等),第四点更与服务器端密切相关,本文着眼点是前两点,第三点和第四点作为未来学习的一个知识点,。

数据存储

关系型数据库

关系数据库(英语:Relational database),是创建在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。现实世界中的各种实体以及实体之间的各种联系均用关系模型来表示。

个人简单的理解,关系数据库大概就是以前学SQL的时候,数据库里通过表来抽象实体数据,表之间通过ID映射关联起来的概念(e.g. 比如数据库里有两张表,一张员工表,一张员工数据表。员工表用于抽象员工ID,员工名字,员工薪资等信息。而员工数据用于存储员工ID,员工入职日期等信息。然后两张表示通过员工ID来映射关联起来。)。

典型的关系型数据库:
MySQL

典型的关系型数据查询语言:
SQL

这里还值得一提的一个本地数据库:
SQLite

详细的MYSQL和SQLite对比参考:
SQLite和MySQL数据库的区别与应用

SQLite

SQLite学习详情参考

非关系型数据库

NoSQL是对不同于传统的关系数据库的数据库管理系统的统称。两者存在许多显著的不同点,其中最重要的是NoSQL不使用SQL作为查询语言。其数据存储可以不需要固定的表格模式,也经常会避免使用SQL的JOIN操作,一般有水平可扩展性的特征。

看了上面Wiki的介绍,本人其实还是一知半解,等待后续深入学习理解非关系型数据库。

有了关系型数据库为什么还需要非关系型数据库了?
当代典型的关系数据库在一些数据敏感的应用中表现了糟糕的性能,例如为巨量文档创建索引、高流量网站的网页服务,以及发送流式媒体。关系型数据库的典型实现主要被调整用于执行规模小而读写频繁,或者大批量极少写访问的事务。NoSQL的结构通常提供弱一致性的保证,如最终一致性,或交易仅限于单个的数据项。不过,有些系统,提供完整的ACID保证在某些情况​​下,增加了补充中间件层(例如:CloudTPS)

典型的菲关系型数据库:
MongoDB,Redis

数据库选择

关系数据库还是NoSQL数据库
数据库选择历程
游戏服务器存储系统设计
本人对数据库的经验还停留在学校学习SQL的时候,也可以理解成使用过一点关系型数据库。
这里是想深入理解关系和非关系之间的区别,同时为选择游戏开发时使用的数据库类型做知识储备。以下理解的不对的地方,忘指出。

结合上面两篇文章的学习理解,个人理解关系型数据库(e.g. MySQL)适用于读写不频繁(面向表级别的锁定),需要强大的查询(SQL)功能。
而非关系型数据库(e.g. Redis)适用于频繁读写(性能要求高 — Redis是Ke-Vlaue结构,且面向内存的存储模型(使用RDB和AOF做持久化)),需要高扩展性。

结合网上的学习理解,使用Redis(做内存Cache)+MySQL(做稳定的数据库存储查询)利用各自优势使用在不同模块来实现高性能的服务器存储设计是比较高效合理的。

这里本人出于学习目的,暂定以关系型数据库(MySQL)作为服务器数据库存储介质,暂时不考虑Redis。

出于单机游戏数据库存储学习,暂时以关系型数据库(SQLite)作为本地数据库存储介质。

后期Redis进阶学习:
Redis 教程

消息通信

消息通信是本篇文章的重点,首先让我们理一理实现消息通信前我们需要做些什么?

  1. 联网方式(选择通信协议,比如: TCP || UPD)
  2. 数据格式定义(会影响数据编码和解析,比如: Protobuf)
  3. 数据收发(涉及到网络通信监听,比如: Socket编程)
  4. 数据解析(涉及到二进制数据写入和读取,一般来说都不会明文传输)

一般来说消息通信的流程如下:

  1. 服务器启动了网络监听(比如通过Socket监听固定端口等待客户端连接)
  2. 客户端通过Socket连接服务器端口
  3. 指定前后端消息协议(Protobuf),基于协议封装消息数据
  4. 客户端通过封装二进制消息数据通过Socket传递给服务器
  5. 服务器接受到二进制数据通过对应方式解析消息后返回客户端消息
  6. 客户端做出对应表现

联网方式

了解联网方式之前,我们需要知道一些相关的网络知识。

以前学计算机网络课程时,印象最深的就是网络的OSI七层分类:

OSI模型

这里我们主要关注的是以下两层:

  1. 会话层

    负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。这一层是负责网络连接的,比如我们通过Socket启动端口监听

  2. 传输层

    把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议(TCP)等。这一层负责添加网络协议相关信息,比如根据不同网络协议(TCP or UDP)用不同的方式处理丢包问题

更详细的网络相关知识介绍参考:

TCP和UDP的区别

这里我们只要知道TCP(Transmission Control Protocol,传输控制协议)协议是面向连接的可靠传输协议,有三次握手过程。而UDP(User Data Protocol,用户数据报协议)协议是无连接不可靠协议

传输协议选择?
TCP
理由:

  1. 游戏对网络数据准确性要求较高

数据格式定义

这里的数据格式指的是网络消息数据格式定义。

在完成了客户端和服务器基础网络数据传输能力(TPC)后,我们必须定义一个统一的消息定义(即数据格式)和消息处理的方式,这样客户端和服务器端才能正常的解析网络数据。

数据格式需求:

  1. 知道数据大小
  2. 知道数据类型
  3. 知道如何序列化和反序列化数据

基础的数据格式定义如下:

  1. 数据长度(short - 2字节) — 用于存储消息数据长度
  2. 消息ID(short - 2字节) — 用于区分消息类型
  3. 二进制数据(Protobuf) — 用于序列化和反序列化数据

数据收发

有了TCP网络数据传输的能力后,我们还需要网络通信的能力。
网路通信方式选择?
Socket

数据解析

数据的解析就是前面提到的数据序列化和反序列化的能力。

数据解析协议选择?
Protobuf
理由:

  1. 强大的生态圈,多语言支持,数据前后兼容性,强大序列化反序列化能力等

服务器验证

为什么需要验证?

单机游戏不联网很容易有外挂的一个原因就是本地数据(本地存储或者只存在内存中)很容易被串改且无法验证正确性,比如通过修改内存数据(e.g. 数值挂),修改游戏运行速度(e.g. 加速挂)

解决这一问题的关键正式服务器验证,把所有的数据都存储在服务器上,所有的数据修改都必须通过服务器校验才能通过,这样一来客户端伪造数据就能被识别出来从而防止部分外挂。

服务器验证的应用场景?

服务器验证有一点很重要的应用就是服务器运行战斗逻辑,确保所有客户端结果一致性。

这一点涉及到前后端同步框架相关的知识,详情参考:

两种同步模式:状态同步和帧同步

这里直接借用博客里的一些定义来加深基础理解:

什么是同步?

所谓同步,就是要多个客户端表现效果是一致,数据是一致。

什么是状态同步?

状态同步下,客户端更像是一个服务端数据的表现层。以服务器通知为准。

什么是帧同步?

帧同步下,通信就比较简单了,服务端只转发操作,不做任何逻辑处理。

FrameSync

状态同步和帧同步的区别?

最大的区别就是战斗核心逻辑写在哪,状态同步的战斗逻辑在服务端,帧同步的战斗逻辑在客户端。

这里只是简单的引用上面博主的部分说明来加深理解,更深入学习请参考前面给出的博客链接。

服务器从零开发

前面是对于服务器相关理论知识的一些基础知识的学习实战。接下来为了真正做到开发一个可用的服务器,接下来打算从零开发搭建一个可用的服务器用于个人独立游戏(最重要的目的是从中学习到服务器相关的知识以及用到自己的游戏中来)。

服务器语言选择

作为一个服务器知识几乎为零的前端小白(用到过的语言为C#,C++,Lua,Python,Java—其中C#和Lua是现在用的最多的),打算入门服务器开发(主要用于个人游戏开发),所以在语言选择上主要是希望易上手,性能也还行。在高并发方面要求不高。

对于相对熟悉C#和Lua的博主来说能使用C#开发服务器的是比较合适的。

未来的语言学习要针对开发的游戏类型对服务器的性能要求等来分析再做具体选型。

结合网上的资料,了解到:

11种服务器编程语言对比(附游戏服务器框架) 2020.06

服务器开源框架

选择一个好的开源框架作为服务器开发入门来说是必不可少的,这里了解基于C#的服务器框架比较少,有一个ET的服务器框架比较出名,但ET看起来过于庞大,入门起来可能比较难,所以这里暂时没有考虑。

未来的学习方向可能不限语言,不一定是ET也可能是别的语言的服务器框架。

现阶段打算先从0开始打通服务器和客户端的流程,所以打断用C#自己写一套简易版的服务器来熟悉整个服务器开发。

网络通信

网络通信是指通过网络进行数据传输处理的能力。为了实现网络通信,我们需要借助于TCP( or UDP or **)协议进行数据传输,借助Socket进行端口监听,从而实现网络通信的能力。

联网方式选择?
TCP协议 + Socket
理由:

  1. 游戏开发对网络稳定性要求较高,所以选择以TCP作为网络传输协议。

数据格式定义选择?
Protobuf
理由:

  1. 强大的生态圈,多语言支持,数据前后兼容性,强大序列化反序列化能力等

前后端通信模式选择?
帧同步
理由:

  1. 现阶段只是为了实现前后端的通信能力,只是希望后端提供消息处理和数据存储的功能,暂时不考虑反外挂,服务器验证等问题,所以选择更适合以后端转发通知的CS模式的帧同步。

联网通信实现

二进制抽象类

联网通信免不了对二进制数据的写入,所以在实现TPC和Socket网络连接传输消息之前,我们需要实现一个对二进制数据进行读取和写入工具类实现。

以下代码参考至:
Unity3D —— Socket通信(C#)

ByteBufferWriter.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/*
* Description: ByteBufferWriter.cs
* Author: TONYTANG
* Create Date: 2019/07/14
*/


using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace Net
{
/// <summary>
/// ByteBufferWriter.cs
/// 二进制写入抽象类
/// </summary>
public class ByteBufferWriter
{
// Note:
// 注意大小端问题
// BinaryWriter和BinaryReader都是小端读写

/// <summary>
/// 内存写入类
/// </summary>
private MemoryStream mMemoryStream;

/// <summary>
/// 二进制流写入类
/// </summary>
private BinaryWriter mBinaryWriter;

public ByteBufferWriter()
{

mMemoryStream = new MemoryStream();
mBinaryWriter = new BinaryWriter(mMemoryStream);
}

public ByteBufferWriter(byte[] data)
{

if(data != null)
{
mMemoryStream = new MemoryStream(data);
mBinaryWriter = new BinaryWriter(mMemoryStream);
}
else
{
mMemoryStream = new MemoryStream();
mBinaryWriter = new BinaryWriter(mMemoryStream);
}
}

/// <summary>
/// 关闭二进制读取写入
/// </summary>
public void Close()
{

mBinaryWriter.Close();
mMemoryStream.Close();
mBinaryWriter = null;
mMemoryStream = null;
}

/// <summary>
/// 写入一个字节数据
/// </summary>
/// <param name="v"></param>
public void WriteByte(byte v)
{

mBinaryWriter.Write(v);
}

******

/// <summary>
/// 获取字节数据
/// </summary>
/// <returns></returns>
public byte[] ToBytes()
{
mBinaryWriter.Flush();
return mMemoryStream.ToArray();
}

/// <summary>
/// 写入数据
/// </summary>
public void Flush()
{

mBinaryWriter.Flush();
}
}
}

ByteBufferReader.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
* Description: ByteBufferReader.cs
* Author: TONYTANG
* Create Date: 2019/07/14
*/


using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace Net
{
/// <summary>
/// ByteBufferReader.cs
/// 二进制读取抽象类
/// </summary>
public class ByteBufferReader
{
// Note:
// 注意大小端问题
// BinaryWriter和BinaryReader都是小端读写

/// <summary>
/// 内存写入类
/// </summary>
private MemoryStream mMemoryStream;

/// <summary>
/// 二进制流读取类
/// </summary>
private BinaryReader mBinaryReader;

public ByteBufferReader()
{

mMemoryStream = new MemoryStream();
mBinaryReader = new BinaryReader(mMemoryStream);
}

public ByteBufferReader(byte[] data)
{

if(data != null)
{
mMemoryStream = new MemoryStream(data);
mBinaryReader = new BinaryReader(mMemoryStream);
}
else
{
mMemoryStream = new MemoryStream();
mBinaryReader = new BinaryReader(mMemoryStream);
}
}

/// <summary>
/// 关闭二进制读取写入
/// </summary>
public void Close()
{

mBinaryReader.Close();
mMemoryStream.Close();
mBinaryReader = null;
mMemoryStream = null;
}

/// <summary>
/// 读取一个字节数据
/// </summary>
public byte ReadByte()
{

return mBinaryReader.ReadByte();
}

******
}
}

测试用例:
NetMessage.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Collections.Generic;

namespace Net
{
/// <summary>
/// 网络消息抽象类
/// </summary>
public class NetMessage
{
/// <summary>
/// 消息ID
/// </summary>
public ushort MessageID
{
get;
private set;
}

/// <summary>
/// 消息二进制数据
/// </summary>
public byte[] MessageData
{
get;
private set;
}

/// <summary>
/// 消息长度
/// </summary>
public ushort MessageLength
{
get;
private set;
}

private NetMessage()
{


}

public NetMessage(ushort messageid, byte[] messagedata)
{

MessageID = messageid;
MessageData = messagedata;
MessageLength = (ushort)messagedata.Length;
}
}
}

NetMessage msg = new NetMessage(1, Encoding.UTF8.GetBytes("我是消息内容"));
ByteBufferWriter bbw = new ByteBufferWriter();
bbw.WriteShort(msg.MessageLength);
bbw.WriteShort(msg.MessageID);
bbw.WriteBytes(msg.MessageData);
var bytesdata = bbw.ToBytes();
bbw.Close();
ByteBufferReader bbb = new ByteBufferReader(bytesdata);
var msglength = bbb.ReadShort();
var msgid = bbb.ReadShort();
var msgcontent = bbb.ReadBytes(msglength);
Debug.Log("msglength = " + msglength);
Debug.Log("msgid = " + msgid);
Debug.Log("MessageContent = " + Encoding.UTF8.GetString(msgcontent));

输出:
ByteBufferOutput

注意问题:

  1. 大小端问题(前后端写入和读取字节的大小端方式必须一致)

大小端知识:
理解字节序
浅谈字节序(Byte Order)及其相关操作

网络通信

TCP + Socket
流程:

  1. 启动服务器端套接字监听等待客户端连接
  2. 服务器端启动线程监听客户端消息
  3. 启动客户端套接字连接服务器套接字端口
  4. 客户端启动消息发送线程等待向服务器发送客户端消息
  5. 服务器端接受并返回客户端消息
  6. 客户端处理服务器端消息

NetConnector.cs(网络连接抽象类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/// <summary>
/// 网络连接抽象类
/// </summary>
public class NetConnector
{
/// <summary>
/// IP地址
/// </summary>
public string IpAddress
{
get;
private set;
}

/// <summary>
/// 端口号
/// </summary>
public int Port
{
get;
private set;
}

/// <summary>
/// 网络套接字
/// </summary>
public Socket NetSocket
{
get;
private set;
}

/// <summary>
/// 是否连接成功
/// </summary>
public bool IsConnected
{
get;
private set;
}

/// <summary>
/// IP地址
/// </summary>
public IPAddress IP
{
get;
private set;
}

/// <summary>
/// IP以及端口地址
/// </summary>
public IPEndPoint IPEndPoint
{
get;
private set;
}

public NetConnector()
{

NetSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IsConnected = false;
}

public NetConnector(ProtocolType protocoltype = ProtocolType.Tcp)
{

NetSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, protocoltype);
IsConnected = false;
}

/// <summary>
/// 监听指定ip和端口地址
/// </summary>
/// <param name="ipaddress"></param>
/// <param name="port"></param>
public void ListenTo(string ipaddress, int port)
{

IpAddress = ipaddress;
Port = port;
IP = IPAddress.Parse(IpAddress);
IPEndPoint = new IPEndPoint(IP, Port);
NetSocket.Bind(IPEndPoint);
NetSocket.Listen((int)SocketOptionName.MaxConnections);
Console.WriteLine("启动监听{0}成功", NetSocket.LocalEndPoint.ToString());
}

/// <summary>
/// 连接指定地址
/// </summary>
/// <param name="ipaddress"></param>
/// <param name="port"></param>
public bool ConnectTo(string ipaddress, int port)
{

IpAddress = ipaddress;
Port = port;
IP = IPAddress.Parse(IpAddress);
IPEndPoint = new IPEndPoint(IP, Port);

try
{
// 连接指定地址
NetSocket.Connect(IPEndPoint);
IsConnected = true;
}
catch
{
IsConnected = false;
}
return IsConnected;
}

/// <summary>
/// 断开连接
/// </summary>
public void Close()
{

IsConnected = false;
NetSocket.Shutdown(SocketShutdown.Both);
NetSocket.Close();
}
}

NetServer.cs(网络服务器抽象类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/// <summary>
/// 网络服务器抽象类
/// </summary>
public class NetServer : SingletonTemplate<NetServer>
{
/// <summary>
/// 服务器是否启动
/// </summary>
public bool IsServerStart
{
get;
private set;
}

/// <summary>
/// 网络连接器
/// </summary>
private static NetConnector mNetConnector;

/// <summary>
/// 限定长度的客户端消息接受二进制数组
/// </summary>
private static byte[] ClientMsg = new byte[1024 * 1024];

public NetServer()
{
mNetConnector = new NetConnector();
IsServerStart = false;
}

public NetServer(ProtocolType protocoltype = ProtocolType.Tcp)
{
mNetConnector = new NetConnector(protocoltype);
IsServerStart = false;
}

/// <summary>
/// 启动服务器端口监听
/// </summary>
public void StartServer()
{
if(IsServerStart == false)
{
// 启动服务器端口监听以及等待客户端连接线程
IsServerStart = true;
mNetConnector.ListenTo("192.168.1.3", 8888);
Thread clientconnectthread = new Thread(ClientConnectListener);
clientconnectthread.Start();
}
else
{
Console.WriteLine("服务器端已启动!");
}
}

/// <summary>
/// 发送消息到客户端
/// </summary>
/// <param name="msg"></param>
public void SendMessage(NetMessage msg)
{
//clientsocket.Send(WriteMessage(msg));
}

/// <summary>
/// 客户端连接请求监听线程
/// </summary>
private void ClientConnectListener()
{
while(true)
{
// 检查是否有客户端连接(阻塞的)
Socket clientsocket = mNetConnector.NetSocket.Accept();
Console.WriteLine("客户端{0}成功连接", clientsocket.RemoteEndPoint.ToString());
// 想客户端发送连接成功数据(这里的二进制数据可以换成Protobuf序列化的)
NetMessage netmsg = new NetMessage(1, Encoding.UTF8.GetBytes("连接成功!"));
clientsocket.Send(WriteMessage(netmsg));
// 为连接的客户端创建接受消息的线程
Thread resmsgthread = new Thread(ReceiveMessage);
resmsgthread.Start(clientsocket);
}
}

/// <summary>
/// 写入网络传输所需数据
/// 1. 数据长度(short - 2字节) -- 用于存储消息数据长度
/// 2. 消息ID(short - 2字节) -- 用于区分消息类型
/// 3. 二进制数据(Protobuf) -- 用于序列化和反序列化数据
/// </summary>
/// <param name="netmsg"></param>
private byte[] WriteMessage(NetMessage netmsg)
{
ByteBufferWriter bbw = new ByteBufferWriter();
bbw.WriteShort(netmsg.MessageLength);
bbw.WriteShort(netmsg.MessageID);
bbw.WriteBytes(netmsg.MessageData);
bbw.Flush();
var bytesdata = bbw.ToBytes();
bbw.Close();
return bytesdata;
}

/// <summary>
/// 接受客户端消息
/// </summary>
/// <param name="clientsocket"></param>
private void ReceiveMessage(object clientsocket)
{
Socket cltsocket = (Socket)clientsocket;
while(true)
{
try
{
int receivenumber = cltsocket.Receive(ClientMsg);
if(receivenumber == 0)
{
Console.WriteLine("客户端 : {0}断开了连接!", cltsocket.RemoteEndPoint.ToString());
cltsocket.Shutdown(SocketShutdown.Both);
cltsocket.Close();
break;
}
else
{
Console.WriteLine(string.Format("接收客户端{0}消息, 长度为{1}", cltsocket.RemoteEndPoint.ToString(), receivenumber));
ByteBufferReader bbr = new ByteBufferReader(ClientMsg);
// 消息数据长度
int msglen = bbr.ReadShort();
// 消息id
int msgid = bbr.ReadShort();
// 数据二进制内容
byte[] msgdata = bbr.ReadBytes(msglen);
var msgcontent = Encoding.UTF8.GetString(msgdata);
Console.WriteLine(string.Format("消息数据长度 : {0} 消息ID : {1} 消息数据内容:{2}", msglen, msgid, msgcontent));
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
cltsocket.Shutdown(SocketShutdown.Both);
cltsocket.Close();
break;
}
}
}
}

NetClient.cs(网络客户端抽象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/// <summary>
/// NetClient.cs
/// 网络客户端抽象
/// </summary>
public class NetClient : SingletonTemplate<NetClient>
{
/// <summary>
/// 服务器端的二进制数据缓存区
/// </summary>
private static byte[] ServerMsg = new byte[1024 * 1024];

/// <summary>
/// 客户端网络连接器
/// </summary>
private NetConnector mNetConnector;

/// <summary>
/// 是否已连接服务器
/// </summary>
public bool IsServerConnected
{
get
{
return mNetConnector.IsConnected;
}
}

/// <summary>
/// 消息接受线程
/// </summary>
private Thread mMsgReceiveThread;

public NetClient()
{
mNetConnector = new NetConnector();
mMsgReceiveThread = null;
}

public NetClient(ProtocolType protocoltype = ProtocolType.Tcp)
{
mNetConnector = new NetConnector(protocoltype);
mMsgReceiveThread = null;
}

/// <summary>
/// 连接服务器
/// </summary>
/// <param name="ip">ip地址</param>
/// <param name="port">端口号</param>
public void ConnectToServer(string ipaddress, int port)
{
if(IsServerConnected)
{
mNetConnector.Close();
}
try
{
// 连接服务器
mNetConnector.ConnectTo(ipaddress, port);
Debug.Log(string.Format("连接服务器:{0}成功!", mNetConnector.IPEndPoint.ToString()));
}
catch
{
Debug.Log(string.Format("连接服务器:{0}失败!", mNetConnector.IPEndPoint.ToString()));
}

if (IsServerConnected)
{
// 启动客户端线程去监听接受服务器消息
mMsgReceiveThread = new Thread(ReceiveMessage);
mMsgReceiveThread.Start(mNetConnector.NetSocket);
}
}

/// <summary>
/// 断开连接
/// </summary>
public void Close()
{
if (IsServerConnected)
{
Debug.Log(string.Format("断开服务器地址 : {0}连接!", mNetConnector.NetSocket.RemoteEndPoint.ToString()));
mNetConnector.Close();
}
}

/// <summary>
/// 发送消息到服务器
/// </summary>
/// <param name="msg"></param>
public void SendMessage(NetMessage msg)
{
if (IsServerConnected == false)
{
Debug.Log("服务器未连接成功,无法发送数据!");
return;
}
else
{
try
{
mNetConnector.NetSocket.Send(WriteMessage(msg));
}
catch
{
mNetConnector.Close();
}
}
}


/// <summary>
/// 写入网络传输所需数据
/// 1. 数据长度(short - 2字节) -- 用于存储消息数据长度
/// 2. 消息ID(short - 2字节) -- 用于区分消息类型
/// 3. 二进制数据(Protobuf) -- 用于序列化和反序列化数据
/// </summary>
/// <param name="netmsg"></param>
private byte[] WriteMessage(NetMessage netmsg)
{
ByteBufferWriter bbw = new ByteBufferWriter();
bbw.WriteShort(netmsg.MessageLength);
bbw.WriteShort(netmsg.MessageID);
bbw.WriteBytes(netmsg.MessageData);
bbw.Flush();
var bytesdata = bbw.ToBytes();
bbw.Close();
return bytesdata;
}

/// <summary>
/// 服务器连接消息接受监听线程
/// </summary>
private void ReceiveMessage(object clientsocket)
{
Socket cltsocket = (Socket)clientsocket;
while (true)
{
try
{
if(mNetConnector.IsConnected)
{
if(mNetConnector.NetSocket.Available > 0)
{
int receivenumber = cltsocket.Receive(ServerMsg);
if (receivenumber == 0)
{
Debug.Log(string.Format("服务器端 : {0}断开了连接!", cltsocket.RemoteEndPoint.ToString()));
break;
}
else
{
ByteBufferReader bbr = new ByteBufferReader(ServerMsg);
var msglength = bbr.ReadShort();
var msgid = bbr.ReadShort();
var msgdata = bbr.ReadBytes(msglength);
string msgcontent = Encoding.UTF8.GetString(msgdata);
Debug.Log(string.Format("服务器返回数据: 消息ID : {0} 消息长度 : {1} 消息内容 : {2}", msgid, msglength, msgcontent));
}
}
}
else
{
break;
}
}
catch (Exception ex)
{
Debug.Log(ex.Message);
mNetConnector.NetSocket.Close();
break;
}
}
}
}

NetMessage.cs(网络消息格式抽象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// <summary>
/// 网络消息抽象类
/// </summary>
public class NetMessage
{
/// <summary>
/// 消息ID
/// </summary>
public ushort MessageID
{
get;
private set;
}

/// <summary>
/// 消息二进制数据
/// </summary>
public byte[] MessageData
{
get;
private set;
}

/// <summary>
/// 消息长度
/// </summary>
public ushort MessageLength
{
get;
private set;
}

private NetMessage()
{


}

public NetMessage(ushort messageid, byte[] messagedata)
{

MessageID = messageid;
MessageData = messagedata;
MessageLength = (ushort)messagedata.Length;
}
}

测试用例:

  1. 启动服务器
1
2
NetServer.Singleton.StartServer();
Console.ReadLine();
  1. 启动客户端
1
2
NetClient.Singleton.ConnectToServer("192.168.1.3", 8888);
NetClient.Singleton.SendMessage(new NetMessage(2, Encoding.UTF8.GetBytes("测试客户端发送给服务器!")));

服务器端输出:
ServerOutput

客户端输出:
ClientOutput

可以看到通过Socket编程,我们使用TPC连接成功将消息从客户端发送到服务器以及服务器发送到客户端进行处理。NetMessage的简单封装是为了上层逻辑能区分收到的是什么消息以及消息数据有多少,基于byte[]的存储让我们可以快速的切换数据序列化和反序列化的方式(为后面使用Protobuf打下基础)。

待续……

数据格式解析

服务器

数据库Redis

Reference

进阶

对于网络框架的设计和理论知识进阶学习:
教你从头写游戏服务器框架

基础知识

关系数据库
NoSQL
关系数据库还是NoSQL数据库
数据库选择历程
Redis
SQLite
SQLite和MySQL数据库的区别与应用
两种同步模式:状态同步和帧同步
游戏服务器架构的演进简史
TCP和UDP的区别
OSI模型

相关资料

SQL 教程
SQL Tutorial

实战讲解

我们来用Unity做一个局域网游戏(上)
Unity3D —— Socket通信(C#)
Unity网络服务器搭建【中高级】
教你从头写游戏服务器框架

unity3d联网游戏:客户端与服务器端的交互

IOShootGameDemo

其他

11种服务器编程语言对比(附游戏服务器框架) 2020.06

NoahGameFrame

文章目錄
  1. 1. 前言
  2. 2. 服务器
    1. 2.1. 数据存储
      1. 2.1.1. 关系型数据库
        1. 2.1.1.1. SQLite
      2. 2.1.2. 非关系型数据库
      3. 2.1.3. 数据库选择
    2. 2.2. 消息通信
      1. 2.2.1. 联网方式
      2. 2.2.2. 数据格式定义
      3. 2.2.3. 数据收发
      4. 2.2.4. 数据解析
    3. 2.3. 服务器验证
  3. 3. 服务器从零开发
    1. 3.1. 服务器语言选择
    2. 3.2. 服务器开源框架
    3. 3.3. 网络通信
      1. 3.3.1. 联网通信实现
        1. 3.3.1.1. 二进制抽象类
        2. 3.3.1.2. 网络通信
        3. 3.3.1.3. 数据格式解析
        4. 3.3.1.4. 服务器
    4. 3.4. 数据库Redis
  4. 4. Reference
    1. 4.1. 进阶
    2. 4.2. 基础知识
    3. 4.3. 相关资料
    4. 4.4. 实战讲解
    5. 4.5. 其他