C#網絡編程入門之TCP


一、概述

UDP和TCP是網絡通訊常用的兩個傳輸協議,C#一般可以通過Socket來實現UDP和TCP通訊,由於.NET框架通過UdpClient、TcpListener 、TcpClient這幾個類對Socket進行了封裝,使其使用更加方便, 本文就通過這幾個封裝過的類講解一下相關應用。

 

二、基本應用:連接、發送、接收

服務端建立偵聽並等待連接:

TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 9000);
tcpListener.Start();
if (tcpListener.Pending())
{
          TcpClient client = tcpListener.AcceptTcpClient();
          Console.WriteLine("Connected");   
}

服務端是通過AcceptTcpClient方法獲得TcpClient對象,而客戶端是直接創建TcpClient對象。

TcpClient tcpClient = new TcpClient();
tcpClient.Connect("127.0.0.1", 9000);

發送數據TcpClient對象創建后,發送接收都通過TcpClient對象完成。

發送數據:

                TcpClient tcpClient = new TcpClient();
                tcpClient.Connect("127.0.0.1", 9000);
                NetworkStream netStream = tcpClient.GetStream();

                int Len = 1024;
                byte[] datas = new byte[Len];

                netStream.Write(datas, 0, Len);               

                netStream.Close();
                tcpClient.Close();

接收數據:

TcpClient client = tcpListener.AcceptTcpClient();
Console.WriteLine("Connected");  
  
  NetworkStream stream = client.GetStream();
  var remote = client.Client.RemoteEndPoint;

   byte[] data = new byte[1024];
   while (true)
   {
         if (stream.DataAvailable)
         {
                int len = stream.Read(data, 0, 1024);
                Console.WriteLine($"From:{remote}:Received ({len})");
          }
        Thread.Sleep(1);
 }   

 

三、 粘包問題

和UDP不太一樣,TCP連接不會丟包,但存在粘包問題。(嚴格來說粘包這個說法是不嚴謹的,因為TCP通訊是基於流的,沒有包的概念,包只是使用者自己的理解。) 下面分析一下粘包產生的原因及解決辦法。

TCP數據通訊是基於流來實現的,類似一個隊列,當有數據發送過來時,操作系統就會把發送過來的數據依次放到這個隊列中,對發送者而言,數據是一片一片發送的,所以自然會認為存在數據包的概念,但對於接收者而言,如果沒有及時去取這些數據,這些數據依次存放在隊列中,彼此之間並無明顯間隔,自然就粘包了。

還有一種情況粘包是發送端造成的,有時我們調用發送代碼時,操作系統可能並不會立即發送,而是放到緩存區,當緩存區達到一定數量時才真正發送。 

要解決粘包問題,大致有以下幾個方案。

1、 約定數據長度,發送端的數據都是指定長度,比如1024;接收端取數據時也取同樣長度,不夠長度就等待,保證取到的數據和發送端一致;

2、 接收端取數據的頻率遠大於發送端,比如發送端每1秒發送一段數據,接收端每0.1秒去取一次數據,這樣基本可以保證數據不會粘起來;

以上兩個方案都要求發送端需要立即發送,不可緩存數據。而且這兩種方案都有缺陷:首先,第一種方案:如果要包大小一致的話,如果約定的包比較大,肯定有較多數據冗余,浪費網絡資源,如果包較小,連接就比較頻繁,效率不高。

其次,第二種方案:這個方案只能在理想環境下可以實現,當服務端遭遇一段時間的計算壓力時可能會出現意外,不能完全保證。

比較完善的解決方案就是對接收到的數據進行預處理:首先通過定義特殊的字符組合作為包頭和包尾,如果傳輸ASCII字符,可以用0x02表示開始(STX),用0x03表示結束(ETX),比如:STX ‘H’ ‘e’ ‘l’ ‘l’ ‘o’ ETX (二進制數據: 02 48 65 6C 6C 6F 03)。如果數據較長可以在包頭留出固定位置存放包長度, 如:

02 00 05 48 65 6C 6C 6F 03

其中02 05 就表示正文長度為5個字節,可以進行校驗。

雖然第三種方案比較嚴謹,但相對復雜,在傳輸比較可靠、應用比較簡單的場景下,也可以采用前面兩種解決方案。

 

四、 一個完整的例程

 服務端:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TCPServer
{
    class Program
    {  
        static void Main(string[] args)
        {   
            TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 9000);
            tcpListener.Start();

            while (true)
            {
                if (tcpListener.Pending())
                {
                    TcpClient client = tcpListener.AcceptTcpClient();
                    Console.WriteLine("Connected");                   

                    Task.Run(() =>
                    {  
                        NetworkStream stream = client.GetStream();
                        var remote = client.Client.RemoteEndPoint;
                      
                        while (true)
                        {
                            if (stream.DataAvailable)
                            {
                                byte[] data = new byte[1024];
                                int len = stream.Read(data, 0, 1024);
                                string Name = Encoding.UTF8.GetString(data,0,len);
                                var senddata = Encoding.UTF8.GetBytes("Hello:" + Name);
                                stream.Write(senddata, 0, senddata.Length);
                            }

                            if (!client.IsOnline())
                            {
                                Console.WriteLine("Connect Closed.");
                                break;
                            }

                            Thread.Sleep(1);
                        }
                    });
                }

                Thread.Sleep(1);
            }
        }
    }

    public static class TcpClientEx
    {
        public static bool IsOnline(this TcpClient client)
        {
            return !((client.Client.Poll(15000, SelectMode.SelectRead) && (client.Client.Available == 0)) || !client.Client.Connected);
        }
    }
}
View Code

客戶端:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TCP_Clent
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.SetMinThreads(100, 100);
            ThreadPool.SetMaxThreads(200, 200); 

            Parallel.For(1, 10,  x =>
            {
                SendData("Tom");
            });

            Console.WriteLine("All Completed!");
            Console.ReadKey();
        }

        private static void SendData(string Name)
        {
            Task.Run(() =>
            {
                Console.WriteLine("Start");
                TcpClient tcpClient = new TcpClient();
                tcpClient.Connect("127.0.0.1", 9000);
                Console.WriteLine("Connected");
                NetworkStream netStream = tcpClient.GetStream();

                Task.Run(() =>
                {
                    Thread.Sleep(100);
                    while (true)
                    {
                        if (!tcpClient.Client.Connected)
                        {
                            break;
                        }

                        if (netStream == null)
                        {
                            break;
                        }

                        try
                        {
                            if (netStream.DataAvailable)
                            {
                                byte[] data = new byte[1024];
                                int len = netStream.Read(data, 0, 1024);
                                var message = Encoding.UTF8.GetString(data, 0, len);
                                Console.WriteLine(message);
                            }
                        }
                        catch
                        {
                            break;
                        }

                        Thread.Sleep(10);
                    }
                });

                for (int i = 0; i < 100; i++)
                {
                    byte[] datas = Encoding.UTF8.GetBytes(Name);
                    int Len = datas.Length;
                    netStream.Write(datas, 0, Len);
                    Thread.Sleep(1000);
                }

                netStream.Close();
                netStream = null;
                tcpClient.Close();

                Console.WriteLine("Completed");
            });
        }       
    }
}
View Code

 

傳送門:

C#網絡編程入門系列包括三篇文章:

(一)C#網絡編程入門之UDP

(二)C#網絡編程入門之TCP

(三)C#網絡編程入門之HTTP


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM