Socket 由淺入深,開發一個真正的通信應用


在說socket之前。我們先了解下相關的網絡知識;

端口

 在Internet上有很多這樣的主機,這些主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不同的端口對應於不同的服務(應用程序)。

例如:http 使用80端口 ftp使用21端口 smtp使用 25端口

端口用來標識計算機里的某個程序   1)公認端口:從0到1023   2)注冊端口:從1024到49151   3)動態或私有端口:從49152到65535

 

Socket相關概念

socket的英文原義是“孔”或“插座”。作為進程通信機制,取后一種意思。通常也稱作“套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。(其實就是兩個程序通信用的。)

socket非常類似於電話插座。以一個電話網為例。電話的通話雙方相當於相互通信的2個程序,電話號碼就是IP地址。任何用戶在通話之前,

首先要占有一部電話機,相當於申請一個socket;同時要知道對方的號碼,相當於對方有一個固定的socket。然后向對方撥號呼叫,

相當於發出連接請求。對方假如在場並空閑,拿起電話話筒,雙方就可以正式通話,相當於連接成功。雙方通話的過程,

是一方向電話機發出信號和對方從電話機接收信號的過程,相當於向socket發送數據和從socket接收數據。通話結束后,一方掛起電話機相當於關閉socket,撤消連接。

 

Socket有兩種類型

流式Socket(STREAM): 是一種面向連接的Socket,針對於面向連接的TCP服務應用,安全,但是效率低;

數據報式Socket(DATAGRAM): 是一種無連接的Socket,對應於無連接的UDP服務應用.不安全(丟失,順序混亂,在接收端要分析重排及要求重發),但效率高.

 

TCP/IP協議

TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標准的協議集,它是為廣域網(WANs)設計的。

UDP協議

UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。

應用層 (Application):應用層是個很廣泛的概念,有一些基本相同的系統級 TCP/IP 應用以及應用協議,也有許多的企業商業應用和互聯網應用。 解釋:我們的應用程序

傳輸層 (Transport):傳輸層包括 UDP 和 TCP,UDP 幾乎不對報文進行檢查,而 TCP 提供傳輸保證。 解釋;保證傳輸數據的正確性

網絡層 (Network):網絡層協議由一系列協議組成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。 解釋:保證找到目標對象,因為里面用的IP協議,ip包含一個ip地址

鏈路層 (Link):又稱為物理數據網絡接口層,負責報文傳輸。 解釋:在物理層面上怎么去傳遞數據

 

你可以cmd打開命令窗口。輸入

netstat -a

查看當前電腦監聽的端口,和協議。有TCP和UDP

 

 

TCP/IP與UDP有什么區別呢?該怎么選擇?

  UDP可以用廣播的方式。發送給每個連接的用戶   而TCP是做不到的

  TCP需要3次握手,每次都會發送數據包(但不是我們想要發送的數據),所以效率低   但數據是安全的。因為TCP會有一個校驗和。就是在發送的時候。會把數據包和校驗和一起   發送過去。當校驗和和數據包不匹配則說明不安全(這個安全不是指數據會不會   別竊聽,而是指數據的完整性)

  UDP不需要3次握手。可以不發送校驗和

  web服務器用的是TCP協議

那什么時候用UDP協議。什么時候用TCP協議呢?   視頻聊天用UDP。因為要保證速度?反之相反

   

下圖顯示了數據報文的格式

 

 

Socket一般應用模式(服務器端和客戶端)

 

 

服務端跟客戶端發送信息的時候,是通過一個應用程序 應用層發送給傳輸層,傳輸層加頭部 在發送給網絡層。在加頭 在發送給鏈路層。在加幀

 

然后在鏈路層轉為信號,通過ip找到電腦 鏈路層接收。去掉頭(因為發送的時候加頭了。去頭是為了找到里面的數據) 網絡層接收,去頭 傳輸層接收。去頭 在到應用程序,解析協議。把數據顯示出來

 

TCP3次握手

在TCP/IP協議中,TCP協議提供可靠的連接服務,采用三次握手建立一個連接。   第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;SYN:同步序列編號(Synchronize SequenceNumbers)。   第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;   第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。

 

 

看一個Socket簡單的通信圖解

 

 

 

1.服務端welcoming socket 開始監聽端口(負責監聽客戶端連接信息)

2.客戶端client socket連接服務端指定端口(負責接收和發送服務端消息)

3.服務端welcoming socket 監聽到客戶端連接,創建connection socket。(負責和客戶端通信)

 

服務器端的Socket(至少需要兩個)

一個負責接收客戶端連接請求(但不負責與客戶端通信)

每成功接收到一個客戶端的連接便在服務端產生一個對應的負責通信的Socket 在接收到客戶端連接時創建. 為每個連接成功的客戶端請求在服務端都創建一個對應的Socket(負責和客戶端通信).

客戶端的Socket

客戶端Socket 必須指定要連接的服務端地址和端口。 通過創建一個Socket對象來初始化一個到服務器端的TCP連接。

 

 

Socket的通訊過程

服務器端:

申請一個socket 綁定到一個IP地址和一個端口上 開啟偵聽,等待接授連接

客戶端: 申請一個socket 連接服務器(指明IP地址和端口號)

服務器端接到連接請求后,產生一個新的socket(端口大於1024)與客戶端建立連接並進行通訊,原監聽socket繼續監聽。

 

 

socket是一個很抽象的概念。來看看socket的位置

 

好吧。我承認看一系列的概念是非常痛苦的,現在開始編碼咯

 

看來編碼前還需要看下sokcet常用的方法

Socket方法: 1)IPAddress類:包含了一個IP地址 例:IPAddress  ip = IPAddress.Parse(txtServer.Text);//將IP地址字符串轉換后賦給ip 2) IPEndPoint類:包含了一對IP地址和端口號 例:IPEndPoint point = new IPEndPoint(ip, int.Parse(txtPort.Text));//將指定的IP地址和端口初始化后賦給point 3)Socket (): 創建一個Socket 例:Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//創建監聽用的socket 4) Bind(): 綁定一個本地的IP和端口號(IPEndPoint) 例:socket.Bind(point);//綁定ip和端口 5) Listen(): 讓Socket偵聽傳入的連接嘗試,並指定偵聽隊列容量 例: socket.Listen(10); 6) Connect(): 初始化與另一個Socket的連接 7) Accept(): 接收連接並返回一個新的socket 例:Socket connSocket =socket .Accept (); 8 )Send(): 輸出數據到Socket 9) Receive(): 從Socket中讀取數據 10) Close(): 關閉Socket (銷毀連接)

 

首先創建服務端,服務端是用來監聽客戶端請求的。

創建服務器步驟:   第一步:創建一個Socket,負責監聽客戶端的請求,此時會監聽一個端口   第二步:客戶端創建一個Socket去連接服務器的ip地址和端口號   第三步:當連接成功后。會創建一個新的socket。來負責和客戶端通信

復制代碼
 1 public static void startServer()  2  {  3  4 //第一步:創建監聽用的socket  5 Socket socket = new Socket  6  (  7 AddressFamily.InterNetwork, //使用ip4  8 SocketType.Stream,//流式Socket,基於TCP  9 ProtocolType.Tcp //tcp協議 10  ); 11 12 //第二步:監聽的ip地址和端口號 13 //ip地址 14 IPAddress ip = IPAddress.Parse(_ip); 15 //ip地址和端口號 16 IPEndPoint point = new IPEndPoint(ip, _point); 17 18 //綁定ip和端口 19 //端口號不能占用:否則:以一種訪問權限不允許的方式做了一個訪問套接字的嘗試 20 //通常每個套接字地址(協議/網絡地址/端口)只允許使用一次。 21 try 22  { 23  socket.Bind(point); 24  } 25 catch (Exception) 26  { 27 28 if (new IOException().InnerException is SocketException) 29 Console.WriteLine("端口被占用"); 30  } 31 //socket.Bind(point); 32 33 //第三步:開始監聽端口 34 35 //監聽隊列的長度 36 /*比如:同時有3個人來連接該服務器,因為socket同一個時間點。只能處理一個連接 37  * 所以其他的就要等待。當處理第一個。然后在處理第二個。以此類推 38  * 39  * 這里的10就是同一個時間點等待的隊列長度為10,即。只能有10個人等待,當第11個的時候。是連接不上的 40 */ 41 socket.Listen(10); 42 43 string msg = string.Format("服務器已經啟動........\n監聽ip為:{0}\n監聽端口號為:{1}\n", _ip, _point); 44  showMsg(msg); 45 46 Thread listen = new Thread(Listen); 47 listen.IsBackground = true; 48  listen.Start(socket); 49 50 }
復制代碼

 

 

觀察上面的代碼。開啟了一個多線程。去執行Listen方法,Listen是什么?為什么要開啟一個多線程去執行?

回到上面的 "Socket的通訊過程"中提到的那個圖片,因為有兩個地方需要循環執行

第一個:需要循環監聽來自客戶端的請求

第二個:需要循環獲取來自客服端的通信(這里假設是客戶端跟服務器聊天)

額。這跟使用多線程有啥關系?當然有。因為Accept方法。會阻塞線程。所以用多線程,避免窗體假死。你說呢?

看看Listen方法

復制代碼
 1 /// <summary>  2 /// 多線程執行  3 /// Accept方法。會阻塞線程。所以用多線程  4 /// </summary>  5 /// <param name="o"></param>  6 static void Listen(object o)  7  {  8 Socket socket = o as Socket;  9 10 //不停的接收來自客服端的連接 11 while (true) 12  { 13 //如果有客服端連接,則創建通信用是socket 14 //Accept方法。會阻塞線程。所以用多線程 15 //Accept方法會一直等待。直到有連接過來 16 Socket connSocket = socket.Accept(); 17 18 //獲取連接成功的客服端的ip地址和端口號 19 string msg = connSocket.RemoteEndPoint.ToString(); 20 showMsg(msg + "連接"); 21 22 //獲取本機的ip地址和端口號 23 //connSocket.LocalEndPoint.ToString(); 24 25 /* 26  如果不用多線程。則會一直執行ReceiveMsg 27  * 就不會接收客服端連接了 28 */ 29 Thread th = new Thread(ReceiveMsg); 30 th.IsBackground = true; 31  th.Start(connSocket); 32 33  } 34 }
復制代碼

 

細心的你在Listen方法底部又看到了一個多線程。執行ReceiveMsg,對,沒錯。這就是上面說的。循環獲取消息

ReceiveMsg方法定義:

復制代碼
 1  /// <summary>  2 /// 接收數據  3 /// </summary>  4 /// <param name="o"></param>  5 static void ReceiveMsg(object o)  6  {  7 Socket connSocket = o as Socket;  8 while (true)  9  { 10 11 //接收數據 12 byte[] buffer = new byte[1024 * 1024];//1M 13 int num = 0; 14 try 15  { 16 //接收數據保存發送到buffer中 17 //num則為實際接收到的字節個數 18 19 //這里會遇到這個錯誤:遠程主機強迫關閉了一個現有的連接。所以try一下 20 num = connSocket.Receive(buffer); 21 //當num=0.說明客服端已經斷開 22 if (num == 0) 23  { 24  connSocket.Shutdown(SocketShutdown.Receive); 25  connSocket.Close(); 26 break; 27  } 28  } 29 catch (Exception ex) 30  { 31 if (new IOException().InnerException is SocketException) 32 Console.WriteLine("網絡中斷"); 33 else 34  Console.WriteLine(ex.Message); 35 break; 36  } 37 38 //把實際有效的字節轉化成字符串 39 string str = Encoding.UTF8.GetString(buffer, 0, num); 40 showMsg(connSocket.RemoteEndPoint + "說:\n" + str); 41 42 43 44  } 45 }
復制代碼

 

提供服務器的完整代碼如下:

復制代碼
  1 using System;  2 using System.Collections.Generic;  3 using System.Linq;  4 using System.Text;  5 using System.Net.Sockets;  6 using System.Net;  7 using System.Threading;  8 using System.IO;  9 namespace CAServer  10 {  11 class Program  12  {  13  14 //當前主機ip  15 static string _ip = "192.168.1.2";  16 //端口號  17 static int _point = 8000;  18  19 static void Main(string[] args)  20  {  21 //Thread thread = new Thread(startServer);  22 //thread.Start();  23  24  startServer();  25  26  Console.ReadLine();  27  28  }  29  30 public static void startServer()  31  {  32  33 //第一步:創建監聽用的socket  34 Socket socket = new Socket  35  (  36 AddressFamily.InterNetwork, //使用ip4  37 SocketType.Stream,//流式Socket,基於TCP  38 ProtocolType.Tcp //tcp協議  39  );  40  41 //第二步:監聽的ip地址和端口號  42 //ip地址  43 IPAddress ip = IPAddress.Parse(_ip);  44 //ip地址和端口號  45 IPEndPoint point = new IPEndPoint(ip, _point);  46  47 //綁定ip和端口  48 //端口號不能占用:否則:以一種訪問權限不允許的方式做了一個訪問套接字的嘗試  49 //通常每個套接字地址(協議/網絡地址/端口)只允許使用一次。  50 try  51  {  52  socket.Bind(point);  53  }  54 catch (Exception)  55  {  56  57 if (new IOException().InnerException is SocketException)  58 Console.WriteLine("端口被占用");  59  }  60 //socket.Bind(point);  61  62 //第三步:開始監聽端口  63  64 //監聽隊列的長度  65 /*比如:同時有3個人來連接該服務器,因為socket同一個時間點。只能處理一個連接  66  * 所以其他的就要等待。當處理第一個。然后在處理第二個。以此類推  67  *  68  * 這里的10就是同一個時間點等待的隊列長度為10,即。只能有10個人等待,當第11個的時候。是連接不上的  69 */  70 socket.Listen(10);  71  72 string msg = string.Format("服務器已經啟動........\n監聽ip為:{0}\n監聽端口號為:{1}\n", _ip, _point);  73  showMsg(msg);  74  75 Thread listen = new Thread(Listen);  76 listen.IsBackground = true;  77  listen.Start(socket);  78  79  }  80 /// <summary>  81 /// 多線程執行  82 /// Accept方法。會阻塞線程。所以用多線程  83 /// </summary>  84 /// <param name="o"></param>  85 static void Listen(object o)  86  {  87 Socket socket = o as Socket;  88  89 //不停的接收來自客服端的連接  90 while (true)  91  {  92 //如果有客服端連接,則創建通信用是socket  93 //Accept方法。會阻塞線程。所以用多線程  94 //Accept方法會一直等待。直到有連接過來  95 Socket connSocket = socket.Accept();  96  97 //獲取連接成功的客服端的ip地址和端口號  98 string msg = connSocket.RemoteEndPoint.ToString();  99 showMsg(msg + "連接"); 100 101 //獲取本機的ip地址和端口號 102 //connSocket.LocalEndPoint.ToString(); 103 104 /* 105  如果不用多線程。則會一直執行ReceiveMsg 106  * 就不會接收客服端連接了 107 */ 108 Thread th = new Thread(ReceiveMsg); 109 th.IsBackground = true; 110  th.Start(connSocket); 111 112  } 113  } 114 /// <summary> 115 /// 接收數據 116 /// </summary> 117 /// <param name="o"></param> 118 static void ReceiveMsg(object o) 119  { 120 Socket connSocket = o as Socket; 121 while (true) 122  { 123 124 //接收數據 125 byte[] buffer = new byte[1024 * 1024];//1M 126 int num = 0; 127 try 128  { 129 //接收數據保存發送到buffer中 130 //num則為實際接收到的字節個數 131 132 //這里會遇到這個錯誤:遠程主機強迫關閉了一個現有的連接。所以try一下 133 num = connSocket.Receive(buffer); 134 //當num=0.說明客服端已經斷開 135 if (num == 0) 136  { 137  connSocket.Shutdown(SocketShutdown.Receive); 138  connSocket.Close(); 139 break; 140  } 141  } 142 catch (Exception ex) 143  { 144 if (new IOException().InnerException is SocketException) 145 Console.WriteLine("網絡中斷"); 146 else 147  Console.WriteLine(ex.Message); 148 break; 149  } 150 151 //把實際有效的字節轉化成字符串 152 string str = Encoding.UTF8.GetString(buffer, 0, num); 153 showMsg(connSocket.RemoteEndPoint + "說:\n" + str); 154 155 156 157  } 158  } 159 /// <summary> 160 /// 顯示消息 161 /// </summary> 162 static void showMsg(string msg) 163  { 164  Console.WriteLine(msg); 165 //Console.ReadKey(); 166  } 167  } 168 }
復制代碼

 

運行代碼。顯示如下

是不是迫不及待的想試試看效果。好吧其實我也跟你一樣,cmd打開dos命令提示符,輸入

telnet  192.168.1.2 8000

回車,會看到窗體名稱變了

 

然后看到服務器窗口

然后在客戶端輸入數字試試

我輸入了1 2 3 。當然,在cmd窗口是不顯示的。這不影響測試。

小技巧:為了便於測試,可以創建一個xx.bat文件。里面寫命令

telnet  192.168.1.2 8000

這樣只有每次打開就會自動連接了。

當然。這僅僅是測試。現在寫一個客戶端,

創建一個winfrom程序,布局如下顯示

請求服務器代碼就很容易了。直接附上代碼

復制代碼
 1 using System;  2 using System.Collections.Generic;  3 using System.ComponentModel;  4 using System.Data;  5 using System.Drawing;  6 using System.Linq;  7 using System.Text;  8 using System.Windows.Forms;  9 using System.Net; 10 using System.Net.Sockets; 11 12 namespace WFAClient 13 { 14 public partial class Form1 : Form 15  { 16 public Form1() 17  { 18  InitializeComponent(); 19  } 20  Socket socket; 21 private void btnOk_Click(object sender, EventArgs e) 22  { 23 //客戶端連接IP 24 IPAddress ip = IPAddress.Parse(tbIp.Text); 25 26 //端口號 27 IPEndPoint point = new IPEndPoint(ip, int.Parse(tbPoint.Text)); 28 29 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 30 31 try 32  { 33  socket.Connect(point); 34 msg("連接成功"); 35 btnOk.Enabled = false; 36  } 37 catch (Exception ex) 38  { 39  msg(ex.Message); 40  } 41  } 42 private void msg(string msg) 43  { 44  tbMsg.AppendText(msg); 45 46  } 47 48 private void btnSender_Click(object sender, EventArgs e) 49  { 50 //發送信息 51 if (socket != null) 52  { 53 byte[] buffer = Encoding.UTF8.GetBytes(tbContent.Text); 54  socket.Send(buffer); 55 /* 56  * 如果不釋放資源。當關閉連接的時候 57  * 服務端接收消息會報如下異常: 58  * 遠程主機強迫關閉了一個現有的連接。 59 */ 60 //socket.Close(); 61 //socket.Disconnect(true); 62  } 63  } 64  } 65 }
復制代碼

 

運行測試,這里需要同時運行客戶端和服務器,

首先運行服務器,那怎么運行客戶端呢。

右鍵客戶端項目。調試--》啟用新實例

 

 

 

好了。一個入門的過程就這樣悄悄的完成了。

以上內容來自:http://www.cnblogs.com/nsky/p/4501782.html

 

根據上面的內容,已經可以開發出一個可以正常通信的Socket示例了,

接下來首先要考慮的就是服務器性能問題

1)在服務器接收數據的時候,定義了一個1M的Byte Buffer,有些設計的更大。更大Buffer可以保證客戶端發送數據量很大的情況全部能接受完全。但是作為一個服務器每收到一條客戶端請求,都要申請一個1M的Buffer去裝客戶端發送的數據。如果客戶端的並發量很大的情況,還沒等到網絡的瓶頸,服務器內存開銷已經吃不消了。

對於這個問題的解決思路是:

定義一個小Buffer,每次接受客戶端請求用:

byte[] bufferTemp = new byte[1024];

和一個大Buffer,裝客戶端的所有數據,其中用到了strReceiveLength,是客戶端發送的總長度,稍后再解釋:

byte[] buffer = new byte[Convert.ToInt32(strReceiveLength)];

 

改寫while (true)循環,每次接受1K的數據,然后用Array.Copy方法,把bufferTemp中的數據復制給buffer:

num = connSocket.Receive(bufferTemp, SocketFlags.None);

ArrayUtil.ArrayCopy(bufferTemp, buffer, check, num);

check += num;

這個Array.Copy是重點,因為TCP數據流在傳輸過程中也是一個包一個包的傳送,最大不超過8K。所以每次接受到的數據,也就是bufferTemp這個變量有可能裝滿,也有可能裝不滿。所以在拷貝的時候一定按照這次接受的長度順序的放入buffer中。等到客戶端全部數據發送完成后,再把buffer轉換:

strReceive = Encoding.UTF8.GetString(buffer, 0, buffer.Length);

而不能夠每次都轉換,再strReceive += 一個Byte數組。這樣做的后果就是中文會被截斷,因為中文在UTF-8編碼下占3-4個字節,很容易出現亂碼。

 

2)數據長度校驗

TCP在傳輸過程中難免會有數據發送不全或者丟失的情況。所以在客戶端發送數據的時候一定帶上校驗長度:

byte[] btyLength = Encoding.UTF8.GetBytes(strContent);

string strLength = btyLength.Length.ToString().PadLeft(8, '0');

string sendData = strLength + strContent;

byte[] buffer = Encoding.UTF8.GetBytes(sendData);

socketClient.Send(buffer);

這樣在服務器端,先把要接受的長度收到:

byte[] bufferLength = new byte[8];
num = connSocket.Receive(bufferLength);

strReceiveLength = Encoding.UTF8.GetString(bufferLength, 0, bufferLength.Length);

在循環里用下面的判斷,來校驗和判斷是否已經接受完畢:

if (check == Convert.ToInt32(strReceiveLength))

 

3)設計上一些方式

很多局域網的部署是分層的,也就是分內網和外網。服務器部署一定要在外網上部署,這里的外網指的是在客戶端之上的網段上。

比如192.168.1.22下有個無線路由,無線連接的IP段為192.168.2.1~254

服務器搭建在192.168.1網段下,192.168.2的客戶端是可以訪問的。但是相反則不行,192.168.1網段下的設備無法主動找到192.168.2的服務器。

 


免責聲明!

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



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