1.前言html
這是本系列的第二篇文章,第一篇文章獲得了不少朋友們的支持,在這裏表示很是的感謝。對於這一系列文章須要補充的是這只是一篇入門級別的Socket通訊文章,對於專業人員來講徹底能夠跳過。本文只介紹一些基本TCP通訊技術並使用該技術實現聊天功能。本篇文章實現聊天服務器搭建,我會把聊天服務器部署到廣域網服務器上,到時候你們就能夠能夠在源碼裏面打開客戶端與我聊天啦!(這只是一個初級版功能簡單不支持離線消息,因此聊天的前提是我在線(用戶名爲張三的就是我,Q我吧)……),也能夠本身打開兩個客戶端測試一下(除張三之外帳戶)。數據庫
2.本篇實現功能數組
1. 聊天室服務器端的建立。服務器
2. 聊天室客戶端的建立。socket
3. 實現客戶與服務器的鏈接通信。ide
4. 實現客戶之間的私聊。測試
3.具體實現ui
(1)客戶端搭建this
1)運行過程 與服務端創建鏈接—>首次鏈接向服務器發送登陸用戶信息(格式例如 張三| )—>聊天:先將聊天消息發送到服務器,而後由服務器解析發給好友(發往服務器的消息以下 張三|李四|你好呀李四?),如圖spa
客戶端代碼實現:
1 //客戶端通訊套接字 2 private Socket clientSocket; 3 //新線程 4 private Thread thread; 5 //當前登陸的用戶 6 private string userName = ""; 7 public Client() 8 { 9 InitializeComponent(); 10 //防止新線程調用主線程卡死 11 CheckForIllegalCrossThreadCalls = false; 12 } 13 14 //經過IP地址與端口號與服務端創建連接 15 private void btnToServer_Click(object sender, EventArgs e) 16 { 17 //鏈接服務器前先選擇用戶 18 if (cmbUser.SelectedItem == null) 19 { 20 MessageBox.Show("請選擇登陸用戶"); 21 return; 22 } 23 userName = cmbUser.SelectedItem.ToString(); 24 this.Text = "當前用戶:" + userName; 25 //登陸後禁止切換用戶 26 cmbUser.Enabled = false; 27 //加載好友列表 28 foreach (object item in cmbUser.Items) 29 { 30 if (item != cmbUser.SelectedItem) 31 { 32 lbFriends.Items.Add(item); 33 } 34 } 35 //新建通訊套接字 36 clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 37 //這裏的ip地址,端口號都是服務端綁定的相關數據。 38 IPAddress ip = IPAddress.Parse(txtIP.Text); 39 var endpoint = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text)); 40 try 41 { 42 clientSocket.Connect(endpoint); //連接有端口號與IP地址肯定服務端. 43 //登陸時給服務器發送登陸消息例如張三| 44 string str = userName + "|" + " "; 45 byte[] buffer = Encoding.UTF8.GetBytes(str); 46 clientSocket.Send(buffer); 47 } 48 catch 49 { 50 MessageBox.Show("與服務器鏈接失敗"); 51 lbFriends.Items.Clear(); 52 } 53 //客戶端在接受服務端發送過來的數據是經過Socket 中的Receive方法,該方法會阻斷線程,因此咱們本身爲該方法建立了一個線程 54 thread = new Thread(ReceMsg); 55 thread.IsBackground = true; //設置後臺線程 56 thread.Start(); 57 } 58 59 public void ReceMsg() 60 { 61 while (true) 62 { 63 64 try 65 { 66 var buffer = new byte[1024 * 1024 * 2]; 67 int dateLength = clientSocket.Receive(buffer); //接收服務端發送過來的數據 68 //把接收到的字節數組轉成字符串顯示在文本框中。 69 string ReceiveMsg = Encoding.UTF8.GetString(buffer, 0, dateLength); 70 string[] msgTxt = ReceiveMsg.Split('|'); 71 string newstr =" "+msgTxt[0] +":我"+ "\r\n"+" " + msgTxt[2] + " ____[" + DateTime.Now +"]" + "\r\n" + "\r\n"; 72 ShowSmsg(newstr); 73 } 74 catch 75 { 76 77 } 78 } 79 } 80 81 private void btnSend_Click(object sender, EventArgs e) 82 { 83 if (lbFriends.SelectedItems.Count != 1) 84 { 85 MessageBox.Show("請選擇好友"); 86 return; 87 } 88 string friend = lbFriends.SelectedItem.ToString(); 89 try 90 { 91 //界面顯示消息 92 string newstr = "我" + ":" + friend + "\r\n" + txtMsg.Text.Trim() + " ____[" + DateTime.Now + 93 "]" + "\r\n" + "\r\n"; 94 ShowSmsg(newstr); 95 //發往服務器的消息 格式爲 (發送者|接收者|信息) 96 string str = userName + "|" + friend + "|" + txtMsg.Text.Trim(); 97 //將消息轉化爲字節數據傳輸 98 byte[] buffer = Encoding.UTF8.GetBytes(str); 99 clientSocket.Send(buffer); 100 txtMsg.Text = ""; 101 } 102 catch 103 { 104 MessageBox.Show("與服務器鏈接失敗"); 105 } 106 } 107 //展現消息 108 private void ShowSmsg(string newStr) 109 { 110 txtChat.AppendText(newStr); 111 } 112 private void btnCloseSer_Click(object sender, EventArgs e) 113 { 114 clientSocket.Close(); 115 }
(2)服務器端搭建
咱們上篇講到聊天服務器與單個客戶端實現通訊,服務器通訊的socket搭建後,開啓新的線程來監聽是否有客戶端連入,爲了實現後期的客戶端對客戶端的通訊咱們首先要存儲客戶端的socket的IP與端口號,以及用戶名信息,服務器接收到消息後將消息解析轉發。我實現的思路以下:
(0)服務器頁面搭建,以下圖
服務器代碼:
1 //客戶端通訊套接字 2 private Socket clientSocket; 3 //新線程 4 private Thread thread; 5 //當前登陸的用戶 6 private string userName = ""; 7 public Client() 8 { 9 InitializeComponent(); 10 //防止新線程調用主線程卡死 11 CheckForIllegalCrossThreadCalls = false; 12 } 13 14 //經過IP地址與端口號與服務端創建連接 15 private void btnToServer_Click(object sender, EventArgs e) 16 { 17 //鏈接服務器前先選擇用戶 18 if (cmbUser.SelectedItem == null) 19 { 20 MessageBox.Show("請選擇登陸用戶"); 21 return; 22 } 23 userName = cmbUser.SelectedItem.ToString(); 24 this.Text = "當前用戶:" + userName; 25 //登陸後禁止切換用戶 26 cmbUser.Enabled = false; 27 //加載好友列表 28 foreach (object item in cmbUser.Items) 29 { 30 if (item != cmbUser.SelectedItem) 31 { 32 lbFriends.Items.Add(item); 33 } 34 } 35 //新建通訊套接字 36 clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 37 //這裏的ip地址,端口號都是服務端綁定的相關數據。 38 IPAddress ip = IPAddress.Parse(txtIP.Text); 39 var endpoint = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text)); 40 try 41 { 42 clientSocket.Connect(endpoint); //連接有端口號與IP地址肯定服務端. 43 //登陸時給服務器發送登陸消息例如張三| 44 string str = userName + "|" + " "; 45 byte[] buffer = Encoding.UTF8.GetBytes(str); 46 clientSocket.Send(buffer); 47 } 48 catch 49 { 50 MessageBox.Show("與服務器鏈接失敗"); 51 lbFriends.Items.Clear(); 52 } 53 //客戶端在接受服務端發送過來的數據是經過Socket 中的Receive方法,該方法會阻斷線程,因此咱們本身爲該方法建立了一個線程 54 thread = new Thread(ReceMsg); 55 thread.IsBackground = true; //設置後臺線程 56 thread.Start(); 57 } 58 59 public void ReceMsg() 60 { 61 while (true) 62 { 63 64 try 65 { 66 var buffer = new byte[1024 * 1024 * 2]; 67 int dateLength = clientSocket.Receive(buffer); //接收服務端發送過來的數據 68 //把接收到的字節數組轉成字符串顯示在文本框中。 69 string ReceiveMsg = Encoding.UTF8.GetString(buffer, 0, dateLength); 70 string[] msgTxt = ReceiveMsg.Split('|'); 71 string newstr =" "+msgTxt[0] +":我"+ "\r\n"+" " + msgTxt[2] + " ____[" + DateTime.Now +"]" + "\r\n" + "\r\n"; 72 ShowSmsg(newstr); 73 } 74 catch 75 { 76 77 } 78 } 79 } 80 81 private void btnSend_Click(object sender, EventArgs e) 82 { 83 if (lbFriends.SelectedItems.Count != 1) 84 { 85 MessageBox.Show("請選擇好友"); 86 return; 87 } 88 string friend = lbFriends.SelectedItem.ToString(); 89 try 90 { 91 //界面顯示消息 92 string newstr = "我" + ":" + friend + "\r\n" + txtMsg.Text.Trim() + " ____[" + DateTime.Now + 93 "]" + "\r\n" + "\r\n"; 94 ShowSmsg(newstr); 95 //發往服務器的消息 格式爲 (發送者|接收者|信息) 96 string str = userName + "|" + friend + "|" + txtMsg.Text.Trim(); 97 //將消息轉化爲字節數據傳輸 98 byte[] buffer = Encoding.UTF8.GetBytes(str); 99 clientSocket.Send(buffer); 100 txtMsg.Text = ""; 101 } 102 catch 103 { 104 MessageBox.Show("與服務器鏈接失敗"); 105 } 106 } 107 //展現消息 108 private void ShowSmsg(string newStr) 109 { 110 txtChat.AppendText(newStr); 111 } 112 private void btnCloseSer_Click(object sender, EventArgs e) 113 { 114 clientSocket.Close(); 115 }
(1)當兩個不一樣客戶端的連入,生成兩個通訊套接字1,2。這時爲了與客戶端實現通訊咱們有必要創建一個客戶端管理類,來存儲客戶端的信息。
(2)用戶名與客戶端通訊的socket的IP與端口號對應,以Dictionary字典形式存入鍵:IP與端口號 ,值:用戶名(這裏爲演示原理因此沒加入數據庫,只是模擬,下一章再加入數據庫);當用戶第一次連入,咱們必須記錄他的IP並與用戶對應起來,若是局域網聊天IP在同一網段兩個客戶端還能夠互相找到, 若是廣域網下兩個客戶端只有經過服務器轉接才能找到。
(3)聲明一個全局消息委託 public delegate void DGSendMsg(string strMsg);
個人思路以下圖:
下面是我寫的客戶端管理類:
1 public class ClientManager 2 { 3 //好友列表控件 4 private System.Windows.Forms.ListBox listClient; 5 //在服務器上顯示消息的委託(全局) 6 DGSendMsg dgSendMsg; 7 //通訊套接字key :客戶端ip value :對應的通訊套接字 8 private Dictionary<string, Socket> ClientSocket; 9 // 通訊套接字key :客戶端ip value :用戶名(因爲沒有添加數據庫咱們暫時這麼模擬,下篇我會講到) 10 private Dictionary<string, string> UserSocket; 11 public ClientManager() 12 { } 13 /// <summary> 14 /// 客戶端管理類 15 /// </summary> 16 /// <param name="lb">列表控件</param> 17 /// <param name="dgSendMsg">顯示消息</param> 18 public ClientManager(System.Windows.Forms.ListBox lb, DGSendMsg dgSendMsg) 19 { 20 //用戶列表 21 this.listClient = lb; 22 //消息委託 23 this.dgSendMsg = dgSendMsg; 24 //通訊字典 25 ClientSocket = new Dictionary<string, Socket>(); 26 //用戶字典 27 UserSocket = new Dictionary<string, string>(); 28 } 29 #region 添加客戶端通訊套接字+ public void AddClient(Socket sokMsg) 30 /// <summary> 31 /// 添加通訊套接字 32 /// </summary> 33 /// <param name="sokMag">負責通訊的socket</param> 34 public void AddClient(Socket sokMsg) 35 { 36 //獲取客戶端通訊套接字 37 string strEndPoint = sokMsg.RemoteEndPoint.ToString(); 38 //通訊套接字加入字典 39 ClientSocket.Add(strEndPoint, sokMsg); 40 //sokServer.Accept()這個接收消息的方法會使線程卡死,因此要開啓新線程 41 Thread thrMag = new Thread(ReciveMsg); 42 //設置爲後臺線程防止卡死 43 thrMag.IsBackground = true; 44 //開啓線程 爲新線程傳入通訊套接字參數 45 thrMag.Start(sokMsg); 46 dgSendMsg(strEndPoint + "成功鏈接上服務端~~~!" + DateTime.Now.ToString()); 47 } 48 #endregion 49 bool isReceing = true; 50 #region void ReciveMsg(object sokMsgObj) 服務接收客戶端消息 51 /// <summary> 52 /// 服務接收客戶端消息 53 /// </summary> 54 /// <param name="sokMsgObj">客戶端Scoket</param> 55 void ReciveMsg(object sokMsgObj) 56 { 57 Socket sokMsg = null; 58 try 59 { 60 //sokMsg接收消息的socket 61 sokMsg = sokMsgObj as Socket; 62 //建立接收消息的緩衝區 默認5M 63 byte[] arrMsg = new byte[5 * 1024 * 1024]; 64 //循環接收 65 while (isReceing) 66 { 67 //接收到的真實消息存入緩衝區 並保存消息的真實長度(由於5M緩衝區不會所有用掉) 68 int realLength = sokMsg.Receive(arrMsg); 69 //將緩衝區的真實數據轉成字符串 70 string strMsg = Encoding.UTF8.GetString(arrMsg, 0, realLength); 71 //dgSendMsg(strMsg); 72 73 string[] msgTxt = strMsg.Split('|'); 74 // msgTxt.Length == 2說明用戶第一次連入,咱們必須記錄他的IP並與用戶對應起來,若是局域網聊天 75 //IP在同一網段兩個客戶端還能夠互相找到, 若是廣域網下只有經過服務器轉接才能找到 76 if (msgTxt.Length == 2) 77 { 78 //若是用戶名已登陸則強制下線 79 if (UserSocket.Values.Contains(msgTxt[0])) 80 { 81 sokMsg.Close(); 82 return; 83 } 84 UserSocket.Add(sokMsg.RemoteEndPoint.ToString(), msgTxt[0]); 85 //顯示列表 86 listClient.Items.Add(sokMsg.RemoteEndPoint + "---" + msgTxt [0]+ @"---上線~\(≧▽≦)/~啦啦啦"); 87 continue; 88 } 89 90 //發送信息給客戶端 91 SendMsgToClient(strMsg); 92 } 93 } 94 catch 95 { 96 //鏈接出錯說明客戶端鏈接斷開 97 RemoveClient(sokMsg.RemoteEndPoint.ToString()); 98 } 99 } 100 #endregion 101 /// <summary> 102 /// 移除下線用戶信息 103 /// </summary> 104 /// <param name="strClientID">IP:端口</param> 105 public void RemoveClient(string strClientID) 106 { 107 //要移除的用戶名 108 string name = UserSocket[strClientID]; 109 // 要移除的在線管理的項 110 string onlineStr = strClientID + "---" + UserSocket[strClientID] + @"---上線~\(≧▽≦)/~啦啦啦"; 111 112 //移除在線管理listClient 集合 113 if (listClient.Items.Contains(onlineStr)) 114 { 115 listClient.Items.Remove(onlineStr); 116 } 117 //斷開socket鏈接 118 if (ClientSocket.Keys.Contains(strClientID)) 119 { 120 ClientSocket[strClientID].Close(); 121 ClientSocket.Remove(strClientID); 122 UserSocket.Remove(strClientID); 123 } 124 dgSendMsg(strClientID + "---" + name + @"---下線_"+DateTime.Now); 125 } 126 public void SendMsgToClient(string Msg) 127 { 128 //解析發來的數據 129 130 string[] msgTxt = Msg.Split('|'); 131 132 //不可解析數據 133 if (msgTxt.Length < 2) 134 { 135 return; 136 } 137 // 解析收消息的用戶名 138 string strTo = msgTxt[1]; 139 //得到當前發送的用戶 140 var nowtouser = UserSocket.Where(w => w.Value == strTo).FirstOrDefault(); 141 if (nowtouser.Key != null) 142 { 143 byte[] buffer = System.Text.Encoding.UTF8.GetBytes(Msg); 144 Socket conn = ClientSocket[nowtouser.Key]; 145 conn.Send(buffer); 146 } 147 148 } 149 }
(4)總結
本次實現了客戶端對客戶端的一對一聊天(本篇不涉及數據庫),實現思路大致爲:客戶端1將消息發給服務器,服務器解析消息把消息發給客戶端2。下一篇咱們講自定義協議發送文件,窗口抖動,以及各類文件格式的接收的解決思路。最後你能夠打開源碼的客戶端,登陸張三之外的客戶端給我發消息,我這邊登陸的是張三的帳戶,或者打開兩個客戶端本身聊天(不須要運行服務端,默認是個人服務器IP,理論上有網就能夠聊天),趕快試一下吧!!!
這個系列未完,待續。。。。。。。。。。。。。。。。。。。。。,期待您的關注