Fork me on GitHub

.NetPK系统

项目简介

前些天公司接到一个在线PK的项目,用户需求是模仿微信小程序的《头脑王者》,不过会比这个简单些,题目是公司内部拟定好的,都是关于疾病之类的比如糖尿病、心脏病等等。还有就是项目是在微信公众号上跳转到的WEB页面,庆幸的是微信公众号开发我还是比较熟悉的,可怕的是用户强烈要求用C#语言,运行环境是Windows Server 2008 、IIS8、SQL Server 2008,用户数量在500左右。好吧我承认我没有做过C#开发,不过还能回忆起遥远的过去在学校学的一点点可能有用的知识,对我来说这是一个挑战,不过好在用户数量很少,这是他们公司内部员工用的软件,真实哔了狗了。

需求分析

根据和客户的对接内容,大概是想要一个前端程序和一个后台管理系统。基本功能为用户通过关注微信公众号之后可以点击链接进入到游戏中心。

其中前端主要包含以下四个模块:

  1. 王者训练场:用户平常练习的场所,每道题回答成功后会获得相应的分数。
  2. 全民PK:用户和用户之间PK的场所,里面包括实时PK和离线PK。
  3. 个人中心:可以查看自己的基本资料和得分情况。
  4. 排行榜:可以查看所有用户的排行情况以及自己的周排名和月排名。

后台主要包含以下四个模块:

  1. 题库管理:用户可以查看和编辑题库。
  2. 广告管理:所谓的广告就是用户在答题个过程中,在题目下发会显示一行文字广告。这里可以编辑广告内容。
  3. 用户管理:管理参与答题的用户和查看答题情况,并每月或者每周提取前20名进行线下奖励,所以这里需要进行按月或者按周排序。
  4. PK管理:查看用户的PK情况以及用户在PK过程中参与答题的情况。

根据以上描述大概整理了两个对应的脑图,我是业余的请忽略美观性:

基础设计

  • 前端:好在客户提供微信端的设计图,PSD格式的文件。我们开发者自己来切图即可,这样我们就省去了UI设计的时间,我们只是数据的搬运工。

  • 后台:后台只有一个管理员维护,对UI要求不高,方便操作即可,我想这是大部分管理后台的通用模式。

  • 开发环境:好吧,我只是iOS开发者,用的是MacPro我尝试用Mac版本的Visual Studio,但是官方上面好像写的是测试版本;而且Mac版本的主打.net core,对.net MVC并不友好而且对数据库之类的操作不太方便;最主要的是IIS环境。好吧我放弃了Mac版本。what the f**k 安装VMware ->Windows Server 2012->IIS8 ->Microsoft Visual Studio,😓😓😓忙活了一下午甚至加了个班,创建个.net MVC可以跑起来了。(我没用.net core)的原因主要是客户那边有技术团队在维护微信公众号,用的是.net MVC,我避免有坑,所有我也照旧了。

  • 数据库:此处省略。

开发

一名同事加入了,他负责微信端所有的页面,其余的后台和接口我来负责😭😭😭。
啪啪啪…
啪啪啪…
啪啪啪…
撞击声伴随着做出东西的快感…
此处省略500字…
一遍又一遍的调试,终于功夫不负有心人,两个周的时间搞定,内部测试通过用户测试没啥问题,接下来就是调整需求了😆😆😆😆(此处不讲了,甲方还是那个甲方)

接下来上效果:

后台:
.Net




前端:






难点

我认为此项目的难点在于PK模块,经过多方查阅资料,也没有找出个所以然来。于是我采取了WebSocket + Timer的方式,这样对于500多用户的应该没什么问题。WebSocket 负责保持和用户通讯,Timer每秒负责循环处理用户的数据。

1.首先创建enum PKUserStatus 用来表示用户的几种状态,这个枚举表示从用户链接成功到PK结束的所有状态:

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
public enum PKUserStatus
{
/**
* 等待匹配
*/
WAIT_PAIR = 0,

/**匹配成功,等待客户端确认
*/
WAIT_PAIR_SUCCESS,

/**匹配失败,可能准备关闭连接
*/
WAIT_PAIR_FAILURE,

/**客户端已经确认,等待服务端发题
*/
WAIT_ISSUE,

/**发题
*/
ISSUE,

/**等待客户端回答
*/
WAIT_ANSWER,

/**胜利
*/
WIN,

/**失败
*/
FAIL,

/**无效
*/
INVALID,

/**平局
*/
//DRAW,
}

2.创建User类:基本的用户对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PKUser
{
//进入系统时间
public DateTime connectTime { get; set; }

//userId
public int userId { get; set; }

//用户当前的状态
private PKUserStatus __status { get; set; }

//回答题目累计用时
public int answerTime { get; set; }

//正在连接的socket
public WebSocket webSocket { get; set; }

//...
根据需要的参数或者方法
...//
}

3.创建PKContext类:类似于房间的概念,每个PKContext表示一个PK场次

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
 public class PKContext
{
//当前PK所需要回答的题目
private List<question_info_vo> questions { get; set; }

//当前正在回答的题目,回答规则是双方同时获得新题目,如果一方回答完,则会等待另一方回答结
束才会有新的试题。
private int currentIndex = 0;

//仅支持2人pk,所以这里直接定义了两个user对象
public PKUser user1 { get; set; }
public PKUser user2 { get; set; }

//获取下一道试题,如果已经为最后一道,则返回null
public question_info_vo nextQuestion()
{
if (currentIndex < questions.Count())
{
return questions[currentIndex++];
}
return null;
}

//保存pk信息
private void insertPKInfo()
{
//...
}

//结算分数
public void insertUserScore()
{
//...
}

//用户答题
public void answers(PKUser user, Dictionary<string, object> options)
{
//...
}

public int score(PKUserStatus staus)
{
return 0;
}
//...
其他方法
...//

}

4.接下来的流程控制在控制器实现即可:

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
public class WSChatController : ApiController
{

/**
* 用户连接池,等待匹配的用户,没有匹配的用户在这里暂存,
* 最大存活60s,过期后服务器即断开此用户的链接
*/
private static List<PKUser> USER_POOL = new List<PKUser>();

/**
* 用户pk池,正在进行的pk保存在这里,
* pk结束后需要移除
*/
private static List<PKContext> PK_POOL = new List<PKContext>();

/**
* 计时器,每隔1s中会轮询各个状态的用户,同时会向用户发送响应的数据
* 弊端在于定时可能会不太精确,或者在回调函数里面操作耗时的动作会影响业务流程,
* 不过对于本系统绰绰有余了
*/
private static Timer timer = new Timer(new TimerCallback(timerCallback), null, 1000, 1000);


public HttpResponseMessage Get()
{
if (HttpContext.Current.IsWebSocketRequest)
{
HttpContext.Current.AcceptWebSocketRequest(ProcessWSChat);
}
return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
}

private async Task ProcessWSChat(AspNetWebSocketContext arg)
{

WebSocket socket = arg.WebSocket;
int userId = int.Parse(arg.QueryString["user"].ToString());

//...
此处需要判断userId的有效性,忽略
...//

try
{
//新连接,添加到连接池里
USER_POOL.Add(new PKUser(/*构造函数*/));

while (true)
{
if (socket.State == WebSocketState.Open)
{
ArraySegment<byte> buffer = new ArraySegment<byte>(new Byte[2048]);
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);

//服务端收到客户端消息后,会继续往下走

try
{
if (socket.State != WebSocketState.Open)
{
//判断客户端的状态是否不是链接状态,否则会走断开用户的方法
offline(userId);
break;
}

string userMsg = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);

Dictionary<string, object> json = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(userMsg);

//此处会收集到客户端的json数据,

//...
根据数据执行相应的操作
...//

}
catch (Exception e)
{

}
}
else
{
//如果用户不是链接状态,执行断开链接方法
offline(userId);
break;
}

//让cpu休息100毫秒,对cpu来说100毫秒已经是很长时间了
Thread.Sleep(100);
}
}
catch (Exception e)
{
offline(userId);
}
}

/**
* 定时器回调函数
*/
private static void timerCallback(Object obj)
{

Dictionary<string, object> msg_dic = new Dictionary<string, object>();
string time = timestamp();

List<PKUser> removeUser = new List<PKUser>();

/**
* 处理没有匹配的User
*/
for (int i = 0; i < USER_POOL.Count(); i++)
{

//在此处遍历等待匹配的User,可以按照规则进行user匹配,
//或者移除超时的user,过程忽略
//还可以在这里给客户端发送自定义心跳包
}

/**
* 处理匹配成功的User
*/
foreach (PKContext context in PK_POOL)
{
if (context.user1.status == PKUserStatus.WAIT_PAIR_SUCCESS)
//在此处判断正在pk的用户状态,然后执行相应的业务流程即可,过程忽略
}
}

/**
* 用户掉线
*/
private static void offline(int userId)
{

//用户断线,
//需要清空用户数据等操作,比如USER_POOL、PK_POOL

}

/**
* 服务器主动断开链接发送close
*/
private static ArraySegment<byte> closeBuffer = new ArraySegment<byte>(new byte[2] { 0x88, 0x00 });
private static void sendClose(WebSocket socket)
{
//socket.CloseAsync(WebSocketCloseStatus.NormalClosure,"websocket closed for server",CancellationToken.None);
sendBuffer(socket, closeBuffer);
}

//...
其他方法
...//
}

结语

写到此处基本上算是对这个系统的核心部分搞明白了(请原谅具体实现没有写出来),至于别的接口、前端页面、数据库之类的就不叙述了,在简单不过了。
最后在吐槽一下这个pk的方案真的是太low了,不过现在跑这个系统基本上没什么问题。如果用户量比较大或者要求更高的话不建议自己写pk对战系统,最好使用商业的服务引擎,比如matchvs就不错哦!

------ 本文结束------
0%