在上篇博客簡單理解socket寫完以後我就但願寫出一個websocket的服務器了,可是一路困難重重,仍是從基礎開始吧,先搞定C# socket編程基本知識,寫一個支持廣播的簡單server/client交互demo,而後再拓展爲websocket服務器。想要搞定這個須要一些基本知識javascript
進程與線程對CS的同窗來講確定耳聞能像了,再囉嗦兩句我我的的理解,每一個運行在系統上的程序都是一個進程,進程就是正在執行的程序,把編譯好的指令放入特定一塊內存,順序執行,這就是一個進程,咱們平時寫的if-else,for循環都按照咱們預期,一步步順序執行,這是由於咱們寫的是單線程的程序,所謂線程是一個進程的執行片斷,咱們寫的單線程程序,整個進程就一個主線程,全部代碼在這個線程內順序執行,但一個進程能夠有多個線程同時執行,這就是多線程程序,利用多線程支持咱們可讓程序一邊監聽客戶端請求,一邊廣播消息。html
熟悉web開發的同窗確定瞭解這個概念,在使用ajax中咱們就會用到異步的請求,同步與異步正好和咱們生活中的理解相反(我嘗試問過學管理的女友)java
同步:下一個調用在上一個調用返回結果後執行,也能夠理解爲事情必須一件作完再去作另外一件,咱們常常編寫的語句都是同步調用web
int a=dosomething(); a+=1;
a+=1; 這條指令必須在dosomething()方法執行完畢返回結果後才能夠執行,不然就亂了套ajax
異步:異步概念和同步相對,當一個異步過程調用發出後,調用者不能馬上獲得結果。實際處理這個調用的部件在完成後,經過狀態、通知和回調來通知調用者(百度上抄的)。理解了同步概念後異步也就不難理解了,以javascript的ajax爲例編程
ajax(arg1,arg2,function(){ //回調函數
a=3; });
a=4;
這個代碼段執行完成後通常狀況會把a賦值爲3而不是4,由於在ajax方法調用後,a=4;這條語句並無等待ajax()返回結果就執行了,也就是在ajax()執行完成調用回調函數以前,a=4;已經執行了,回調函數再把a賦值爲3使之成爲最後結果,爲此在ajax調用中咱們常常會使用回調函數,其實在不少異步處理中咱們都會使用到回調函數。服務器
瞭解了上面知識咱們就能夠按照下圖來寫咱們的服務器了websocket
關於怎麼具體一步步使用socket我就不說了,有興趣同窗能夠看看你得學會而且學得會的Socket編程基礎知識,看看咱們服務器的結構,我寫了一個TcpHelper類來處理服務器操做多線程
首先定義 一個ClientInfo類存放Client信息異步
public class ClientInfo { public byte[] buffer; public string NickName { get; set; } public EndPoint Id { get; set; } public IntPtr handle { get; set; } public string Name { get { if (!string.IsNullOrEmpty(NickName)) { return NickName; } else { return string.Format("{0}#{1}", Id, handle); } } } }
而後是一個SocketMessage類,記錄客戶端發來的消息
public class SocketMessage { public bool isLogin { get; set; } public ClientInfo Client { get; set; } public string Message { get; set; } public DateTime Time { get; set; } }
而後定義兩個全局變量記錄全部客戶端及全部客戶端發來的消息
private Dictionary<Socket, ClientInfo> clientPool = new Dictionary<Socket, ClientInfo>(); private List<SocketMessage> msgPool = new List<SocketMessage>();
而後就是幾個主要方法的定義
/// <summary> /// 啓動服務器,監聽客戶端請求 /// </summary> /// <param name="port">服務器端進程口號</param> public void Run(int port); /// <summary> /// 在獨立線程中不停地向全部客戶端廣播消息 /// </summary> private void Broadcast(); /// <summary> /// 把客戶端消息打包處理(拼接上誰何時發的什麼消息) /// </summary> /// <returns>The message.</returns> /// <param name="sm">Sm.</param> private byte[] PackageMessage(SocketMessage sm); /// <summary> /// 處理客戶端鏈接請求,成功後把客戶端加入到clientPool /// </summary> /// <param name="result">Result.</param> private void Accept(IAsyncResult result); /// <summary> /// 處理客戶端發送的消息,接收成功後加入到msgPool,等待廣播 /// </summary> /// <param name="result">Result.</param> private void Recieve(IAsyncResult result);
逐個分析一下把
這是該類惟一提供的共有方法,供外界調用,來根據port參數建立一個socket
public void Run(int port) { Thread serverSocketThraed = new Thread(() => { Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); server.Bind(new IPEndPoint(IPAddress.Any, port)); server.Listen(10); server.BeginAccept(new AsyncCallback(Accept), server); }); serverSocketThraed.Start(); Console.WriteLine("Server is ready"); Broadcast(); }
代碼很簡單,須要注意的有幾點
1.在一個新線程中建立服務器socket,最多容許10個客戶端鏈接。
2.在方法最後調用Broadcast()方法用於向全部客戶端廣播消息
3.BeginAccept方法,MSDN上有權威解釋,可是以爲不夠接地氣,簡單說一下個人理解,首先這個方法是異步的,用於服務器接受一個客戶端的鏈接,第一個參數其實是回調函數,在C#中使用委託,在回調函數中經過調用EndAccept就能夠得到嘗試鏈接的客戶端socket,第二個參數是包含請求state的對象,傳入server socket對象自己就能夠了
方法用於處理客戶端鏈接請求
private void Accept(IAsyncResult result) { Socket server = result.AsyncState as Socket; Socket client = server.EndAccept(result); try { //處理下一個客戶端鏈接 server.BeginAccept(new AsyncCallback(Accept), server); byte[] buffer = new byte[1024]; //接收客戶端消息 client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), client); ClientInfo info = new ClientInfo(); info.Id = client.RemoteEndPoint; info.handle = client.Handle; info.buffer = buffer; //把客戶端存入clientPool this.clientPool.Add(client, info); Console.WriteLine(string.Format("Client {0} connected", client.RemoteEndPoint)); } catch (Exception ex) { Console.WriteLine("Error :\r\n\t" + ex.ToString()); } }
BeginRecieve方法的MSDN有解釋,和Accept同樣也是異步處理,接收客戶端消息,放入第一個參數中,它也傳入了一個回調函數的委託,和帶有socket state的對象,用於處理下一次接收。咱們把接收成功地客戶端socket及其對應信息存放到clientPool中
方法用於接收客戶端消息,並把全部消息及其發送者信息存入msgInfo,等待廣播
private void Recieve(IAsyncResult result) { Socket client = result.AsyncState as Socket; if (client == null || !clientPool.ContainsKey(client)) { return; } try { int length = client.EndReceive(result); byte[] buffer = clientPool[client].buffer; //接收消息 client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), client); string msg = Encoding.UTF8.GetString(buffer, 0, length); SocketMessage sm = new SocketMessage(); sm.Client = clientPool[client]; sm.Time = DateTime.Now; Regex reg = new Regex(@"{<(.*?)>}"); Match m = reg.Match(msg); if (m.Value != "") //處理客戶端傳來的用戶名 { clientPool[client].NickName = Regex.Replace(m.Value, @"{<(.*?)>}", "$1"); sm.isLogin = true; sm.Message = "login!"; Console.WriteLine("{0} login @ {1}", client.RemoteEndPoint,DateTime.Now); } else //處理客戶端傳來的普通消息 { sm.isLogin = false; sm.Message = msg; Console.WriteLine("{0} @ {1}\r\n {2}", client.RemoteEndPoint,DateTime.Now,msg); } msgPool.Add(sm); } catch { //把客戶端標記爲關閉,並在clientPool中清除 client.Disconnect(true); Console.WriteLine("Client {0} disconnet", clientPool[client].Name); clientPool.Remove(client); } }
這個的代碼都很簡單,就很少解釋了,我加入了用戶名處理用於廣播客戶端消息的時候顯示客戶端自定義的暱稱而不是生硬的ip地址+端口號,固然這裏須要客戶端配合
服務器已經和客戶端鏈接成功,而且接收到了客戶端消息,咱們就能夠看看該怎麼廣播消息了,Broadcast()方法已經在run()方法內調用,看看它是怎麼運做廣播客戶端消息的
private void Broadcast() { Thread broadcast = new Thread(() => { while (true) { if (msgPool.Count > 0) { byte[] msg = PackageMessage(msgPool[0]); foreach (KeyValuePair<Socket, ClientInfo> cs in clientPool) { Socket client = cs.Key; if (client.Connected) { client.Send(msg, msg.Length, SocketFlags.None); } } msgPool.RemoveAt(0); } } }); broadcast.Start(); }
Broadcast()方法啓用了一個新線程,循環檢測msgPool是否爲空,當不爲空的時候遍歷全部客戶端,調用send方法發送msgPool裏面的第一條消息,而後清除該消息繼續檢測,直到消息廣播完,其實這就是一個閹割版的觀察者模式 ,順便看一下打包數據方法
private byte[] PackageMessage(SocketMessage sm) { StringBuilder packagedMsg = new StringBuilder(); if (!sm.isLogin) //消息是login信息 { packagedMsg.AppendFormat("{0} @ {1}:\r\n ", sm.Client.Name, sm.Time.ToShortTimeString()); packagedMsg.Append(sm.Message); } else //處理普通消息 { packagedMsg.AppendFormat("{0} login @ {1}", sm.Client.Name, sm.Time.ToShortTimeString()); } return Encoding.UTF8.GetBytes(packagedMsg.ToString()); }
static void Main(string[] args) { TcpHelper helper = new TcpHelper(); helper.Run(8080); }
這樣咱們就啓用了server,看看簡單的客戶端實現,原理相似,再也不分析了
1 class Program 2 { 3 private static byte[] buf = new byte[1024]; 4 static void Main(string[] args) 5 { 6 Console.Write("Enter your name: "); 7 string name = Console.ReadLine(); 8 Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 9 client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080)); 10 Console.WriteLine("Connected to server, enter $q to quit"); 11 name = "{<" + name.Trim() + ">}"; 12 byte[] nameBuf = Encoding.UTF8.GetBytes(name); 13 client.BeginSend(nameBuf, 0, nameBuf.Length, SocketFlags.None, null, null); 14 client.BeginReceive(buf, 0, buf.Length, SocketFlags.None, new AsyncCallback(Recieve), client); 15 while (true) 16 { 17 string msg = Console.ReadLine(); 18 if (msg == "$q") 19 { 20 client.Close(); 21 break; 22 } 23 byte[] output = Encoding.UTF8.GetBytes(msg); 24 client.BeginSend(output, 0, output.Length, SocketFlags.None, null, null); 25 } 26 Console.Write("Disconnected. Press any key to exit... "); 27 Console.ReadKey(); 28 } 29 30 private static void Recieve(IAsyncResult result) 31 { 32 try 33 { 34 Socket client = result.AsyncState as Socket; 35 int length = client.EndReceive(result); 36 string msg = Encoding.UTF8.GetString(buf, 0, length); 37 Console.WriteLine(msg); 38 client.BeginReceive(buf, 0, buf.Length, SocketFlags.None, new AsyncCallback(Recieve), client); 39 } 40 catch 41 { 42 } 43 } 44 }
這樣一個簡單的支持廣播地socket就完成了,咱們能夠進行多個客戶端聊天了,看看運行效果吧
其實socket編程沒有一開始我想象的那麼難,重要的仍是搞明白原理,接下來事情就迎刃而解了,這個簡單的server還有很多待完善之處,主要是展現一下C# socket編程基本使用,爲下一步作websocket server作準備,實習二者很類似,只是websocket server 添加了協議處理部分,這兩天會盡快分享出來
感興趣的同窗能夠看看源碼 (註釋是我寫博客的時候加上的,源碼中沒有,無論看過博客的人應該沒問題)