本節介紹如何使用基礎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