用SocketAsyncEventArgs+池+線程構(gòu)建服務器“推”
2009-07-23 15:46 by Creason New, 3573 visits, 收藏, 編輯下篇文章:【用SocketAsyncEventArgs+池+線程構(gòu)建服務器“推”】的源代碼分析1
說到服務器推技術(shù),可謂是讓人們又愛又恨,愛的是它能實現(xiàn)特殊的功能效果,恨的是它性能太低實現(xiàn)起來太復雜。大家知道Framework里有Socket、異步Socket,但實現(xiàn)起來只能是權(quán)且觀賞,要真實際應用卻問題重重,于是我拜讀了園子里的很多大俠們的文章,從中找到了一些線索,于是參考兼獨創(chuàng)弄出來一套方案。好,進入正題。
先來個引子。要做服務器推,當然要保證幾點:連接數(shù)、性能、穩(wěn)定性、靈活性,不能一臺服務器只能連個百八十個用戶,也不能一推就弄個CPU占用率100%甚至直接down掉,穩(wěn)定嘛,自然就要保證服務器不能因為用戶的變化而斷開服務,而且還要有判斷用戶的能力,不能那邊用戶斷開半個小時了,你服務器還以為人家還在線呢。因此,考慮到這些因素,我采用了SocketAsyncEventArgs對象(實際上就是BeginXX,EndXX一個變種)作為異步傳輸,構(gòu)建連接池和緩沖區(qū)來提高性能,以及多線程來管理接收與發(fā)送同時進行的情況。
再介紹一下原理吧。首先為了不在Accept一個用戶之后new一個對象,聰明一點的辦法就是在服務一啟動就把所有的對象創(chuàng)建好了,然后Accept一個用戶之后從“池子”里拿出一個對象給這個用戶,當這個用戶斷開了再把這個對象放回“池子”里,這也是線程池的原理。而且在創(chuàng)建連接池的時候我們把接收的緩沖區(qū)也給創(chuàng)建好了,也就是說緩沖區(qū)也不用new了,直接拿來用(嘿嘿,性能應該不會太差吧)。當然連接池里對象的個數(shù)根據(jù)你的服務器允許的并發(fā)的連接數(shù)在創(chuàng)建連接池的時候指定。再就是接收和發(fā)送同時進行的問題了,我把接收和發(fā)送放到兩個線程里,這樣就可以一邊發(fā)送一邊接收了,擺脫了對講機式的通話,而且性能上多線程要好些。
貼個類圖:

各個類說明:
MySocketAsyncEventArgs:為了給SocketAsyncEventArgs對象一個用戶和收發(fā)的屬性,我讓MySocketAsyncEventArgs繼承自SocketAsyncEventArgs,并添加了UID和Property屬性,UID保持用戶標識,Property值為“Send”或“Receive”,就是標記“我”是管發(fā)送信息還是接收信息的。
SocketAsyncEventArgsWithId:連接池里的一個連接單元,它有一個用戶標識UID(跟MySocketAsyncEventArgs對象的UID是相同的值)和兩個分別用于接收和發(fā)送的MySocketAsyncEventArgs對象。
RequestHandler:所傳輸數(shù)據(jù)的協(xié)議。
BufferManager:給SocketAsyncEventArgsWithId對象分配和管理緩沖區(qū)的類。
SocketListener和SocketClient就不用解釋了,是地球人都能看懂。
服務器端代碼
1 class Program
3 static SocketListener server;
4 static string command;
5 static bool exit = true;
6 static void Main(string[] args)
7 {
8 Console.WriteLine("Server is Starting"+Environment.NewLine);
9 try
10 {
11 SocketListener.GetIDByIPFun getid = new SocketListener.GetIDByIPFun(GetId);//創(chuàng)建一個類庫可以調(diào)用的根據(jù)IP返回用戶標識的方法的委托,比如bill gate的ip是192.168.1.1,那么類庫不知道bill gate,只知道ip,所以在用的時候要讓類庫知道這么一個對應關(guān)系
12 server = new SocketListener(20000, 32768, getid);//參數(shù)為:并行連接的數(shù)目,每個連接的緩沖區(qū)大小,ip/id對應的函數(shù)委托
13 server.StartListenThread += new SocketListener.StartListenHandler(server_listen);//可以開始監(jiān)聽用戶發(fā)送的信息時觸發(fā)的事件
14 server.OnMsgReceived += new SocketListener.ReceiveMsgHandler(server_recInfo);//服務器收到來著客戶端的信息時觸發(fā)的事件
15 server.OnSended += new SocketListener.SendCompletedHandler(server_sendcompleted);//服務器發(fā)送完信息時觸發(fā)的事件
16 server.Init();
17 server.Start(2008);
18 Console.WriteLine("Server is Running" + Environment.NewLine);
19 Console.WriteLine("1-send 2-stop" + Environment.NewLine);
20
21 while (exit)
22 {
23 Console.WriteLine("command:");
24 command = Console.ReadLine();
25 if (command == "1")
26 {
27 Console.WriteLine("input your msg:");
28 string msg = Console.ReadLine();
29 Console.WriteLine("who will you send:");
30 string uid = Console.ReadLine();
31 server.Send(uid, msg);
32 continue;
33 }
34 else if (command == "2")
35 {
36 server.Stop();
37 continue;
38 }
39 else
40 {
41 Console.WriteLine("command not found");
42 continue;
43 }
44 }
45 }
46 catch (Exception e)
47 {
48 Console.WriteLine(e.Message + Environment.NewLine);
49 Console.ReadLine();
50 }
51 }
52 static void server_listen()
53 {
54 Thread listenthread = new Thread(new ThreadStart(server.Listen));
55 listenthread.Start();//開啟新的線程用于監(jiān)聽發(fā)來的數(shù)據(jù)
56 }
57 static void server_recInfo(string uid, string info)
58 {
59 Console.WriteLine("msg:" + info + "\n from:" + uid + Environment.NewLine);
60 }
61 static void server_sendcompleted(bool successorfalse)
62 {
63 Console.WriteLine("your msg send success" + successorfalse + Environment.NewLine);
64 }
65 static string GetId(string IP)
66 {
67 return "110";
68 }
69 }
客戶器端代碼
2 class Program
3 {
4 static SocketClient client;
5 static string command;
6 static bool exit = true;
7 static void Main(string[] args)
8 {
9 Console.WriteLine("Client is Starting" + Environment.NewLine);
10 try
11 {
12 client = new SocketClient("192.168.170.1", 2008);
13 client.StartListenThread += new SocketClient.StartListenHandler(client_listen);
14 client.OnMsgReceived += new SocketClient.ReceiveMsgHandler(client_recInfo);
15 client.OnSended += new SocketClient.SendCompleted(client_sendcompleted);
16 Console.WriteLine("Client is Running" + Environment.NewLine);
17 Console.WriteLine("1-connect 2-disconnect 3-send" + Environment.NewLine);
18
19 while (exit)
20 {
21 Console.WriteLine("command:");
22 command = Console.ReadLine();
23 if (command == "1")
24 {
25 client.Connect();
26 continue;
27 }
28 else if (command == "2")
29 {
30 client.Disconnect();
31 continue;
32 }
33 else if (command == "3")
34 {
35 Console.WriteLine("input your msg:");
36 string msg = Console.ReadLine();
37 client.Send(msg);
38 }
39 else
40 {
41 Console.WriteLine("command not found");
42 continue;
43 }
44 }
45 }
46 catch (Exception e)
47 {
48 Console.WriteLine(e.Message + Environment.NewLine);
49 Console.ReadLine();
50 }
51 }
52
53 static void client_listen()
54 {
55 Thread listenthread = new Thread(new ThreadStart(client.Listen));
56 listenthread.Start();
57 }
58
59 static void client_recInfo(string info)
60 {
61 Console.WriteLine(info);
62 }
63
64 static void client_sendcompleted(bool successorfalse)
65 {
66 Console.WriteLine("msg send success:" + successorfalse + Environment.NewLine);
67 }
68 }



參考文章:http://www.cnblogs.com/jeriffe/articles/1407603.html
http://www.cnblogs.com/chuncn/archive/2009/06/22/1508018.html
http://www.cnblogs.com/dabing/archive/2009/07/10/1520586.html
http://www.cnblogs.com/JimmyZhang/archive/2008/09/16/1291854.html
附上源代碼:http://files.cnblogs.com/niuchenglei/SocketLib.zip







#1樓 povy 2009-07-23 16:25
看看~~#2樓 Jeffrey Zhao 2009-07-23 17:08
沒有仔細看實現(xiàn),你這個25M是什么內(nèi)存?看看虛擬內(nèi)存?// 異步Socket應該用了IOCP,性能的確會很好。
#3樓 大 兵 2009-07-23 17:45
寫的不錯。1:其實客戶端的鏈接,發(fā)送和接收以及服務端的檢測端口,接收以及發(fā)送我們都是用了異步。我們并沒有等一個操作完成了才進行下個操作。所以還是可以保證同一時刻同一端口接收和發(fā)送在同時進行的。
2:服務器推我猜想你是在解決客戶端關(guān)閉而服務端不知道這個問題的吧。修正接收過程【e.BytesTransferred > 0 && e.SocketError == SocketError.Success)】否則就關(guān)閉客戶端連接。
圖畫的不錯。
#4樓 小黑三 2009-07-23 18:09
不錯,能否把代碼上傳???#5樓 非空 2009-07-23 22:15
那個并發(fā)連接數(shù)的問題解決了,能否透露下你怎么解決通訊中出現(xiàn)的各種異常啊,比如積極拒絕啊 那邊突然掉了 拔網(wǎng)線了等等#6樓 Galactica 2009-07-24 08:36
圖很漂亮,用啥畫的?#7樓[樓主] 小老牛 2009-07-24 09:03
@Jeffrey Zhao25M內(nèi)存就是任務管理器里面的內(nèi)存啊,虛擬內(nèi)存從哪里看啊,你看看我貼的那個任務管理器的截圖的第三個和第四個進程。
#8樓[樓主] 小老牛 2009-07-24 09:05
@大 兵其實在我的類庫里面已經(jīng)封裝了檢測機制了,就是用e.BytesTransferred > 0 && e.SocketError == SocketError.Success來檢測連接狀態(tài)的。把源代碼貼上,里面的注釋很詳細,你看看吧。謝謝回貼
#9樓[樓主] 小老牛 2009-07-24 09:06
@小黑三貼上代碼地址了,看看吧,謝謝
#10樓[樓主] 小老牛 2009-07-24 09:08
@非空我用了e.BytesTransferred > 0 和 e.SocketError == SocketError.Success來確定連接的狀態(tài),我想這足以解決各種連接中斷的問題了吧,你有什么高招嗎?請指教。
#11樓[樓主] 小老牛 2009-07-24 09:08
@Galactica那Enterprise Architect做的
#12樓 Jeffrey Zhao 2009-07-24 09:17
任務管理器里select column,內(nèi)存有許多種。
#13樓 Autumn770[未注冊用戶]2009-07-29 12:05
老兄,我看了你的服務器后,我試了一下,在客戶端斷開后,再連接以后發(fā)消息服務器就收不到了。#14樓 songcan 2009-10-16 10:31
代碼無法下載#15樓 heihoo 2010-11-26 20:47
急需這個代碼,謝謝。heihoo@163.com#16樓 qiuqingpo 2010-12-27 11:57
70 /// <summary>171 /// 開始監(jiān)聽線程的入口函數(shù)
172 /// </summary>
173 public void Listen()
174 {
175 while (true)
176 {
177 string[] keys = readWritePool.OnlineUID;
178 foreach (string uid in keys)
179 {
180 if (uid != null && readWritePool.busypool[uid].ReceiveSAEA.LastOperation != SocketAsyncOperation.Receive)
181 {
182 Boolean willRaiseEvent = (readWritePool.busypool[uid].ReceiveSAEA.UserToken as Socket).ReceiveAsync(readWritePool.busypool[uid].ReceiveSAEA);
183 if (!willRaiseEvent)
184 ProcessReceive(readWritePool.busypool[uid].ReceiveSAEA);
185 }
186 }
187 }
188 }
大哥.你這樣不讓我CPU 100% 才怪呢.真不知道你是怎么測試的?
#17樓 l_rain 2011-10-21 18:03
寫的很詳細,最新的代碼可以給我發(fā)一份嗎? email : l_rain@foxmail.樓上說的沒錯,CPU 40% 以上,內(nèi)存25M,這個效率太低了,不實用。內(nèi)存不是那樣算的