一、前言

  • 最近老师要求做课设,实现一个 “炸飞机” 游戏,我是负责UI界面实现和Socket通信实现的,在这里想总结一下我实现Socket的具体过程,对其中的产生的问题和实现的方法进行进行分析。由于我是第一次具体实现Socket通信,所以走了不少弯路,请教了许多人,其中尤其是我的舍友,对我帮助很大。

二、实现思路

我采用的模式是C/S模式(客户端-服务器模式),并且是TCP模式
  • 首先是单例化对象,对客户端和服务器都进行了单例化,确保炸飞机时只有一个客户端和一个服务器(因为这个游戏是1V1嘛);
  • 然后对客户端的和服务器端 send()receive() 函数进行编写,要注意的一点是:这里不能盲目照搬网络上的代码,其代码使用场景简单,通常是发送一次接收一次(或者是发送一次一直接收),总之对本项目而言是不能适用的;
  • 再然后是封装类,封装好之后在其他命名空间中调用接口

三、具体代码

客户类代码

1. 主体代码部分
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace TestBoom
{
    class Client   //这是封装好的客户端类
    {
        public String receivestr = null;
        private static Client  client;
        private Socket ClientSocket;
        private Client(string ip1, int port1)
        {
            ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            Init(ip1, port1);
        }
        public static Client clientsocket(string ip1,int port1)
        {
            if (client == null)
            {
                client = new Client(ip1,port1);
            }
            return client;
        }
        private void Init(string ip1, int port1)
        {
            IPEndPoint iPEnd = new IPEndPoint(IPAddress.Parse(ip1), port1);
            ClientSocket.Connect(iPEnd);
            Thread reciveThread = new Thread(Recive);
            reciveThread.IsBackground = true;
            reciveThread.Start();
        }
        public void Recive()
        {
            while (true)
            {
                byte[] Btye = new byte[1024];
                ClientSocket.Receive(Btye);
                receivestr = Encoding.UTF8.GetString(Btye,0,3);
                if (receivestr[0] == '0')
                {
                    Console.WriteLine($"接受对方了轰炸位置{receivestr}");
                }
                else if(receivestr[0]=='1')
                {
                    Console.WriteLine($"接受轰炸位置结果{receivestr}");
                }
            }
        }
        public void Send(int i,int x,int y)
        {
            string str =Convert.ToString(i)+Convert.ToString(x) + Convert.ToString(y);
            byte[] Btye = Encoding.UTF8.GetBytes(str);
            ClientSocket.Send(Btye);
            if (str[0] == '0')
            {
                Console.WriteLine($"已发送轰炸位置 {str}");
            }
            else if (str[0] == '1')
            {
                Console.WriteLine($"已发送对方轰炸位置结果{str}");
            }
        }
    }
}
2. 具体分析:

1. 首先这个游戏我们必须要知道的一点是我们想要实现两台电脑之间的交互,就必须使用ip和端口进行连接,而想要进行连接就必须使用一个实例化的对象(在这里我没有体现出来,因为实例化对象在另一个from中,在按钮事务响应的函数中进行实例化),而且在这个游戏中,实例化对象必须是单例模型,原因之前提到过,那么实例化对象就必须包含单例化的过程;

  public String receivestr = null;  //receivestr是接受函数中接收到对方的传输过来的信息,后面用到
  private static Client  client;    //单例化对象所需要的对象
  private Socket ClientSocket;      //Socket类的一个实例化对象
  private Client(string ip1, int port1)   //Client()客户端构造函数
  {
      ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      Init(ip1, port1);
  }
  public static Client clientsocket(string ip1,int port1) //单例化实现函数
  {
      if (client == null)   //如果实例化对象不存在,则创建一个
      {
          client = new Client(ip1,port1);
      }
      return client;        //如果存在,则直接返回存在的那个对象,这样便实现了单例化
  }
  private void Init(string ip1, int port1)    //初始化,用于进行客户端和服务器端的连接
  {
      IPEndPoint iPEnd = new IPEndPoint(IPAddress.Parse(ip1), port1);
      ClientSocket.Connect(iPEnd);
      Thread reciveThread = new Thread(Recive);
      reciveThread.IsBackground = true;
      reciveThread.Start();
  }

2. 其次在连接妥当之后,必须进行信息传输,如果而现在假定客户端时先手,则要进行 send() 函数的调用,在函数中你可以发送任意的数据,但必须时btye数组(因为在物理层传输数据是发送的是比特,发送到对方物理层会进行解析还原,但是这些东西C#的Socket类已经封装好了,我们调用接口即可),需要注意的是在使用 send() 的时候必须调用(这个之后再详细说);

  public void Send(int i,int x,int y) //这里面的参数 i,x,y 的含义分别是 模式0/1,x坐标, y坐标,可以根据需求改变
  {
      string str =Convert.ToString(i)+Convert.ToString(x) + Convert.ToString(y);    //将数字转化为string类型字符串
      byte[] Btye = Encoding.UTF8.GetBytes(str);    //将刚刚转化好的string类型字符串转化为byte类型数组
      ClientSocket.Send(Btye);    //调用Socket类中的Send()函数发送数据
      if (str[0] == '0')      //判断模式0/1,在己方控制台显示己方发送过去的内容,方便自己查看
      {
          Console.WriteLine($"已发送轰炸位置 {str}");
      }
      else if (str[0] == '1')
      {
          Console.WriteLine($"已发送对方轰炸位置结果{str}");
      }
  }

3. 最后阐述一下 receive() 函数,再对方(服务器端)接收到你发送的信息之后,一定会返回一个信息(因为下棋是交互的嘛),这时候你便需要一个接收函数 receive() ,这个函数是用来接受对方发送的信息的,但是需要注意的是这个函数会随着你的进程一直运行,在from中是不需要调用的。

    public void Recive()
      {
          while (true)      //因为是一直在另一个进程中运行,所以给一个死循环
          {
              byte[] Btye = new byte[1024];     //接收也是byte数组的
              ClientSocket.Receive(Btye);
              receivestr = Encoding.UTF8.GetString(Btye,0,3);   //转化为string类型
              if (receivestr[0] == '0')         //判断模式0/1,在己方控制台显示对方发送过来的内容,方便查看对方信息
              {
                  Console.WriteLine($"接受对方了轰炸位置{receivestr}");
              }
              else if(receivestr[0]=='1')
              {
                  Console.WriteLine($"接受轰炸位置结果{receivestr}");
              }
          }
      }

服务器类代码

1.主体代码部分
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace TestBoom
{
    class Server
    {
        public String receivestr = null;
        private Socket SocketWatch;
        private Socket SocketSend;
        private static Server server = null;
        private Server(string ip1, int port1)
        {
            SocketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            Init(ip1,port1);
        }
        public static Server serversocket(string ip1, int port1)
        {
            if (server == null)
            {
                server = new Server(ip1, port1);
            }
            return server;
        }

        private void Init(string ip1, int port1)
        {
            SocketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint iPEnd = new IPEndPoint(IPAddress.Parse(ip1), port1);
            SocketWatch.Bind(iPEnd);
            SocketWatch.Listen(1);
            System.Windows.Forms.MessageBox.Show("开始监听...");
            Thread thread = new Thread(Listen);
            thread.IsBackground = true;
            thread.Start();
        }
        void Listen()
        {
            while (SocketSend==null)
            {
                SocketSend = SocketWatch.Accept();
            }
            System.Windows.Forms.MessageBox.Show("连接成功..." + SocketSend.RemoteEndPoint.ToString());
            Thread reciveThread = new Thread(Recive);
            reciveThread.IsBackground = true;
            reciveThread.Start();
        }

        public void Recive()
        {
            while (true)
            {
                byte[] buffer = new byte[1024];
                SocketSend.Receive(buffer);
                receivestr = Encoding.UTF8.GetString(buffer, 0, 3);
                if (receivestr[0] == '0')
                {
                    Console.WriteLine($"接受对方了轰炸位置{receivestr}");
                }
                else if (receivestr[0] == '1')
                {
                    Console.WriteLine($"接受我方轰炸位置结果{receivestr}");
                }
            }
        }

        public void Send(int i,int x,int y)
        {
            string str = Convert.ToString(i) + Convert.ToString(x) + Convert.ToString(y);
            byte[] buffer = Encoding.UTF8.GetBytes(str);
            SocketSend.Send(buffer);
            if (str[0] == '0')
            {
                Console.WriteLine($"已发送轰炸位置 {str}");
            }
            else if (str[0] == '1')
            {
                Console.WriteLine($"已发送对方轰炸位置结果{str}");
            }
        }
    }
}
2.具体分析:
  1. 对于 send()receive() 函数就不过多赘述,主要分析一下 listen() 函数,listen() 函数其实是一个监听函数,只有监听成功之后才能够连接,才可以实例化一个发送Socket对象和一个接收Thread对象,而服务器端也是单例模式的,与客户端结构基本相同。

四、对服务器类和客户类的具体使用

1.代码部分(部分代码,不能直接使用,注释部分即内容)
private void button2_Click(object sender, EventArgs e)
{
    if (Plane_Sum < 3)
    {
        label4.Text = "请先放置坤坤";
    }
    else
    {
        if(ipok && portok)
        {
            label4.Text = "坤坤已放置好";
            label3.Text = "你的回合";
            client = Client.clientsocket(ip,port); //实例化客户端
            serorcli = true;
        }
        else
        {
            label4.Text = "没有输入ip或者端口";
        }
    }
}

private void button1_Click(object sender, EventArgs e)
{
    if (Plane_Sum < 3)
    {
        label4.Text = "请先放置坤坤";
    }
    else
    {
        if(ipok && portok)
        {
            label4.Text = "坤坤已放置好";
            label3.Text = "对手回合";
            server = Server.serversocket(ip, port); //实例化服务器端
            serorcli = false;
        }
        else
        {
            label4.Text = "没有输入ip或者端口";
        }
    }
}

private void button3_Click(object sender, EventArgs e)
{
    if (Plane_Sum < 3)
    {
        label4.Text = "请先放置坤坤";
    }
    else
    {
        if (serorcli == false&&ipok&&portok)
        {
            while (server.receivestr == null) { }  //判断有没有接收,直到有接收才可以跳出循环
            if (server.receivestr[0] == '0')//接收直接使用,由于接收是处于一直接收的状态
            {
                PutKunKun2(server.receivestr[1] - '0', server.receivestr[2] - '0');
                server.Send(1, Board1[server.receivestr[1] - '0', server.receivestr[2] - '0'], 0);  //发送调用
                server.receivestr = null;  //赋值为null,为下一次接收做准备
            }
        }
    }
}
private void textBox1_ipChanged(object sender, EventArgs e)
{
    ip = textBox1.Text; //输入ip
    ipok = true;
}
private void textBox2_portChanged(object sender, EventArgs e)
{
    int.TryParse(textBox2.Text,out port); //输入port
    portok = true;
}

五、问题分析与总结

1.问题分析:
  1. 在本项目中单例化对象中,对单例化思想并不清楚, 求助于舍友,在他的帮助下明白了,对象没有就创建,有的话就直接返回已经创建的对象。
  2. 在引用实例化对象的函数时,搞不清楚使用的位置,经过多次试错,多次调整,才明白使用逻辑
  3. 开始使用 receive() 这一函数时,以为其必须进行调用才行,后来才知道其在对象线程中一直存在,根本不需要调用。
2.总结:

在这个项目中,我对计算机网络中学习的内容有了更深的理解,对Socket通信有了更深的认识,对TCP和UDP也有了不同于书本单薄的理解。

11-11 20:38