網絡編程之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 }
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 }

 

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

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

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

 

 

 

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

 

 

 


免責聲明!

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



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