C#版网络对战五子棋以及Socket通信_c#五子棋编程对等协议-程序员宅基地

技术标签: Windows 编程  C#  网络编程  五子棋  Socket  

前言

    这个网络版五子棋游戏是今年四月初写的。当时觉得自己应该学一些网络编程的东西。而我课程设计的题目已经定了———做一个Everything。 那就帮我斐哥做个网络版的五子棋吧。

源码:https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ

    界面是WinForm的,使用GDI绘图来完成棋盘与棋子的绘制,落子坐标通过定义的公式来计算。我原先做过人机对战版的五子棋,因此游戏逻辑这个最重要的部分并没有花很多时间。这个程序一个多星期就搞的差不多了。
在这里插入图片描述
在这里插入图片描述
不过现在看来,当时的代码太青涩了,一是课程设计马上就要中期检查没太多时间,二是水平和眼界确实不高。
比如:

  • 消息对象的序列化。那时候不知道有JSON序列化,所以自己就写了个ToString()方法,对方收到之后,解析出字符串,再Split,重建实体。
  • 消息处理。在Switch里写大量的逻辑代码,来处理不同类型的消息。
  • 以及大量Bug等。

源码:https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ

设计

    玩家对战与人机对战的区别其实就是将玩家A的操作发送给玩家B,玩家B那边的界面渲染。我将游戏里的操作指令封装为了枚举类型。

public enum MsgType
{
    LuoZi=0,//玩家落子
    Connect=1,//玩家上线
    Quit=2,//玩家退出房间
    IsWin=3,//是否胜利
    CreateRoom=4,//创建房间
    JoinRoom=5,//加入房间
    UserList=6,//请求|发送玩家列表
    RoomList,//请求|发送房间列表
    Other,//其他
    Start,//开始游戏
    Exit,//玩家连接断开
    OtherName,//忘了干嘛的了
    Restart,//重新开始游戏
    Msg//聊天
}

消息对象:

public class MessagePackage
{
    public MsgType msgType;
    public string data;
    public string senderIP = "";
    public string senderName = "";
    public string sendTime;

    public MessagePackage()
    {

    }

    public MessagePackage(string msg)
    {
        string[] msgs = msg.Split('|');
        msgType = (MsgType)int.Parse(msgs[0]);
        data = msgs[1];
        senderIP = msgs[2];
        senderName = msgs[3];
        sendTime = msgs[4];
    }

    public MessagePackage(MsgType msg, string data, string senderIP, string senderName, string sendTime)
    {
        this.msgType = msg;
        this.data = data;
        this.senderIP = senderIP;
        this.senderName = senderName;
        this.sendTime = sendTime;
    }

    public string ConvertToString()
    {
        string msg = ((int)msgType).ToString() + "|" + data + "|" + senderIP + "|" + senderName + "|" + sendTime;
        return msg;
    }


}

客户端逻辑

  • GDI绘制
  • 游戏逻辑
  • 登录建房
  • 加入开始
  • 结束重来
  • 聊天信息
  • 退出
    -在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    我决定举一个最基本的栗子———游戏逻辑中的玩家落子。

落子

    进入游戏房间后,我会用GDI画出15*15的棋盘。使用过GDI的朋友都知道,它是根据像素为单位的,这样做是不简单的。

    比如你想将棋子落在棋盘上(7,7)这个点上,那就需要用GDI来画一个白色的棋子在那个位置上。GDI提供的绘圆方法是什么呢?FillEllipse,你需要指定一个长方形,包括这个长方形左上角的横纵坐标,以及它的长和宽,以及填充的颜色。这个方法才能为你画出这个长方形里最大的那个圆,或是椭圆。

    private bool GraphicsPiece(Point upleft, Color c)
    {
        Graphics g = this.panel1.CreateGraphics();
        if (upleft.X != -1 || upleft.Y != -1)
        {
            g.FillEllipse(new SolidBrush(c), upleft.X, upleft.Y, CheckerBoard.chessPiecesSize, CheckerBoard.chessPiecesSize);
            return true;
        }
        return false;
    }

    重点就是这个长方形的左上角坐标怎么得到?我们知道鼠标点击事件中,参数Args带给我们的是一个以像素为单位的,相对与绘图区的位置。而且你不能指望用户正好点在棋盘的那个点上,他可能点在(7,7)上面一点,或是下面一点。因此我们就需要对鼠标点击的坐标值就行处理,将其转化相对的表现形式(7,7)。

将像素坐标转化成相对坐标:
    public static Piece ConvertPointToCoordinates(Point p,int flag)
    {
        int x, y;
        Piece qi;
        if (p.X<leftBorder||p.Y<topBorder||p.X>(lineNumber-1)*distance+leftBorder|| p.Y > (lineNumber - 1) * distance + topBorder)
        {
             qi= new Piece(-1,-1,flag);
        }
        else
        {
            float i = ((float)p.X - leftBorder) / distance;
            float j= ((float)p.Y - topBorder) / distance;
            x = Convert.ToInt32(i);
            y = Convert.ToInt32(j);
            if (GameControl.ChessPieces[x, y] != 0)
            {
                qi = new Piece(-1, -1, flag);
            }
            else
            {
                qi = new Piece(x, y,flag);                  
            }              
        }
        return qi;
    }
将相对坐标转化成像素坐标:
    public static Point ConvertCoordinatesToPoint(Piece p)
    {
        int x, y;
        x = p.X * distance + leftBorder - chessPiecesSize / 2;
        y = p.Y * distance + topBorder - chessPiecesSize / 2;
        return new Point(x, y);
    }
落子:绘制本地棋子并将相对坐标发送给服务器;如果取得胜利,则发送胜利消息给服务器,服务器根据房间信息,查找到对手玩家,发送消息给对手玩家。
    Piece p = CheckerBoard.ConvertPointToCoordinates(new Point(e.X, e.Y), 1);
            if (p.X != -1)
            {
                Point point = CheckerBoard.ConvertCoordinatesToPoint(p);
                if (Program.gc.AddPiece(p))
                {
                    GraphicsPiece(point, myColor);
                    MessageBox.Show("黑棋获胜");
                    return;
                }
                else
                {
                    GraphicsPiece(point, myColor);
                    p = Program.gc.MachineChoose();
                    point = CheckerBoard.ConvertCoordinatesToPoint(p);
                    if (Program.gc.AddPiece(p))
                    {
                        GraphicsPiece(point, otherColor);
                        turnFlag = true;
                        MessageBox.Show("白棋获胜");
                        return;
                    }
                    GraphicsPiece(point, otherColor);
                    lbmyscore.Text = (0 - Program.gc.GetScore()).ToString();
                    lbhisscore.Text = Program.gc.GetScore().ToString();
                    turnFlag = true;
                }
            }
对方收到落子消息后
    case MsgType.LuoZi:
                {
                    string[] qi = mp.data.Split(',');
                    int x = int.Parse(qi[0]);
                    int y = int.Parse(qi[1]);
                    Piece p = new Piece(x, y, 3 - flag);
                    Point point = CheckerBoard.ConvertCoordinatesToPoint(p);
                    if (Program.gc.AddPiece(p))
                    {                           
                        GraphicsPiece(point, otherColor);                            
                        start = false;
                        btnStart.Enabled = true;
                        MessageBox.Show("对方获胜");
                    }
                    else
                    {
                        GraphicsPiece(point, otherColor);
                        turnFlag = true;
                    }                        
                    break;
                }

    将相对坐标转化成本地像素坐标,绘制棋子,然后本人落子。

服务器设计

    没有考虑很多,实现“上传下达”的功能就好了。

  • 消息转发
  • 控制用户数量
  • 维护房间列表信息
  • 维护用户列表信息
  1. 比如,玩家断开连接:要及时从玩家列表清理,更新列表,并发送给在线的玩家。
  2. 比如,玩家退出房间:查找到该房间,更新房间信息,发送给在线玩家
    在这里插入图片描述

举个栗子

    相对于客户端而言,服务端的代码量少很多,除了通用的代码,大概四百行左右。
某玩家退出某房间
    case MsgType.Quit:
                {
                    GameRoom r = SearchRoomBySenderName(mp.senderName);
                    GamePlayer p = SearchUserByName(mp.senderName);
                    r.QuitRoom(p);
                    if (r.PlayerNumber == 0)
                        roomList.Remove(r);
                    else
                    {
                        mp = new MessagePackage(MsgType.Quit, "", "", "", "");
                        tcpServer.Send(r.RoomMaster.Session, mp.ConvertToString());                          
                    }
                    mp = new MessagePackage(MsgType.RoomList, GetRoomList(), "", "", DateTime.Now.ToString());
                    foreach (Session session in tcpServer.SessionTable.Values)
                    {
                        tcpServer.Send(session, mp.ConvertToString());
                    }
                    break;
                }
  1. 根据玩家名称,从房间列表该找到房间。
  2. r.QuitRoom§: 判断该玩家是不是房主,是:将另一名玩家提升为房主;finally:从房间中清除该玩家。
  3. 若房间玩家全部退出,删除该房间
  4. 发送新的房间列表信息给所有玩家。

在这里插入图片描述

Socket通信

    重点来了,我开头就说要学网络编程的。最后简单介绍一下C#中Socket编程。当然,C#也提供了更高级别的封装如TcpClient,TcpListener。以及更高性能的异步套接字:SocketAsyncEventArgs。

服务端Socket

    mainSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    mainSocket.Bind(new IPEndPoint(IPAddress.Any, 4396));
    mainSocket.Listen(5);
    mainSocket.BeginAccept(new AsyncCallback(AcceptConn), mainSocket);
  1. 新建Socket实例:指定使用IPv4,流传输,TCP协议。

  2. 绑定到本机,4396端口

  3. 开始监听,连接队列最大为5

  4. 将AcceptConn函数注册为连接回调函数。回调函数必须接收一个类型为IAsyncResult的参数。

     mainSocket.BeginAccept(new AsyncCallback(AcceptConn), mainSocket);
    
BeginAccept会阻塞当前线程。当有连接进入后,将mainSocket封装为作为IAsyncResult对象,作为参数传递给AcceptConn。

连接回调函数AcceptConn的用法

    protected virtual void AcceptConn(IAsyncResult iar)
    {          
        Socket Server = (Socket)iar.AsyncState;
        Socket client = Server.EndAccept(iar);
        if (clientCount == maxClient)
        {
            ServerFull?.Invoke(this, new NetEventArgs(new Session(client)));
        }
        else
        {
            Session clientSession = new Session(client);
            sessionTable.Add(clientSession.SessionId, clientSession);
            clientCount++;
            clientSession.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(ReceiveData), clientSession.Socket);
            ClientConn?.Invoke(this, new NetEventArgs(clientSession));
            Server.BeginAccept(new AsyncCallback(AcceptConn), Server);
        }
    }
  1. 从IAsyncResult中获取到mainSocket,并结束异步操作。这是较为经典的异步编程模型写法。

  2. 服务器满,触发ServerFull事件,通知客户端无法进入。

  3. 服务器未满,将接入的socket连接进行封装,加入到玩家集合中

  4. 开始接收该Socket的消息

     clientSession.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(ReceiveData), clientSession.Socket);
    
     BeginReceive函数有多种重载形式,看看说明不难理解。
    
  5. 服务端继续监听连接

     Server.BeginAccept(new AsyncCallback(AcceptConn), Server);
    

客户端Socket

  1. 连接

     Socket newSoc = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
     IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse(ip), port);
     newSoc.BeginConnect(remoteEP, new AsyncCallback(Connected), newSoc);
    
  2. 发送

     public virtual void Send(string datagram)
     {
         if (datagram.Length == 0)
         {
             return;
         }
         if (!isConnected)
         {
             throw (new ApplicationException("没有连接服务器,不能发送数据"));
         }
         //获得报文的编码字节 
         byte[] data = coder.GetEncodingBytes(datagram);
         session.Socket.BeginSend(data, 0, data.Length, SocketFlags.None,new AsyncCallback(SendDataEnd), session.Socket);
     }
    
  3. 接收

      session.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(RecvData), socket);
    

结尾

当然实际编程的时候会遇到好多问题,比如:

  1. Socket连接正常断开和异常断开的问题。
  2. 事件驱动模型中,事件侦听程序不再直接引用,发布程序仍会有引用存在,垃圾回收器就不能对其进行回收。当多个界面都存在事件侦听操作时,会发生混乱。等。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40404477/article/details/100803543

智能推荐

Java程序员面试失败的五大原因!_java程序员面试:工作中有没有碰到比较棘手的问题,你是怎么解决的-程序员宅基地

文章浏览阅读3.5k次。微信公众号:javafirst下面是 Java 程序员面试失败最有可能的5大原因,当然也许这5点原因适用于所有的程序员,所以,如果你是程序员,请认真阅读以下内容。看点01说得太少尤其是那些开放式的问题,如“请介绍下你自己”或“请讲一下你曾经解决过的复杂问题”。面试官会通过你对这些技术和非技术问题的回答来评估你的激情。他们也会通过模拟团队氛围和与你的交流互动来判断你的经验和能力。所以,仅仅只用两三句..._java程序员面试:工作中有没有碰到比较棘手的问题,你是怎么解决的

EMMC驱动中常用命令说明及初始化顺序_mmc_send_cid-程序员宅基地

文章浏览阅读1.4k次。EMMC驱动中常用命令说明及初始化顺序一、命令说明mmc_go_idle发送CMD0指令,GO_IDLE_STATE使mmc card进入idle state。虽然进入到了Idle State,但是上电复位过程并不一定完成了,这主要靠读取OCR的busy位来判断,而流程归结为下一步。mmc_send_op_cond发送CMD1指令,SEND_OP_COND这里会设置card的工作电..._mmc_send_cid

3ds Max随堂笔记 二维、三维、世界空间修改器_由二维图纸转换为三维模型的插件-程序员宅基地

文章浏览阅读2.6k次,点赞2次,收藏6次。 2022.3.28 早十文章目录4.1 修改器建模综述4.2 修改器命令面板4.3 二维图形编辑修改器4.4 三维图形编辑修改器4.4.1弯曲修改器4.4.2锥化修改器4.4.3扭曲修改器4.4.4FFD修改器(自由变形)5.1 世界空间修改器概述5.2 路径变形修改器5.2 毛发修改器4.1 修改器建模综述#mermaid-svg-12QA56qYK5ChV8B7 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16p_由二维图纸转换为三维模型的插件

带你们了解数据安全探索者之路-程序员宅基地

文章浏览阅读202次。数据是新时代的生产要素;保护数据原生价值,实现数据的所有权保护、交换与管理;完善数据在收集、使用、存储等阶段的全生命周期安全;研究分析复杂物理数据交互场景中的数据安全攻防机理;在保护数据所有权的前提下实现高价值数据的安全交易;安全技术标准的推广与法律法规的完善。一、数据安全防护是重大战略需求当前的行业共识认为数据是驱动数字经济发展的核心动力。以数据为基础的云计算、物联网、区块链、人工智能等经济生态及相关产业链在智慧城市升级、国家重大基建产业发展等方面发挥着积极的作用。《中国数字经济发展白皮书》显示,2.

深度强化学习(DRL)算法系列文章合集-程序员宅基地

文章浏览阅读1.1k次,点赞34次,收藏9次。这个系列介绍了常用的单智能体强化学习方法,也有些没有写到,比如 SAC,希望以后有时间可以回来补完。下个系列会开始介绍 RLXF(包括 RLHF、RLAIF)欢迎关注。奋斗,追求,不达目的,誓不罢休!

html5黑色大气的个人博客全屏滚动个人主页源码HTML+JS+CSS-程序员宅基地

文章浏览阅读666次,点赞7次,收藏6次。html5黑色大气的个人博客全屏滚动个人主页源码HTML+JS+CSS

随便推点

LWIP互联网资料汇总_lwip cli-程序员宅基地

文章浏览阅读1k次。LWIP互联网资料汇总分类: UCOSII/LWIP2013-01-01 11:06 4946人阅读 评论(2) 收藏 举报目录(?)[+]本文主要搜集了下互联网上关于LWIP的资料和教程欢迎补充第一部分:移植LWIP在UCOS上移植LWIP 在STM32上移植 http://www.docin.com/p-459242028_lwip cli

托福写作4-the entertainer or athlete you most want meet_famous enterainer and athlete-程序员宅基地

文章浏览阅读432次。if you could meet a famous entertainer or athlete, who would that be , and why?_famous enterainer and athlete

骨龄测试软件app_【测试工具】这些APP实用测试工具,不知道你就out了!-程序员宅基地

文章浏览阅读2.4k次。本期,我将给大家介绍14款实用的测试工具,希望能够帮到大家!(建议收藏)UI自动化测试工具1.uiautomator2Github地址:https://github.com/openatx/uiautomator2star:1.9k介绍:openatx开源的ui自动化工具,支持android和ios。主要面向的编程语言是python,api设计简洁易用,在开源社区也是很受欢迎。原理图:与appi..._测骨龄软件

STM8学习笔记---串口uart1_stm8串口发送字符串-程序员宅基地

文章浏览阅读1.4w次,点赞3次,收藏14次。使用uart1串口,需要用到stm8s_uart1.c和stm8s_uart1.h两个文件1、建立工程目录结构如下:2、编写uart.h文件如下:#ifndef __UART_H#define __UART_H#include "stm8s.h"#include "stm8s_clk.h"void USART_Configuration(void); //串口配置函数void UART_sen..._stm8串口发送字符串

Sass语法学习-程序员宅基地

文章浏览阅读275次,点赞4次,收藏9次。自动监控把sass编译成css文件,命令行在监控的sass后面,可以为 sass 生成 css 样式指定生成的格式,默认是nested型;通过 --style nested( 嵌套 - 默认 )|compact( 紧促型 )|compressed( 压缩 )|expended( 扩展 ) 命令,可以为 sass 生成 css 样式指定生成的格式2、合成文件@improt在sass中,使用@improt可以把多个不同的sass文件合成一个css文件,在合成的sass中有两种方式,

Struts2的优点_struts2框架优点-程序员宅基地

文章浏览阅读5.5k次。首先,我们得知道struts2是什么,那我们才知道这个struts2有什么优缺点,是吧。所以,我先来解释一下struts2到底是什么。Apache Struts是一个免费,开源,MVC框架, 现代Java web应用框架。 它有利于约定优于配置, 可扩展的使用一个插件架构,并附带插件的支持 休息,AJAX和JSON。 所以呢,针对于struts2是什么,我们就可以知道struts2有什么优点了。_struts2框架优点

推荐文章

热门文章

相关标签