1. 程式人生 > >C#網路程式設計之---TCP協議的同步通訊(二)

C#網路程式設計之---TCP協議的同步通訊(二)

上一篇學習日記C#網路程式設計之--TCP協議(一)中以服務端接受客戶端的請求連線結尾
既然服務端已經與客戶端建立了連線,那麼溝通通道已經打通,載滿資料的小火車就可以彼此傳送和接收了。現在讓我們來看看資料的傳送與接收

先把服務端與客戶端的連線程式碼敲出來

服務端
IPAddress ip = new IPAddress(new byte[] { 127, 1, 1, 1 });
TcpListener server = new TcpListener(ip, 8005);
server.Start();//服務端啟動偵聽
TcpClient client = server.AcceptTcpClient();
//接受發起連線物件的同步方法
Console.WriteLine("收到客戶端連線請求")//如果沒有客戶端請求連線,這句話是無法Print out的
客戶端
IPAddress ip=IPAddress.Parse("127.1.1.1");
TcpClient client=new TcpClient();
client.Connect(ip,8005);//8005埠號,必須與服務端給定的埠號一致,否則天堂無門

先看看服務端的特殊標記的那句程式碼

AcceptTcpClient() 這個方法是一個同步方法,在沒有接受到連線請求的時候,位於它下面的程式碼是不會被執行的,也就是執行緒阻塞在這裡,進行不下去了,想出城沒有城防長官的批覆是不能的,嘿嘿...

連線後,客戶端要傳送資料給服務端,先貼程式碼再說

NetworkStream dataStream=client.GetStream();
string msg="服務端親啟!";
byte[] buffer=Encoding.default.getBytes(msg);
stream.write(buffer,0,buffer.length);
//這段程式碼呈接上面那段客戶端程式碼

NetworkStream 在網路中進行傳輸的資料流,也就是說傳輸資料必須寫入此流中,才能夠互通有無。
首先客戶端先獲取用於傳送資訊的流,然後將要傳送的資訊存入byte[] 陣列中(資料必須是byte[] 才能夠寫入流中),最後就是寫入傳輸的資料流,傳送

聰明的你想必已經知道如何在服務端獲取資料了
既然客戶端費力的把資料包裝發給服務端了,那麼服務端自然要把包裝拆了,得到資料,上程式碼:

NetworkStream dataStream=client.GetStream();
byte[] buffer=new byte[8192];
int dataSize=dataStream.Read(buffer,0,8192);
Console.write(Encoding.default.GetString(buffer,0,dataSize));
//這段程式碼呈接上面那段服務端程式碼

程式碼一寫,我覺得再說多餘了,不過還要在說一兩句,嘿嘿
Read() 方法需要三個引數,1,儲存資料的快取空間。2,寫入資料的起始點就是從儲存空間的什麼位置開始寫入資料。3,就是儲存空間的大小。返回寫入資料的大小值
Encoding.default.GetString() 引數解析
1,儲存資料的快取空間。2,從什麼位置開始接收資料。3,接收多少資料

以上只是再簡單不過的資料傳送,而且只是客戶端發給服務端,只能發一條資訊而已,那如果想彼此互發,並且想發多少條資訊都可以,怎麼辦呢

首先基於以上的程式碼,編寫一個WPF的小程式

下圖分別是客戶端和服務端

介面很簡單,要實現的功能就是客戶端與服務端互發資訊。

感覺還是直接上程式碼吧

服務端的全部程式碼如下: 

        public delegate void showData(string msg);//委託,防止跨執行緒的訪問控制元件,引起的安全異常
        private const int bufferSize = 8000;//快取空間
        private TcpClient client;
        private TcpListener server;
        
        /// <summary>
        /// 結構體:Ip、埠
        /// </summary>
        struct IpAndPort
        {
            public string Ip;
            public string  Port;
        }

        /// <summary>
        /// 開始偵聽
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            if (txtIP.Text.Trim() == string.Empty)
            {
                return;
            }
            if (txtPort.Text.Trim() == string.Empty)
            {
                return;
            }

            Thread thread = new Thread(reciveAndListener);
       //如果執行緒繫結的方法帶有引數的話,那麼這個引數的型別必須是object型別,所以講ip,和埠號 寫成一個結構體進行傳遞
            IpAndPort ipHePort = new IpAndPort();
            ipHePort.Ip = txtIP.Text;
            ipHePort.Port = txtPort.Text;

            thread.Start((object)ipHePort);
        }      

        /// <summary>
        /// 傳送資訊給客戶端
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSend_Click(object sender, RoutedEventArgs e)
        {
            if (txtSendMsg.Text.Trim() != string.Empty)
            {
                NetworkStream sendStream = client.GetStream();//獲得用於資料傳輸的流
                byte[] buffer = Encoding.Default.GetBytes(txtSendMsg.Text.Trim());//將資料存進快取中
                sendStream.Write(buffer,0,buffer.Length);//最終寫入流中
                txtSendMsg.Text = string.Empty;
            }

        }

        /// <summary>
        /// 偵聽客戶端的連線並接收客戶端傳送的資訊
        /// </summary>
        /// <param name="ipAndPort">服務端Ip、偵聽埠</param>
        private void reciveAndListener(object ipAndPort)
        {
            IpAndPort ipHePort = (IpAndPort)ipAndPort;

            IPAddress ip = IPAddress.Parse(ipHePort.Ip);
            server = new TcpListener(ip, int.Parse(ipHePort.Port));
            server.Start();//啟動監聽
            rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText), "服務端開啟偵聽....\n");
          //  btnStart.IsEnabled = false;

            //獲取連線的客戶端物件
            client = server.AcceptTcpClient();
            rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText),"有客戶端請求連線,連線已建立!");//AcceptTcpClient 是同步方法,會阻塞程序,得到連線物件後才會執行這一步  

            //獲得流
            NetworkStream reciveStream = client.GetStream();

            #region 迴圈監聽客戶端發來的資訊

            do
            {
                byte[] buffer = new byte[bufferSize];
                int msgSize;
                try
                {
                    lock (reciveStream)
                    {
                        msgSize = reciveStream.Read(buffer, 0, bufferSize);
                    }
                    if (msgSize == 0)
                        return;
                    string msg = Encoding.Default.GetString(buffer, 0, bufferSize);
                    rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText), "\n客戶端曰:" + Encoding.Default.GetString(buffer, 0, msgSize));
                }
                catch
                {
                    rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText), "\n 出現異常:連線被迫關閉" );
                    break;
                }
            } while (true);

            #endregion
        }

客戶端程式碼:

       TcpClient client;
        private const int bufferSize = 8000;
        NetworkStream sendStream;
        public delegate void showData(string msg);

        private void btnConnect_Click(object sender, RoutedEventArgs e)
        {
            if (txtIP.Text.Trim() == string.Empty)
            {
                return;
            }
            if (txtPort.Text.Trim() == string.Empty)
            {
                return;
            }
            IPAddress ip = IPAddress.Parse(txtIP.Text);
            client = new TcpClient();
            client.Connect(ip, int.Parse(txtPort.Text));
            rtbtxtShowData.AppendText("開始連線服務端....\n");
            rtbtxtShowData.AppendText("已經連線服務端\n");
            //獲取用於傳送資料的傳輸流
            sendStream = client.GetStream();
            Thread thread = new Thread(ListenerServer);
            thread.Start();
        }

        private void btnSend_Click(object sender, RoutedEventArgs e)
        {
            if (client != null)
            {
                //要傳送的資訊
                if (txtSendMsg.Text.Trim() == string.Empty)
                    return;
                string msg = txtSendMsg.Text.Trim();
                //將資訊存入快取中
                byte[] buffer = Encoding.Default.GetBytes(msg);
                //lock (sendStream)
                //{
                    sendStream.Write(buffer, 0, buffer.Length);
                //}
                    rtbtxtShowData.AppendText("傳送給服務端的資料:" + msg + "\n");
                txtSendMsg.Text = string.Empty;
            }
        }

        private void ListenerServer()
        {
            do
            {
                try
                {
                    int readSize;
                    byte[] buffer = new byte[bufferSize];
                    lock (sendStream)
                    {
                        readSize = sendStream.Read(buffer, 0, bufferSize);
                    }
                    if (readSize == 0)
                        return;
                    rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText), "服務端曰:" + Encoding.Default.GetString(buffer, 0, readSize)+"\n");

                }
                catch
                {
                    rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText), "報錯");
                }
                //將快取中的資料寫入傳輸流
            } while (true);
        }         

其中用到了,多執行緒處理還有委託,因為以上我們用到的不管是Connect,還是AcceptTcpClient方法 都是同步方法,會阻塞程序,導致視窗無法自由移動

 rtbtxtShowData.Dispatcher.Invoke(new showData(rtbtxtShowData.AppendText), "服務端開啟偵聽....\n");

上面這句程式碼或許有些人不解,我也花了一些時間才懂這樣寫的

其實由於在WPF中不允許跨執行緒訪問,訪問了會拋異常,但是在WPF中的視窗控制元件都有一個Dispatcher(排程器)屬性,允許訪問控制元件的執行緒;既然不允許直接訪問,就告訴控制元件我們要幹什麼就好了。

所以在多執行緒中使用控制元件的Dispatcher屬性,這樣就不是跨執行緒訪問了,然後我們在看看Invoke方法

通過上面的標示,看的出需要一個委託型別的方法,所以就將RichTextBox 的賦值方法AppendText 繫結到一個委託showData上。

下面是一段引用,看了或許能更明白點

WPF的UI執行緒都交給一個叫做排程器的類了。

     WPF 應用程式啟動時具有兩個執行緒:一個用於處理呈現,另一個用於管理 UI。 呈現執行緒實際上隱藏在後臺執行,而 UI 執行緒則接收輸入、處理事件、繪製螢幕以及執行應用程式程式碼。UI 執行緒在一個名為 Dispatcher 的物件中將工作項進行排隊。 Dispatcher 根據優先順序選擇工作項,並執行每一個工作項直到完成。Dispatcher 類提供兩種註冊工作項的方法:Invoke 和 BeginInvoke。 這兩個方法都會安排執行一個委託。Invoke 是同步呼叫,即它直到 UI 執行緒實際執行完該委託時才返回。BeginInvoke 是非同步呼叫,因而將立即返回。------引用自WPF筆記12: 執行緒處理模型

執行以上程式的效果圖:

Ok,至此客戶端與服務端的資料傳遞就大功告成了,這只是一個很簡單的操作,如果有多個客戶端呢?要求非同步通訊,怎麼辦?不急,慢慢來,不積跬步無以至千里

如果有什麼錯的,希望指正。