-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
1,397 additions
and
975 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# 为什么使用C# .net core做服务端? | ||
游戏服务端从早期的单服到分布式,开发越来越复杂,对稳定性,开发效率要求越来越高。开发语言的选择也逐步发生了变化,C 到 C++ 到 C++ + PYTHON 或者C++ + LUA 到现在 很多公司开始使用erlang,go,java,c#。目前是一个百花齐放的局面。 | ||
|
||
但是如果是要你重新做一个网游server,不考虑对公司或者已有的东西兼容性,你会怎么选择?我仔细想了一下这个问题,大概有这个几个方面需要考虑: | ||
|
||
###### 1. 语言的稳定性(致命性) | ||
游戏服务器的特点是高负载低延时。所以一般服务端进程都是带状态的,一旦挂掉就意味着数据丢失,这点是无法容忍的。 | ||
|
||
###### 2. 运行时热更(致命性) | ||
游戏服务器逻辑极其复杂,很容易出现bug,但是又不能经常停服,所以热更修复bug就显得十分必要。出现错误开发人员可以立即编写代码,然后热更修复,线上用户完全感觉不到。 | ||
|
||
###### 3. 是否有协程支持(重要性5星) | ||
分布式服务器架构,进程与进程之间必然会有大量交互。由于游戏逻辑很难拆分成多线程,所以一般来说都是逻辑单线程。如果没有协程支持,必然产生大量回调,代码维护会变得非常困难。 | ||
|
||
###### 4. 编译速度(重要性5星) | ||
使用c++开发,30%的时间都浪费在编译上。假如编译很快或者不需要编译,必定大大提高开发效率。 | ||
|
||
###### 5. 跨平台(4星) | ||
一般游戏服务器都架设在linux上面。但是平常开发,使用windows会更加方便,如果跨平台,开发以及测试效率会大大提升,并且不需要单独搞一个开发机,本机电脑就可以满足平常开发 | ||
|
||
###### 6. 可阅读性,可重构性(3星) | ||
代码可以重构能大大减轻写代码的难度 | ||
|
||
###### 7. 库是否齐全,生态是否完善(3星) | ||
库齐全,生态好,自己需要造的轮子就少 | ||
|
||
###### 8.跟客户端统一语言(3星) | ||
客户端服务端共用语言,优势十分明显,很多代码可以复用,逻辑程序员不再需要区分前后端,双端都可以写,一个人即可完成一个功能,大大减少了沟通的时间成本。 | ||
|
||
###### 9. IDE的支持(3星) | ||
代码提示,重构等支持,优秀的IDE能提高几倍的开发效率。 | ||
|
||
###### 10. 语言的性能(1星) | ||
目前服务器性能都不是太大问题,不过性能好总比性能差要强。 | ||
|
||
| 语言 | C# | C/C++ | Java | Go | Lua | Python | Erlang | | ||
| -- | :--: | :--: | :--: | :--: | :--: | :--: | :--: | | ||
| 稳定性 | 稳定 | 容易挂 | 稳定 | 稳定 | 稳定 | 稳定 | 稳定 | | ||
| 运行时热更 | 支持 | 较难支持 | 支持 | 不支持 | 支持 | 支持 | 支持 | | ||
| 跨平台 | 支持 | 较难支持 | 支持 | 支持 | 较难支持 | 支持 | 支持 | | ||
| 协程 | 有 | 需要自己实现 | 支持不好 | 支持 | 支持 | 支持 | 支持 | | ||
| 编译速度 | 快 | 慢 | 快 | 快 | 不需要编译 | 不需要编译 | 不需要编译| | ||
| 阅读性重构性 | 好 | 一般 | 好 | 一般 | 差 | 差 | 差| | ||
| 游戏库跟生态 | 好 | 好 | 一般 | 一般 | 差 | 好 | 一般| | ||
| 客户端统一语言 | Unity | Unity、UE4 | 暂无 | 暂无 | Unity、UE4 | UE4 | 暂无| | ||
| IDE的支持 | 好 | 好 |好 | 普通| 差 | 差 | 差| | ||
| 语言的性能 | 好|极好| 好| 好| 差|很差|差| | ||
|
||
从表格可以看出: | ||
1. C/C++稳定性差,编译速度慢,存在致命缺陷 | ||
2. Go不支持热更,由于不支持泛型,重构性较差,无法跟客户端共享代码,存在致命缺陷 | ||
3. Java协程支持差,无法跟客户端共享代码 | ||
4. Lua库少,性能差,代码可阅读性可重构性差,跨平台完全依赖C/C++,处理起来麻烦,ide支持差 | ||
5. Python 性能很差,代码可阅读性可重构性差,无法跟客户端共享代码,ide支持差 | ||
6. Erlang 性能差,函数式风格不好上手,ide支持差 | ||
7. C# .net core各个方便都非常优秀,不过跟UE4无法共享代码 | ||
|
||
当前Unity是最火的游戏引擎,C#服务端搭配Unity完全是天作之合,基本上找不到缺陷。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
# 什么是协程 | ||
说到协程,我们先了解什么是异步,异步简单说来就是,我要发起一个调用,但是这个被调用方(可能是其它线程,也可能是IO)出结果需要一段时间,我不想让这个调用阻塞住调用方的整个线程,因此传给被调用方一个回调函数,被调用方运行完成后回调这个回调函数就能通知调用方继续往下执行。举个例子: | ||
下面的代码,主线程一直循环,每循环一次sleep 1毫秒,计数加一,每10000次打印一次。 | ||
|
||
```csharp | ||
private static void Main() | ||
{ | ||
int loopCount = 0; | ||
while (true) | ||
{ | ||
int temp = watcherValue; | ||
|
||
Thread.Sleep(1); | ||
|
||
++loopCount; | ||
if (loopCount % 10000 == 0) | ||
{ | ||
Console.WriteLine($"loop count: {loopCount}"); | ||
} | ||
} | ||
} | ||
``` | ||
这时我需要加个功能,在程序一开始,我希望在5秒钟之后打印出loopCount的值。看到5秒后我们可以想到Sleep方法,它会阻塞线程一定时间然后继续执行。我们显然不能在主线程中Sleep,因为会破坏掉每10000次计数打印一次的逻辑。 | ||
```csharp | ||
// example2_1 | ||
class Program | ||
{ | ||
private static int loopCount = 0; | ||
|
||
private static void Main() | ||
{ | ||
OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance; | ||
|
||
WaitTimeAsync(5000, WaitTimeFinishCallback); | ||
|
||
while (true) | ||
{ | ||
OneThreadSynchronizationContext.Instance.Update(); | ||
|
||
Thread.Sleep(1); | ||
|
||
++loopCount; | ||
if (loopCount % 10000 == 0) | ||
{ | ||
Console.WriteLine($"loop count: {loopCount}"); | ||
} | ||
} | ||
} | ||
|
||
private static void WaitTimeAsync(int waitTime, Action action) | ||
{ | ||
Thread thread = new Thread(()=>WaitTime(waitTime, action)); | ||
thread.Start(); | ||
} | ||
|
||
private static void WaitTimeFinishCallback() | ||
{ | ||
Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
} | ||
|
||
/// <summary> | ||
/// 在另外的线程等待 | ||
/// </summary> | ||
private static void WaitTime(int waitTime, Action action) | ||
{ | ||
Thread.Sleep(waitTime); | ||
|
||
// 将action扔回主线程执行 | ||
OneThreadSynchronizationContext.Instance.Post((o)=>action(), null); | ||
} | ||
} | ||
``` | ||
我们在这里设计了一个WaitTimeAsync方法,WaitTimeAsync其实就是一个典型的异步方法,它从主线程发起调用,传入了一个WaitTimeFinishCallback回调方法做参数,开启了一个线程,线程Sleep一定时间后,将传过来的回调扔回到主线程执行。OneThreadSynchronizationContext是一个跨线程队列,任何线程可以往里面扔委托,OneThreadSynchronizationContext的Update方法在主线程中调用,会将这些委托取出来放到主线程执行。为什么回调方法需要扔回到主线程执行呢?因为回调方法中读取了loopCount,loopCount在主线程中也有读写,所以要么加锁,要么永远保证只在主线程中读写。加锁是个不好的做法,代码中到处是锁会导致阅读跟维护困难,很容易产生多线程bug。这中将逻辑打包成委托然后扔回另外一个线程多线程开发中常用的技巧。 | ||
|
||
我们可能又有个新需求,WaitTimeFinishCallback执行完成之后,再想等3秒,再打印一下loopCount。 | ||
```csharp | ||
private static void WaitTimeAsync(int waitTime, Action action) | ||
{ | ||
Thread thread = new Thread(()=>WaitTime(waitTime, action)); | ||
thread.Start(); | ||
} | ||
private static void WaitTimeFinishCallback() | ||
{ | ||
Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
WaitTimeAsync(3000, WaitTimeFinishCallback2); | ||
} | ||
|
||
private static void WaitTimeFinishCallback2() | ||
{ | ||
Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
} | ||
``` | ||
我们这时还可能改需求,需要在程序启动5秒后,接下来4秒,再接下来3秒,打印loopCount,也就是上面的逻辑中间再插入一个3秒等待。 | ||
```csharp | ||
private static void WaitTimeAsync(int waitTime, Action action) | ||
{ | ||
Thread thread = new Thread(()=>WaitTime(waitTime, action)); | ||
thread.Start(); | ||
} | ||
|
||
private static void WaitTimeFinishCallback() | ||
{ | ||
Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
WaitTimeAsync(4000, WaitTimeFinishCallback3); | ||
} | ||
|
||
private static void WaitTimeFinishCallback3() | ||
{ | ||
Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
WaitTimeAsync(3000, WaitTimeFinishCallback2); | ||
} | ||
|
||
private static void WaitTimeFinishCallback2() | ||
{ | ||
Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
} | ||
``` | ||
这样中间插入一段代码,显得非常麻烦。这里可以回答什么是协程了,其实这一串串回调就是协程。 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# 更好的协程 | ||
上文讲了一串回调就是协程,显然这样写代码,增加逻辑,插入逻辑非常容易出错。我们需要利用异步语法把这个异步回调的形式改成同步的形式,幸好C#已经帮我们设计好了,看代码 | ||
```csharp | ||
// example2_2 | ||
class Program | ||
{ | ||
private static int loopCount = 0; | ||
|
||
static void Main(string[] args) | ||
{ | ||
OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance; | ||
|
||
Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}"); | ||
|
||
Crontine(); | ||
|
||
while (true) | ||
{ | ||
OneThreadSynchronizationContext.Instance.Update(); | ||
|
||
Thread.Sleep(1); | ||
|
||
++loopCount; | ||
if (loopCount % 10000 == 0) | ||
{ | ||
Console.WriteLine($"loop count: {loopCount}"); | ||
} | ||
} | ||
} | ||
|
||
private static async void Crontine() | ||
{ | ||
await WaitTimeAsync(5000); | ||
Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
await WaitTimeAsync(4000); | ||
Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
await WaitTimeAsync(3000); | ||
Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}"); | ||
} | ||
|
||
private static Task WaitTimeAsync(int waitTime) | ||
{ | ||
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); | ||
Thread thread = new Thread(()=>WaitTime(waitTime, tcs)); | ||
thread.Start(); | ||
return tcs.Task; | ||
} | ||
|
||
/// <summary> | ||
/// 在另外的线程等待 | ||
/// </summary> | ||
private static void WaitTime(int waitTime, TaskCompletionSource<bool> tcs) | ||
{ | ||
Thread.Sleep(waitTime); | ||
|
||
// 将tcs扔回主线程执行 | ||
OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true), null); | ||
} | ||
} | ||
``` | ||
在这段代码里面,WaitTimeAsync方法中,我们利用了TaskCompletionSource类替代了之前传入的Action参数,WaitTimeAsync方法返回了一个Task类型的结果。WaitTime中我们把action()替换成了tcs.SetResult(true),WaitTimeAsync方法前使用await关键字,这样可以将一连串的回调改成同步的形式。这样一来代码显得十分简洁,开发起来也方便多了。 | ||
|
||
这里还有个技巧,我们发现WaitTime中需要将tcs.SetResult扔回到主线程执行,微软给我们提供了一种简单的方法,参考example2_2_2,在主线程设置好同步上下文, | ||
```csharp | ||
// example2_2_2 | ||
SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance); | ||
``` | ||
在WaitTime中直接调用tcs.SetResult(true)就行了,回调会自动扔到同步上下文中,而同步上下文我们可以在主线程中取出回调执行,这样自动能够完成回到主线程的操作 | ||
```csharp | ||
private static void WaitTime(int waitTime, TaskCompletionSource<bool> tcs) | ||
{ | ||
Thread.Sleep(waitTime); | ||
|
||
tcs.SetResult(true); | ||
} | ||
``` | ||
如果不设置同步上下文,你会发现打印出来当前线程就不是主线程了,这也是很多第三方库跟.net core内置库的用法,默认不回调到主线程,所以我们使用的时候需要设置下同步上下文。其实这个设计本人觉得没有必要,交由库的开发者去实现更好,尤其是在游戏开发中,逻辑全部是单线程的,回调每次都走一遍同步上下文就显得多余了,所以ET框架提供了不使用同步上下文的实现ETTask,代码更加简洁更加高效,这个后面会讲到。 |
Oops, something went wrong.