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: