C#網絡程序設計(2)Socket基礎編程


    本節介紹如何使用基礎Socket實現TCP通信。

 

    (1)Socket詳細介紹:

    Socket的英文原義是“孔”或“插座”。通常稱作"套接字",用於描述IP地址和端口,是一個通信鏈的句柄。在Internet上的主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不同的端口對應於不同的服務。Socket正如其英文原意那樣,象一個多孔插座。

    Socket的發展:

    七十年代中,美國國防部高研署(DARPA)將TCP/IP的軟件提供給加利福尼亞大學Berkeley分校后,TCP/IP很快被集成到Unix中,同時出現了許多成熟的TCP/IP應用程序接口(API)。這個API稱為Socket接口。 今天,SOCKET接口是TCP/IP網絡最為 通用的API,也是在INTERNET上進行應用開發最為通用的API。

    九十年代初,由Microsoft聯合了其他幾家公司共同制定了一套 WINDOWS下的網絡編程接口,即Windows Sockets規范(簡稱WinSock)。它是Berkeley Sockets的重要擴充,主要是增加了一些異步函數,並增加了符合 Windows 消息驅動特性的網絡事件異步選擇機制

    WinSock:

    Windows Sockets規范是一套開放的、支持多種協議的 Windows下的網絡編程接口。 Windows Socket規范1.1版,只支持TCP/IP協議。 Windows Socket規范2.0版,支持多種協議。

 

    (2)套接字分類:

    根據不同的應用協議的需要,套接字分為字節流套接字(stream Socket),數據報套接字(datagram Socket),原始套接字(raw Socket):

1.字節流套接字(stream Socket)
流套接字用於提供面向連接、可靠的數據傳輸服務。
該服務將保證數據能夠實現無差錯、無重復發送,並按順序接收。
流套接字之所以能夠實現可靠的數據服務,原因在於其使用了傳輸控制協議,即TCP(The Transmission Control Protocol)協議。
2.數據報套接字(datagram Socket)
數據報套接字提供了一種無連接的服務。
該服務並不能保證數據傳輸的可靠性,數據有可能在傳輸過程中丟失或出現數據重復,且無法保證順序地接收到數據。
數據報套接字使用UDP(User Datagram Protocol)協議進行數據的傳輸。
由於數據報套接字不能保證數據傳輸的可靠性,對於有可能出現的數據丟失情況,需要在程序中做相應的處理。
3.原始套接字(raw Socket)
原始套接字與標准套接字(標准套接字指的是前面介紹的流套接字和數據報套接字)的區別在於:
原始套接字可以讀寫內核沒有處理的IP數據包,而流套接字只能讀取TCP協議的數據,數據報套接字只能讀取UDP協議的數據。
因此,如果要訪問其他協議發送數據必須使用原始套接字。
原始套接字允許對底層協議如IP或ICMP進行直接訪問,功能強大但使用較為不便,主要用於一些協議的開發。

    根據套接字的不同,套接字編程又分為:面向連接,無連接,原始套接字編程。

    套接字的TCP通信流程:

    TCP是面向連接的,程序運行后,服務器有一個Socket一直處於偵聽狀態,客戶端Socket與服務器通信之前必須首先發起連接請求,服務器上負責偵聽的Socket接受請求並另外創建一個Socket與客戶端通信息,自己則繼續偵聽新的請求。

 

    (3)Socket編程實例:

    1)面向連接-基於TCP:

  

    服務器端(帶窗體):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

//添加的引用
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace TCPServer
{
    public partial class SocketTcpServerForm : Form
    {
        private int port;
        Socket tcpSocket=null;
        List<Socket> clientSockets;
        public SocketTcpServerForm()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = false;
            port = 10000;//默認使用端口號10000
            portTextBox.Text = port.ToString();
            clientSockets= new List<Socket>();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            
        }

        private void portTextBox_TextChanged(object sender, EventArgs e)
        {

        }

        private void listenButton_Click(object sender, EventArgs e)
        {
            string portString = portTextBox.Text;
            port = int.Parse(portString);
            IPEndPoint ipep = new IPEndPoint(IPAddress.Any,port);
            tcpSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
            tcpSocket.Bind(ipep);
            tcpSocket.Listen(10);
            showDataListBox.Items.Add("1.開始監聽端口" + port.ToString() + "......");
            showDataListBox.Items.Add("2.等待客戶端連接......");
            Thread listenThread = new Thread(ListenConnect);
            listenThread.Start(tcpSocket);
        }

        private void ListenConnect(object obj)
        {
            Socket tcpSocket = (Socket)obj;
            while (true)
            {
                Socket client = tcpSocket.Accept();
                clientSockets.Add(client);
                Thread sonProcess = new Thread(acceptClient);
                sonProcess.Start(client);
            }
        }
        private void acceptClient(object client)
        {
            Socket socketClient = (Socket)client;
            IPEndPoint remoteIP = (IPEndPoint)socketClient.RemoteEndPoint;
            socketClient.Send(Encoding.UTF8.GetBytes("已連接到服務端在端口:"+port.ToString()),SocketFlags.None);
            showDataListBox.Items.Add("客戶端連接:"+remoteIP.Address+"("+remoteIP.Port+")");
            while (true)
            {
                //一直接收客戶端發來的信息
                try
                {
                    byte[] receiveData = new byte[64];
                    int longth = socketClient.Receive(receiveData);
                    string s = Encoding.UTF8.GetString(receiveData);
                    showDataListBox.Items.Add(s + "(" + socketClient.ToString() + ")");
                    if (s == "exit")
                    {
                        showDataListBox.Items.Add("客戶端斷開連接" + "(" + socketClient.ToString() + ")");
                        clientSockets.Remove(socketClient);
                        socketClient.Close();
                        break;
                    }
                }catch(Exception e){
                    Console.WriteLine("出現錯誤");
                }
            }
        }

        private void sendDataButton_Click(object sender, EventArgs e)
        {
            string sendData = sendDataBox.Text;
            foreach (Socket s in clientSockets)
            {
                s.Send(Encoding.UTF8.GetBytes(sendData));
            }
        }
    }
}

    客戶端(控制台):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace TCPClient
{
    class Program
    {
        private static bool flag = true;
        static void Main(string[] args)
        {
            Socket newclient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            Console.Write("請輸入要連接的IP:");
            string ipadd = Console.ReadLine();
            Console.Write("請輸入要連接的端口:");
            int port = Convert.ToInt32(Console.ReadLine());
            IPEndPoint ie = new IPEndPoint(IPAddress.Parse(ipadd), port);//服務器的IP和端口 
            try
            {
                newclient.Connect(ie);
            }
            catch (SocketException e)
            {
                Console.WriteLine("連接服務器失敗");
                Console.WriteLine(e.ToString());
                return;
            }
            //在第一次連接到客戶端時,服務端會返回一個字符串,客戶端接收並顯示在控制台上
            byte[] data = new byte[1024]; 
            int recv = newclient.Receive(data);
            string stringdata = Encoding.UTF8.GetString(data, 0, recv);
            Console.WriteLine(stringdata);
            //啟動一個子線程專門用來接收服務器發送的數據
            Thread receiveThread = new Thread(receiveData);
            receiveThread.Start(newclient);
            while (true)
            {
                string input = Console.ReadLine();
                newclient.Send(Encoding.UTF8.GetBytes(input), SocketFlags.None);
                if (input == "exit"){
                    flag=false;
                    break;
                }
                
            }
            Console.WriteLine("與服務端斷開連接");
            newclient.Shutdown(SocketShutdown.Both);
            newclient.Close();
            Console.ReadKey();
        }
        private static void receiveData(object obj)
        {
            Socket newclient = (Socket)obj;
            byte[] data=new byte[64];
            while(flag==true)
            {
                try
                {
                    int length = newclient.Receive(data);
                    string s = Encoding.UTF8.GetString(data, 0, length);
                    Console.WriteLine("服務端傳來數據:" + s);
                }
                catch(Exception e)
                {
                    break;
                }
            }
        }
    }
}

    顯示效果:

  

  

    2)無連接-基於UDP

    

    服務端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Net;
using System.Net.Sockets;
namespace MyUDPServer
{
    class Program
    {
        static void Main(string[] args)
        {
            int recv;      
            byte[] data = new byte[1024];            
            IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9050);//定義一網絡端點            
            Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//定義一個Socket            
            newsock.Bind(ipep);//Socket與本地的一個終結點相關聯            
            Console.WriteLine("Waiting for a client..");            
            IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);//定義要發送的計算機的地址           
            EndPoint Remote = (EndPoint)(sender);//            
            recv = newsock.ReceiveFrom(data, ref Remote);//接受數據                      
            Console.WriteLine("Message received from{0}:", Remote.ToString());
            string stringdata = Encoding.ASCII.GetString(data, 0, recv);
            Console.WriteLine(stringdata);          
            string welcome = "Welcome to my test server!";
            data = Encoding.UTF8.GetBytes(welcome);           
            newsock.SendTo(data, data.Length, SocketFlags.None, Remote);         
            while (true)            
            {               
                data = new byte[1024];   
                recv = newsock.ReceiveFrom(data, ref Remote);
                Console.WriteLine(Encoding.UTF8.GetString(data, 0, recv));        
                newsock.SendTo(data, recv, SocketFlags.None, Remote);   
            }       
        }
    }
}

    客戶端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Net;
using System.Net.Sockets;

namespace MyUDPClient
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] data = new byte[1024];//定義一個數組用來做數據的緩沖區           
            string input, stringData;           
            IPEndPoint server_ipep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);           
            Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);         
            string welcome = "Hello,are you there?";
            data = Encoding.UTF8.GetBytes(welcome);          
            client.SendTo(data, data.Length, SocketFlags.None, server_ipep);//將數據發送到指定的終結點     
            IPEndPoint sender = new IPEndPoint(IPAddress.Any,0);           
            EndPoint Remote = (EndPoint)sender;       
            data = new byte[1024];           
            int recv = client.ReceiveFrom(data, ref Remote);//接受來自服務器的數據           
            Console.WriteLine("Message received from{0}:", Remote.ToString());            
            Console.WriteLine(Encoding.UTF8.GetString(data, 0, recv));    
            while (true)//讀取數據          
            {               
                input = Console.ReadLine();//從鍵盤讀取數據          
                if (input == "exit")//結束標記          
                {                  
                    break;       
                }
                client.SendTo(Encoding.UTF8.GetBytes(input), Remote);//將數據發送到指定的終結點Remote    
                data = new byte[1024];               
                recv = client.ReceiveFrom(data, ref Remote);//從Remote接受數據          
                stringData = Encoding.UTF8.GetString(data, 0, recv);              
                Console.WriteLine(stringData);         
            }          
            Console.WriteLine("Stopping client");       
            client.Close();
        }
    }
}

    顯示效果:

    

   

    3)原始套接字:

    原始套接字編程流程:

1.創建原始套接字
2.定義數據包頭部數據結構
3.發送報文
4.接收報文

    IP報文首部:

  

    IP報文:

  

    ICMP報文:(IP報文承載)

  

    Ping程序的實現:

    功能:PING命令是用於測試兩個端系統之間的網絡連通性。如果連通:輸出源主機到目的主機的往返時延(時間),目的主機的操作系統(TTL)。如果不連通:輸出可能原因。

    原理:向網絡上的另一個主機系統發送ICMP回送請求報文,如果指定系統得到了報文,它將把報文一模一樣地傳回給發送者。 ICMP echo & ICMP echo reply (rfc792)

    TTL(Time To Live)生存期

    指定數據報被路由器丟棄之前允許通過的網段數量。 TTL 是由發送主機設置的。

Windows 9x/Me      TTL=32
LINUX              TTL=64
Windows 200x/XP    TTL=128
Unix               TTL=255

    PING程序的基本框架

  

   設置發送數據就是封裝ICMP數據報的過程。需要兩級封裝:首先添加ICMP報頭形成ICMP報文,再添加IP頭形成IP數據報。注意:IP頭不需要我們實現,由內核協議棧自動添加,我們只需要實現ICMP報文。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;

namespace Csharp_PING
{
    class MyPing
    {
        const int SOCKET_ERROR = -1;
        const int ICMP_ECHO = 8;
        public string PingHost(string host,ref int spentTime)
        {
            IPHostEntry serverHE, fromHE;
            int nBytes = 0;
            int dwStart = 0, dwStop = 0;

            Socket socket =
             new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 1000);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000);
            // Get the server endpoint
            try
            {
                serverHE = Dns.GetHostEntry(host);
            }
            catch (Exception)
            {
                return "Host not found"; //解釋主機名失敗
            }

            // Convert the server IP_EndPoint to an EndPoint
            //用IP地址和端口號構造IPEndPoint對象
            IPEndPoint ipepServer = new IPEndPoint(serverHE.AddressList[1], 0);
            EndPoint epServer = (ipepServer);

            
            //獲得本地計算機的EndPoint
            fromHE = Dns.GetHostEntry(Dns.GetHostName());//Dns.GetHostName()獲取本地計算機的主機名
            IPEndPoint ipEndPointFrom = new IPEndPoint(fromHE.AddressList[0], 80);
            EndPoint EndPointFrom = (ipEndPointFrom);

            int PacketSize = 0;
            IcmpPacket packet = new IcmpPacket();
            // 構建ICMP數據包
            //構建數據報、報頭字節、數據設計為字節
            packet.Type = ICMP_ECHO; //值為8,1個字節
            packet.SubCode = 0;  //1個字節
            packet.CheckSum = UInt16.Parse("0");  //2個字節
            packet.Identifier = UInt16.Parse("45");  //2個字節
            packet.SequenceNumber = UInt16.Parse("0"); //2個字節
            int PingData = 32; // sizeof(IcmpPacket) - 8;
            packet.Data = new Byte[PingData];
            //初始化ICMP包的數據部分,即Packet.Data
            for (int i = 0; i < PingData; i++)
            {
                packet.Data[i] = (byte)'a';
            }
            //保存數據報的長度
            PacketSize = PingData + 8;//ICMP數據包大小
            Byte[] icmp_pkt_buffer = new Byte[PacketSize];
            Int32 Index = 0;
            //調用Serialize方法
            //報文總共的字節數
            //序列化數據包,驗證數據包大小
            Index = Serialize(
             packet,
             icmp_pkt_buffer,
             PacketSize,
             PingData);
            if (Index == -1)
            {
                return "Error Creating Packet";
            }

            //將ICMP數據包轉換成 UInt16數組
            //獲取轉換后的數組長度
            Double double_length = Convert.ToDouble(Index);
            Double dtemp = Math.Ceiling(double_length / 2);//向上取整;
            int cksum_buffer_length = Convert.ToInt32(dtemp);
            //生成一個字節數組
            UInt16[] cksum_buffer = new UInt16[cksum_buffer_length];
            //初始化Uint16類型數組
            int icmp_header_buffer_index = 0;
            for (int i = 0; i < cksum_buffer_length; i++)
            {
                cksum_buffer[i] =
                 BitConverter.ToUInt16(icmp_pkt_buffer, icmp_header_buffer_index);
                icmp_header_buffer_index += 2;
            }
            //獲取ICMP數據包的校驗碼 
            // 調用checksum,返回檢查和
            UInt16 u_cksum = checksum(cksum_buffer, cksum_buffer_length);
            //保存校驗碼
            packet.CheckSum = u_cksum;

            // 再次序列化數據包
            //再次檢查報的大小
            Byte[] sendbuf = new Byte[PacketSize];
            Index = Serialize(
             packet,
             sendbuf,
             PacketSize,
             PingData);
            //如果有錯誤,給出提示
            if (Index == -1)
            {
                return "Error Creating Packet";
            }

            dwStart = System.Environment.TickCount; // 開始時間用socket發送數據報
            //通過socket發送數據包
            if ((nBytes = socket.SendTo(sendbuf, PacketSize, 0, epServer)) == SOCKET_ERROR)
            {
                return "Socket Error: cannot send Packet";
            }
            // 初始化緩沖區, 接收緩沖區
            //大小為ICMP報頭+IP報頭的大小(20字節),共32+8+20=60字節.
            Byte[] ReceiveBuffer = new Byte[60];
            nBytes = 0;
            //接收字節流
            bool recd = false;
            int timeout = 0;
            //循環檢查目標主機響應時間
            while (!recd)
            {
                try
                {
                    nBytes = socket.ReceiveFrom(ReceiveBuffer, 60, SocketFlags.None, ref EndPointFrom);
                    if (nBytes == SOCKET_ERROR)//如果超過時間限制,則提示
                    {
                        return "Host not Responding";
                    }
                    else if (nBytes > 0)
                    {
                        dwStop = System.Environment.TickCount - dwStart;// 停止計時
                        spentTime = dwStop;
                        return "Reply from " + epServer.ToString() + " in "
                         + dwStop + "ms.  Received: " + nBytes + " Bytes."
                         +" TTL="+PingTTl(host);
                    }              
                }
                catch (SocketException e)
                {
                    return "Time Out";
                }
            }

            socket.Close();
            return "";
        }

        //序列化數據包(計算數據包的大小並將數據轉換成字節數組)
        public static Int32 Serialize(IcmpPacket packet, Byte[] Buffer,
         Int32 PacketSize, Int32 PingData)
        {
            //取得報文內容,轉化為字節數組,然后計算報文的長度
            Int32 cbReturn = 0;
            //數據報結構轉化為數組
            int Index = 0;

            Byte[] b_type = new Byte[1];
            b_type[0] = (packet.Type);

            Byte[] b_code = new Byte[1];
            b_code[0] = (packet.SubCode);

            Byte[] b_cksum = BitConverter.GetBytes(packet.CheckSum);
            Byte[] b_id = BitConverter.GetBytes(packet.Identifier);
            Byte[] b_seq = BitConverter.GetBytes(packet.SequenceNumber);

            Array.Copy(b_type, 0, Buffer, Index, b_type.Length);
            Index += b_type.Length;

            Array.Copy(b_code, 0, Buffer, Index, b_code.Length);
            Index += b_code.Length;

            Array.Copy(b_cksum, 0, Buffer, Index, b_cksum.Length);
            Index += b_cksum.Length;

            Array.Copy(b_id, 0, Buffer, Index, b_id.Length);
            Index += b_id.Length;

            Array.Copy(b_seq, 0, Buffer, Index, b_seq.Length);
            Index += b_seq.Length;
            //復制數據
            Array.Copy(packet.Data, 0, Buffer, Index, PingData);
            Index += PingData;
            if (Index != PacketSize/*如果不等於數據包長度,則返回出錯信息*/)
            {
                cbReturn = -1;
                return cbReturn;
            }

            cbReturn = Index;
            return cbReturn;
        }

        //計算數據包的校驗碼
        public static UInt16 checksum(UInt16[] buffer, int size)
        {
            Int32 cksum = 0;
            int counter = 0;
            //把ICMP報頭的二進制數據以字節為單位累加起來。
            while (size > 0)
            {
                UInt16 val = buffer[counter];
                cksum += Convert.ToInt32(buffer[counter]);
                counter += 1;
                size -= 1;
            }
            /*弱ICMP報頭為奇數個字節,就會剩下最后1個字節。把最后一個字節是為1個
             *2個字節數據的高字節,這個字節數據的低字節繼續累加*/
            cksum = (cksum >> 16) + (cksum & 0xffff);
            cksum += (cksum >> 16);
            return (UInt16)(~cksum);
        }

        
        public int PingTTl(string host)
        {
            Ping pingSender = new Ping();
            PingOptions options = new PingOptions();

            // Use the default Ttl value which is 128,
            // but change the fragmentation behavior.
            options.DontFragment = true;

            // Create a buffer of 32 bytes of data to be transmitted.
            string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            byte[] buffer = Encoding.ASCII.GetBytes(data);
            int timeout = 1000;
            PingReply reply = pingSender.Send(host, timeout, buffer, options);
            if (reply.Status == IPStatus.Success)
            {
                return reply.Options.Ttl;
            }
            return -1;
        }
    } 
          
    //ICMP數據報類
    public class IcmpPacket
    {
        public Byte Type;    // 類型:回顯請求(8),應答(0)
        public Byte SubCode;    // 編碼
        public UInt16 CheckSum;   // 校驗碼
        public UInt16 Identifier;      // 標識符
        public UInt16 SequenceNumber;     // 序列號
        public Byte[] Data;

    }
}

   

    出現錯誤:

  

    程序的基本實現是這樣,但會出現訪問權限不足的問題,以后再解決。

 

實驗文檔:http://files.cnblogs.com/files/MenAngel/Socket.zip


免責聲明!

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



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