0. 摘要
之前我们玩了2次黄金数游戏,我也幸运的得到了一本《代码大全》,嘿嘿。这次的作业是一个Client/Server程序,自动化完成多轮重复游戏。
我完成了Client部分,使用C#编写。下面简要阐述。
1. 总体设计:
思考后,我认为这个客户端程序要能满足如下要求:
1. 保证信息传输到服务器。如果发送的信息没有得到相应,应可以不断重试。
2. 一定的错误恢复能力,当因网络问题错过某些回合,应该可以跳过而继续运行。
3. 恰当的算法,提供相对准确的黄金数字预测。
4. 具有自动获取可用端口能力,使得20个客户端同时开启能够不冲突的与服务器连接。
除此之外,为了确保游戏执行期间不出现漏洞(如假冒身份发送数字等),客户端还具有如下考虑:
1. 游戏开始前,使用ID和密码注册。
2. 等待回合开始;只有当接收到服务器新回合开始的消息,且新回合的序号大于上一次的序号时,才开始计算和条过程。提交信息附带了本客户端的用户名密码。当然,服务器也可以根据客户端的IP和端口判断身份。这里考虑万一不得不更换网络并继续进行回合的情况。
3. 每次发送消息均要等待服务器返回确认信息,并且是当前消息的确认信息。在未收到反馈时,启用一个线程不断重试发送。
4. 当服务器停止游戏,客户端不再进入发送线程和等待线程。
5. 客户端可以停止游戏。不论如何停止客户端,客户端需要正常告知服务器退出游戏。以避免服务器重试发送新回合开始消息。
6. 客户端提供GUI,可自定义服务器IP和端口,不填写默认为本地。随时显示当前状态,便于观察和调试。
2. 端口设计
基于上述考虑,我的客户端/服务器通讯协议为:
str = TYPE + ";" + OP1 + ";" + OP2 + ";" + OP3;
这是一条类似指令的收发字符串。各各数值使用分好分割。其中TYPE指明消息类型,包含以下信息:
客户端注册,
服务器注册确认,
服务器开始新回合,
客户端提交数字,
服务器确认收到提交数字,
服务器停止游戏。
具体来说,我们为每一个消息举个例子:
1. 客户端以ID为11061128,密码123456向服务器注册:
str = "1;11061128;123456"
2.服务器收到这个注册,注册成功:
str = "2"
3.服务器开始第5个回合,开始提交数字。上一回合的黄金数是17:
str = "3;5;17"
4.客户端提交第5回合的数字11,并验证身份:
str = "4;5;11;11061128;123456"
5.服务器收到第5回合提交的数字,正在等待他人提交:
str = "5;5"
6.服务器停止游戏:
str = "6";
3. 线程调度
客户端完成相关逻辑是通过一个控制线程调动其他线程的起止。线程有:
control线程:循环的状态机,直到停止。此线程一直运行。
receive线程:需要接受服务器确认结果时候,一直运行直到收到相应信息,完成等待。
其他线程:完成各自信息发送的功能,与前两者同时进行。当收到相应确认信息时,线程停止,不再重试发送信息。(一遍不断的发信息,一遍看服务器收到没有)。
4. 测试结果
经过为服务器的测试,客户端已经可以正确运行。下图的客户端Log显示了状态机、信息收发的情况,正常。通过服务器读取客户端发来的消息,并发送正确指令,测试正常:
客户端:
服务器显示客户端发来的连接和信息:
5. 主要代码
下面是主要代码部分。该部分代码的重点是状态机、线程调度和信息收发:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Threading.Tasks; 9 using System.Windows.Forms; 10 using System.Net; 11 using System.Net.Sockets; 12 using System.Threading; 13 using System.IO; 14 15 namespace AsyncTcpServer 16 { 17 public partial class mainForm : Form 18 { 19 20 const int STATE_REGISTER = 1; 21 const int STATE_REGISTER_OK = 2; 22 const int STATE_NEW_ROUND = 3; 23 const int STATE_SUBMIT = 4; 24 const int STATE_SUBMIT_OK = 5; 25 const int STATE_SERVER_STOP = 6; 26 const int INTERVAL = 3000; 27 28 const string TYPE_REGISTER = "1"; 29 const string TYPE_REGISTER_OK = "2"; 30 const string TYPE_NEW_ROUND = "3"; 31 const string TYPE_SUBMIT = "4"; 32 const string TYPE_SUBMIT_OK = "5"; 33 const string TYPE_SERVER_STOP = "6"; 34 35 const string DEFAULT_SERVER_IP = "192.168.1.2"; 36 const int DEFAULT_SERVER_PORT = 51888; 37 const string DEFAULT_USER_ID = "11061128"; 38 const string DEFAULT_USER_PASSWD = "123456"; 39 40 private string ip_input = DEFAULT_SERVER_IP; 41 private int port = DEFAULT_SERVER_PORT; 42 private string id = DEFAULT_USER_ID; 43 private string passwd = DEFAULT_USER_PASSWD; 44 45 private int tstate = 2; 46 private int State = STATE_REGISTER; 47 private int Round = 0; 48 private int[] PrevRslt = new int[5000]; 49 private int PrevRslt_idx = 0; 50 private int GoldPoint; 51 private string[] rcvs; 52 private bool isRunning = false; 53 54 private TcpClient client = null; 55 private StreamWriter sw; 56 private StreamReader sr; 57 private Service service; 58 private NetworkStream netStream; 59 60 Thread threadRegister; 61 Thread threadSubmit; 62 Thread threadReceive; 63 Thread threadWaitNewRound; 64 Thread threadControl; 65 66 public mainForm() 67 { 68 InitializeComponent(); 69 service = new Service(lb_log, sw); 70 } 71 72 private void btn_start_Click(object sender, EventArgs e) 73 { 74 if (isRunning == true) 75 { 76 service.SetListBox("Already running, Press STOP first"); 77 return; 78 } 79 80 if (String.Compare(tb_svr_IP.Text, "IP") != 0) 81 { 82 ip_input = tb_svr_IP.Text.Trim(); 83 } 84 if (String.Compare(tb_svr_port.Text, "PORT") != 0) 85 { 86 port = Int32.Parse(tb_svr_port.Text.Trim()); 87 } 88 if (String.Compare(tb_id.Text, "ID") != 0) 89 { 90 id = tb_id.Text.Trim(); 91 } 92 if (String.Compare(tb_passwd.Text, "PASSWORD") != 0) 93 { 94 passwd = tb_passwd.Text.Trim(); 95 } 96 IPAddress serverIP = IPAddress.Parse(ip_input); 97 client = new TcpClient(); 98 try 99 { 100 client.Connect(serverIP, port); 101 } 102 catch (System.Exception ex) 103 { 104 service.SetListBox(ex.Message); 105 return; 106 } 107 try 108 { 109 netStream = client.GetStream(); 110 } 111 catch (System.Exception ex) 112 { 113 service.SetListBox(ex.Message); 114 return; 115 } 116 sr = new StreamReader(netStream, System.Text.Encoding.UTF8); 117 sw = new StreamWriter(netStream, System.Text.Encoding.UTF8); 118 service = new Service(lb_log, sw); 119 isRunning = true; 120 threadControl = new Thread(new ThreadStart(control)); 121 threadControl.Start(); 122 } 123 124 private void control() 125 { 126 threadRegister = new Thread(new ThreadStart(Register)); 127 //threadSubmit = new Thread(new ThreadStart(Cal_and_Submit)); 128 threadReceive = new Thread(new ThreadStart(ReceiveData)); 129 //threadWaitNewRound = new Thread(new ThreadStart(WaitNewRound)); 130 //threadWaitNewRound.Start(); threadReceive.Suspend(); 131 //threadSubmit.Start(); threadSubmit.Suspend(); 132 133 while (isRunning == true) 134 { 135 if (tstate == 1) //wait finish 136 { 137 threadWaitNewRound.Abort(); 138 tstate = 0; 139 } 140 else if(tstate == 0)//waiting 141 { 142 continue; 143 } 144 //nothing to wait 145 switch (State) 146 { 147 case STATE_REGISTER: 148 threadRegister.Start(); 149 threadRegister.Join(); 150 State = STATE_REGISTER_OK; 151 break; 152 case STATE_REGISTER_OK: 153 threadWaitNewRound = new Thread(new ThreadStart(WaitNewRound)); 154 threadWaitNewRound.Start(); 155 threadWaitNewRound.Join(); 156 tstate = 2; 157 State = STATE_NEW_ROUND;///// 158 break; 159 case STATE_NEW_ROUND: 160 threadSubmit = new Thread(new ThreadStart(Cal_and_Submit)); 161 threadSubmit.Start(); 162 threadSubmit.Join(); 163 State = STATE_SUBMIT_OK; 164 break; 165 case STATE_SUBMIT_OK: 166 threadWaitNewRound = new Thread(new ThreadStart(WaitNewRound)); 167 threadWaitNewRound.Start(); 168 threadWaitNewRound.Join(); 169 State = STATE_NEW_ROUND; 170 tstate = 2; 171 break; 172 case STATE_SERVER_STOP: 173 service.SetListBox("Server Stop"); 174 endMission(); 175 break; 176 default: 177 break; 178 } 179 } 180 } 181 182 private void ReceiveData() 183 { 184 while (isRunning == true) 185 { 186 string receiveString = null; 187 try 188 { 189 receiveString = sr.ReadLine(); 190 } 191 catch (Exception e) 192 { 193 service.SetListBox(e.Message); 194 } 195 196 if (receiveString == null) 197 { 198 // 199 service.SetListBox("wait to re receive"); 200 // 201 Thread.Sleep(INTERVAL); 202 continue; 203 } 204 // 205 service.SetListBox("recieved" + receiveString); 206 // 207 rcvs = receiveString.Split(';'); 208 switch (rcvs[0]) 209 { 210 case TYPE_REGISTER_OK: 211 State = STATE_REGISTER_OK; 212 break; 213 case TYPE_SUBMIT_OK: 214 if (Round == Int32.Parse(rcvs[1])) 215 { 216 State = STATE_SUBMIT_OK; 217 } 218 break; 219 case TYPE_NEW_ROUND: 220 if (Round == Int32.Parse(rcvs[1]) - 1) 221 { 222 State = STATE_NEW_ROUND; 223 } 224 break; 225 case TYPE_SERVER_STOP: 226 State = STATE_SERVER_STOP; 227 service.SetListBox("Server Stop"); 228 tstate = 2; 229 endMission(); 230 break; 231 default: 232 break; 233 } 234 } 235 } 236 237 private void Register() 238 { 239 threadReceive.Start(); 240 String str = TYPE_REGISTER + ";" + id + ";" + passwd; 241 service.SendToServer(str); 242 while (isRunning && State != STATE_REGISTER_OK) 243 { 244 // 245 service.SetListBox("re register"); 246 // 247 Thread.Sleep(INTERVAL); 248 service.SendToServer(str); 249 } 250 threadReceive.Suspend(); 251 } 252 253 private void WaitNewRound() 254 { 255 threadReceive.Resume(); 256 while (isRunning && State != STATE_NEW_ROUND) 257 { 258 // 259 service.SetListBox("waiting"); 260 // 261 Thread.Sleep(INTERVAL); 262 } 263 Round++; 264 PrevRslt[PrevRslt_idx++] = Int32.Parse(rcvs[1]); 265 threadReceive.Suspend(); 266 tstate = 1; 267 } 268 269 private void Cal_and_Submit() 270 { 271 threadReceive.Resume(); 272 GoldPoint = calculate(); 273 string str = TYPE_SUBMIT + ";" + Round.ToString() + ";" + GoldPoint.ToString(); 274 service.SendToServer(str); 275 while (isRunning && State != STATE_SUBMIT_OK) 276 { 277 Thread.Sleep(INTERVAL); 278 // 279 service.SetListBox("submitting"); 280 // 281 service.SendToServer(str); 282 } 283 threadReceive.Suspend(); 284 } 285 private int calculate() 286 { 287 // 288 service.SetListBox("calculation"); 289 // 290 if (Round == 1) 291 { 292 return 100; 293 } 294 else 295 return (int)(PrevRslt[PrevRslt_idx] * 0.618); 296 } 297 298 private void btn_stop_Click(object sender, EventArgs e) 299 { 300 endMission(); 301 } 302 303 private void mainForm_FormClosing(object sender, FormClosingEventArgs e) 304 { 305 endMission(); 306 } 307 308 private void endMission() 309 { 310 // 311 service.SetListBox("ending mission"); 312 // 313 if (isRunning) 314 { 315 netStream.Close(); 316 client.Close(); 317 } 318 isRunning = false; 319 } 320 } 321 }
下一次作业,客户端将完善线程操作(弃用已经过时的suspend和resume方法),并使用最小二乘法逼近黄金点曲线的方法来预测数值。