網絡編程之Socket詳解

在說socket以前。咱們先了解下相關的網絡知識;web

端口安全

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

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

 

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

 

Socket相關概念socket

socket的英文原義是「孔」或「插座」。做爲進程通訊機制,取後一種意思。一般也稱做「套接字」,用於描述IP地址和端口,是一個通訊鏈的句柄。(其實就是兩個程序通訊用的。)tcp

socket很是相似於電話插座。以一個電話網爲例。電話的通話雙方至關於相互通訊的2個程序,電話號碼就是IP地址。任何用戶在通話以前,ide

首先要佔有一部電話機,至關於申請一個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 }
View Code

 

運行代碼。顯示以下

是否是火燒眉毛的想試試看效果。好吧其實我也跟你同樣,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 }

 

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

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

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

 

 

 

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

 

 

 

相關文章
相關標籤/搜索