網絡編程一直是經久不衰的話題,今天就網絡編程裏面一些問題作個總結,因爲UDP相對於TCP的處理問題比較簡單,因此此次總結的都是有關TCP的。html
不少人認爲單臺服務器的最大TCP鏈接數是65536也就是受限於服務器的端口的數量 若是服務器想開6W以上的TCP鏈接就要綁定多個IP。其實這是一個重大的錯誤認識,其實按照TCP/IP協議的規定,肯定一個鏈接的惟一標識是一個4元組 <本地IP,本地端口,遠程IP,遠程端口>,由於服務器綁定的端口和IP是固定的(通常進程都只綁定一個端口和IP),因此決定鏈接的因素就是客戶機器的遠程IP和遠程端口 ,與服務器端口數量根本沒有任何關係,也就是說客戶機的IP範圍和端口號範圍纔是決定鏈接的重要因素,咱們知道IP地址的範圍是2^32個(這裏IP和端口不考慮保留問題),端口的數量是2^16個 那麼服務器保持的最大鏈接數的理論值是2^32*2^16=2^48 。不過,難道服務器端真的能開那麼多鏈接麼,答案是要看服務器有多少物理內存。也就是說物理內存的大小纔是決定服務器能開多少鏈接的決定因數(固然也和操做系統的設置有關 例如:最大文件句柄的數量,但整體來講是受限於物理內存),若是隻有1G的物理內存想開100W的鏈接這是不可能的。咱們知道創建一個socket鏈接和打開一個文件同樣,是由操做系統創建一個文件句柄,文件句柄指向一個稱爲文件對象的windows內核對象 ,建立文件對象的數量決定了TCP的最大鏈接數,也能夠這麼認爲:對於有限的資源 服務器最大的TCP鏈接數是操做系統能打開文件的最大數量,若是系統資源足夠大時那麼最大就是2^48。node
爲了證實服務端的I鏈接數P和端口沒有關係 我寫了一個測試程序,該程序爲了屏蔽線程棧消耗的內存採用了.net 封裝好的IOCP的方式。編程
客戶端測試機使用了3臺普通的PC機 ,三臺臺器分別與服務器保持5W,4W,1W個鏈接,統計服務端的鏈接數是不是10W。windows
程序代碼以下:服務器
1. 服務端代碼:網絡
/// <summary> /// IOCP soket /// </summary> class Server { private Socket connSocket; //統計鏈接總數 private static int count; public Server() { connSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); IPAddress ip = IPAddress.Parse("192.168.1.123"); IPEndPoint endPoint = new IPEndPoint(ip, 6530); connSocket.Bind(endPoint); connSocket.Listen(50000); } /// <summary> /// 接收客戶端鏈接 /// </summary> public void Start() { IAsyncResult acceptResult=null; acceptResult = connSocket.BeginAccept(AcceptCallback, connSocket); } private void AcceptCallback(IAsyncResult ar) { Socket accetpSocket = ar.AsyncState as Socket; if(accetpSocket==null) { return; } Socket receiveSocket= accetpSocket.EndAccept(ar); //打印鏈接數 count++; Console.WriteLine("鏈接數:{0}",count); Start(); Receieve(receiveSocket); } /// <summary> /// 接收發送的數據 /// </summary> /// <param name="receiveSocket">接收數據的socket</param> private void Receieve(Socket receiveSocket) { byte[] buffer = new byte[28]; receiveSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, (state) => { Socket receive = state.AsyncState as Socket; try { int length = receive.EndReceive(state); Console.WriteLine(Encoding.Unicode.GetString(buffer)); Receieve(receive); } catch (SocketException socketEx) { receive.Dispose(); } }, receiveSocket); } }
2. 客戶端代碼:多線程
/// <summary> /// 測試客戶端 /// </summary> class Program { static List<Socket> socketList = new List<Socket>(); static void Main(string[] args) { for (int i = 0; i < 40000;i++) { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); int port = 6530; IPAddress ip = IPAddress.Parse("192.168.1.123");//本地 socket.Connect(ip, port); socketList.Add(socket); } Console.ReadKey(); } }
3.測試結果: socket
(1)Server運行結果:性能
圖3.1測試
(2)Tcpview監視的鏈接:
圖3.2
(3)server佔用資源狀況:
圖3.3
根據圖3.1的結果能夠看出, 服務端接受的鏈接數爲10W ,因此說 最大TCP鏈接數與服務端的端口的數量不要緊,並且根據圖3.2得知服務端的端口是保持不變的,變的只是客戶端的IP和端口,因此一個鏈接的惟一標識就是一個4元組 <本地IP,本地端口,遠程IP,遠程端口>。
進一步分析,咱們假定該程序全部的內存都是鏈接佔用的,根據圖3.3能夠看出10W個鏈接佔用了 200 M內存,也就是每一個鏈接佔用了 2kb 在物理內存4G的32操做系統下開起鏈接數最大約爲100W(用戶模式使用了2G的尋址空間),可是即便有4G的物理內存也未必能開啓50W個鏈接,由於在內核模式的地址空間分爲分頁內存池和非分頁內存池(用戶模式下老是分頁的),文件對象這個windows內核對象是存儲在內核模式下的非分頁內存池,所謂非分頁內存池就是該內存區始終在物理內存而不會在磁盤文件的頁交換文件中,而內核模式的分頁內存池也佔用了一部分物理內存,致使整個內核模式的非分頁內存池不能達到2G,因此100W個鏈接在32位操做系統是也沒法開啓。那麼在64位操做系統上,理論上內存大的話是沒有限制的,亞馬遜曾經測試過node.js的鏈接數 100W個鏈接佔用了16G的內存(.net程序沒有測過100W的內存狀況,因此4G的100W個鏈接只是推測,和實際狀況可能差異較大) http://blog.caustik.com/2012/08/19/node-js-w1m-concurrent-connections/。
最後還要補充一句:服務器的性能和處理多少鏈接數不要緊,即便100W個鏈接只是鏈接而不發送數據,服務器只是浪費點內存,若是100W個同時發送數據,那麼服務器可能會處理不過來,處理不過來只是相對於而言,若是發送的每一個鏈接消息很是小,服務器的配置好,並且服務端處理消息的邏輯相對簡單,那可能會處理過來,相反 一個鏈接只發送一條消息,但服務器處理這條消息的邏輯很是複查,好比要作個幾分鐘的大型的計算,那麼服務器也是處理不過來的。因此對於服務端來講 一味的追求多少鏈接數是不可取的,主要仍是要衡量處理消息的邏輯。
TCP/IP協議限制了每一個鏈接的最大傳輸速度。並且使各大鏈接的速度保持一直,因此說不考慮服務端的處理速度等其餘因數,多個鏈接的發送數據速度要比單個鏈接的快,因此迅雷採用了多線程下載的方式。固然,咱們實際開發中要採用多少個鏈接須要根據服務端對每一個鏈接的處理能力進行測算,找出一個平衡點。
在.net平臺之間進行通信時能夠忽略主機字節序的問題,.net平臺統一採用了小端法,若是須要跨平臺傳輸,就須要統一字節序,通常有兩種方案:
1.採用字符串的形式,注意UNICODE編碼的字符串佔2個字節,因此一樣會有字節序的問題,因此應該使用ANSCI的字符串。
2.都採用大端或小端,在報文裏用一個字節作個表識來代表主機的字節序。
由於TCP屬於流式傳輸,因此沒有數據邊界,因此咱們不知道每次發送數據的長度,因此每次接受數據時多是兩個包或者多是一個不完整的包,這就咱們須要處理TCP斷包和粘包這兩個場景,通常解決方案是在頭部前4個字節來標識整個數據包的長度,接受數據時先接受4個字節的長度,而後根據解析出的包長進行循環接收直到數據包接完整爲止,注意接收前4個字節的包長是也要進行循環接受直到接滿4個字節。
有時候數據包傳輸過程當中會出現錯誤,通常都是校驗CRC是否完整來校驗包的正確性。一旦數據包出現錯誤通常有兩種測試處理:第一是直接丟棄整個包, 該方法簡單但可能會失去某些比較重要的數據。 第二個是對根據報文對整個包進行遍歷把可能沒有錯誤的數據保持下來,可是該方案比較麻煩。
最後一句感言:不管什麼平臺和語言,基礎知識都是通用的,制約發展的因素不是咱們會哪幾種語言,而是咱們運用基礎知識去解決實際問題的能力。