开学了,去图书馆借了几本书,没有找到想要的C++网络编程,倒是找到了几本LINUX的书,以及一本《Visual C#经典游戏编程开发》。翻了翻发现里面有个可以联网对弈的中国象棋游戏,一直写ASP.NET的网页,也想试着写写Winform的图形程序了,而且可以了解一下.NET的网络编程相关的内容。
象棋方面,先从网上找了张棋盘图片,在PS里把棋子一一扣了出来作为素材。编写对应的棋子类和棋盘类,前者实现棋子身份的识别和棋子图片的绘制,后者实现棋盘绘制、走棋规则、棋谱生成等功能。
网络通信的协议使用的是UDP协议,发送的信息规则是:“命令|参数1|参数2……”,主要命令:join表示用户想要联机,处于接受对方连接的状态;conn表示收到对方联机命令,已准备好开始游戏,对方可以开局了;new表示其中一方提出重新开始游戏;move|id|x|y表示移动棋盘上编号为id的棋子到x,y处;succ|赢方代号 表示此局已分出胜负;exit表示退出游戏。
游戏开始后创建了一个线程,在read方法中创建UdpClient对象,循环侦听指定端口中由指定IP主机传来的信息。主要代码是:
udpClient = new UdpClient(Convert.ToInt32(txt_port.Text)); int id, x, y; while(readFlag == true) { try { byte[] data = udpClient.Receive(ref remote); string strData = Encoding.UTF8.GetString(data); string[] a = strData.Split('|'); // 分割命令与参数 switch(a[0]) // 判断命令 { case "join": case "conn": //...... } } catch { break; } }
游戏过程中发送数据则是通过send方法,创建UdpClient网络服务对象,向指定IP的主机发送消息到设定的端口号。完成后发送后,关闭UDP网络服务。主要代码:
UdpClient sendUdp = new UdpClient(); UPAddress remoteIP; try { remoteIP = IPAddress.Parse(txt_IP.Text); } catch { MessageBox.Show("请输入正确的IP地址!", "错误"); return; } remote = new IPEndPoint(remoteIP, Convert.ToInt32(txt_remoteport.Text)); byte[] buffer = Encoding.UTF8.GetBytes(str); sendUdp.Send(buffer, buffer.Length, remote); sendUdp.Close();
原书中的代码有一些小问题,所以做了点改动和简化。不过基本的思路还是和原书上是一样的。
通信过程方面,每次联机都需要双方填写好对方的IP、端口号以及自己的端口号。而且,由于没有明显的游戏过程标记,所以造成了一些通信问题,导致在不正确的阶段收到对方错误的请求时游戏出现意外的情况。
于是在完成后,就开始了对原程序的改造。考虑到内网用户连接外网的问题,就将游戏中的UDP协议通信改为了使用Socket通过TCP协议进行游戏通信。添加游戏状态state字段,包括以下几个主要状态:
public const short WAITING_CLIENT = 1; // 作为主机,等待客户端连接 public const short SERVING = 2; // 作为主机,客户端已连接,游戏中 public const short WAITING_SERVER = 3; // 作为从机,等待主机接受连接 public const short CONNECTING = 4; // 作为从机,连接到主机,游戏中
游戏的命令和监听流程也做了对应的改动。
主机建立游戏等待客户端连接的过程主要包括:监听端口、建立连接、开始监听消息,这三个步骤。首先创建线程,在线程中执行acceptClientConnect监听端口等待连接,检测到连接后建立连接,转入Read监听阶段。
private void WaitClient() { // 启动一个线程来接受请求 thread = new Thread(acceptClientConnect); thread.Start(); SetState(WAITING_CLIENT); } // 接受请求 private void acceptClientConnect() { IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, DEFAULT_PORT); listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); toolStripStatusLabel1.Text = "正在等待其他玩家连接……"; try { if (timeoutThread != null) { timeoutThread.Abort(); } timeoutThread = new Thread(WaiteTimeOut); timeoutThread.Start(); listener.Bind(localEndPoint); listener.Listen(1); connection = listener.Accept(); listener.Close(); listener = null; Read(); } catch { toolStripStatusLabel1.Text = "连接中断,游戏终止。"; SetState(FREE); } } private void WaiteTimeOut() { Thread.Sleep(30000); if (connection == null || !connection.Connected) { if (listener != null) { SetState(FREE); toolStripStatusLabel1.Text = "连接超时,无玩家连接游戏。"; } } timeoutThread = null; }
预定的监听超时时间是30秒,30秒后没有连接时,转到FREE状态并终止监听。终止的实现思路是在主机本身创建一个TCP协议的Socket对象,向自身端口发送终止连接的消息,listener建立连接后在Read中接受到此消息停止阻塞,结束游戏彻底转为FREE状态,线程结束。
private void SetState(int newState) // 支线程 { state = newState; switch (state) { case FREE: btnCreate.Text = "创建游戏"; btnCreate.Enabled = true; btnJoin.Text = "加入游戏"; btnJoin.Enabled = true; if (listener != null) { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect(IPAddress.Parse("127.0.0.1"), DEFAULT_PORT); if (socket.Connected) { socket.Send(Encoding.UTF8.GetBytes("end")); } } if (connection != null) { connection.Close(); connection = null; } break; case WAITING_CLIENT: btnCreate.Text = "关闭游戏"; btnJoin.Enabled = false; break; case SERVING: break; case WAITING_SERVER: btnJoin.Text = "断开游戏"; btnCreate.Enabled = false; break; case CONNECTING: break; } }
接受消息的Read方法和发送消息的Send方法方面,原本的UdpClient收发消息改为通过创建的connection(Socket)连接来通信。
private void Read() { int t, x1, y1, x2, y2; while (state != FREE) { try { byte[] data = new byte[1024]; int len = connection.Receive(data); string msg = Encoding.UTF8.GetString(data); string[] a = msg.Split('|'); switch (a[0]) { case "end": // 终止listener的监听 if (state != WAITING_CLIENT) continue; connection.Close(); connection = null; SetState(FREE); return; case "join": // 客户端请求加入游戏 break; case "acce": // 服务端接受加入的请求 break; case "exit": // 对方方退出游戏 break; case "new": // 对方方提出重新开局 break; case "succ": // 一方获胜 break; case "lose": // 一方认输 break; case "move": // 对方移动棋子 break; } } catch { break; } } } private void Send(string msg) { if (connection != null) { byte[] buffer = Encoding.UTF8.GetBytes(msg); connection.Send(buffer); } }
具体游戏效果截图:
项目工程下载: