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,理论上有网就能够聊天),赶快试一下吧!!!
这个系列未完,待续。。。。。。。。。。。。。。。。。。。。。,期待您的关注