C#實踐炸飛機socket通訊

一、前言

  • 最近老師要求做課設,實現一個 「炸飛機」 遊戲,我是負責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也有了不同於書本單薄的理解。

Tags: