網絡編程一直是經久不衰的話題,今天就網絡編程里面一些問題做個總結,由于UDP相對于TCP的處理問題比較簡單,所以這次總結的都是有關TCP的。
很多人認為單臺服務器的最大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。
為了證明服務端的I連接數P和端口沒有關系 我寫了一個測試程序,該程序為了屏蔽線程棧消耗的內存采用了.net 封裝好的IOCP的方式。
客戶端測試機使用了3臺普通的PC機 ,三臺臺器分別與服務器保持5W,4W,1W個連接,統計服務端的連接數是否是10W。
程序代碼如下:
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.測試結果:
(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是否完整來校驗包的正確性。一旦數據包出現錯誤一般有兩種測試處理:第一是直接丟棄整個包, 該方法簡單但可能會失去某些比較重要的數據。 第二個是對根據報文對整個包進行遍歷把可能沒有錯誤的數據保持下來,但是該方案比較麻煩。
最后一句感言:無論什么平臺和語言,基礎知識都是通用的,制約發展的因素不是我們會哪幾種語言,而是我們運用基礎知識去解決實際問題的能力。
新聞熱點
疑難解答