本文主要是介绍【黑马程序员】视频拓展——多线程聊天室客户端与客户端的交互,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
---------------------- ASP.Net+Android+IOS开发、.Net培训、期待与您交流! ----------------------
Socket不仅可以实现服务端与客户端的交互,而且还可以实现客户端与客户端的交互,就是从一个客户端发送消息,然后在另一个客户端接收,就好比我们的聊天软件。实现客户端与客户端的交互有两种方法可以实现。第一种方法是我们可以把客户端看成与另外一个服务端(除主服务端之外的),当一个客户端成功连接主服务端之后,我们让这个客户端也处于监听状态,监听来自其它客户端的连接。并且还有最重要的一步是从主服务端获取在线客户端的IP端口,这样才能通过IP端口连接其它客户端。其过程如下图所示:
本文不是用第一种方法实现的,而是是用第二种方法实现。第二种方法需要通过服务端转发数据的方式来进行。我们可以向服务端发送数据和文件,也可以通过服务端的转发向其它客户端发送数据和文件,那服务端是如何识别客户端发送来的数据或文件是要转发的还是发给自己的呢?这就需要我们在发送的数据中做个标识,就像标识数据是文字还是文件一样,当接收的字节数组(byte[])的第二个位置(第一个位置已被占用,即标识数据是文字还是文件)的值为0时,我们视为服务端你客户端发送的数据;只要这个位置的值为其它值,我们就可以视为是服务端转发的数据。如何识别数据是否转发的问题已解决,那假如有多个客户端,服务器又如何识别这条数据具体是发往哪个客户端的呢?同样,我们仍然需要用标记。我们可以建立一个专门用数字标记IP端口的字典集合IPSign,当服务端监听到有客户端连接成功之后,会返回一个和这个客户端进行通信的Socket,我们可以给这个Socket的远程IP端口一个int类型的特殊标记,并把它作为key值与IP端口一起添加到字典集合中。然后在客户端,如果我们要向其它某个客户端的发送数据,就必须先从IPSign字典集合中获取与该客户端的IP端口相对应的key值,并把它赋给传输数据的字节数组的第二个位置。当服务端接收到这组字节数组之后,就根据该数组第二个位置的值(也就是IPSign字典集合的key值)来从IPSign字典集合中获取对应的IP端口,然后把这个IP端口作为key值从存储所有通信连接的connSockets中获取与目标IP对应的Socket,最后服务端就用这个Socket把数据发送出去。
如下代码为服务端的主要代码:
int index = 0; //当前在线的客户端数量//存储并标记在线客户端的IP端口字符串Dictionary<int, string> IPSign = new Dictionary<int, string>();//专门用于存储//服务端负责与客户端通讯的Socket的集合Dictionary<string, Socket> connSockets = new Dictionary<string, Socket>();/// <summary>/// 监听客户端连接事件/// </summary>private void WatchConnection(){while (true) //使用一个死循环持续不断的监听新的客户端的连接请求{//开始监听客户端的连接请求Socket connSocket = listenSocket.Accept();//给连接成功的客户端添加一个标记IPSign.Add(++index, connSocket.RemoteEndPoint.ToString());//把连接成功的客户端的IP回馈给其它在线的客户端,并把其它在线的//客户端的IP也回馈给刚刚上线的客户端FeedbackOnline(connSocket); //向ListBox中添加一个IP端口字符串,作为访问该客户端的唯一标志lbUniqueSign.Items.Add(connSocket.RemoteEndPoint.ToString());//将与客户端通讯的Socket添加到集合中connSockets.Add(connSocket.RemoteEndPoint.ToString(), connSocket);Thread thread = new Thread(ReciveMessage);thread.IsBackground = true;//以IP端口字符串为Key值,把接收消息的线程添加到recThread集合中。recThreads.Add(connSocket.RemoteEndPoint.ToString(), thread);thread.Start(connSocket);ShowMsg("客户端连接成功!" + connSocket.RemoteEndPoint.ToString());}}/// <summary>/// 循环接收客户端发送过来的数据/// </summary>/// <param name="socketParam">当前与客户端通信的Socket</param>private void ReciveMessage(object socketParam){//把参数由object类型转换为Socket类型Socket socketClient = socketParam as Socket;while (true){//声明一个2M空间的字节数组byte[] arrRecMsg = new byte[1024 * 1024 * 2];//把接收到的字节存入字节数组中,并获取接收到的字节数int length = socketClient.Receive(arrRecMsg);//先取出字节数组中的值进行判断if (arrRecMsg[0] == 0) //当前数据是文字{string message = Encoding.UTF8.GetString(arrRecMsg, 2, length-2);//如果字节数组的第二个位置的值为0,说明是发送给服务端的信息if (arrRecMsg[1] == 0){//按照接收到的实际字节数获取发送过来的消息ShowMsg(socketClient.RemoteEndPoint.ToString() + ":\t" + message);}else //不为0的话就说明是发送给客户端的信息{string destIP = IPSign[arrRecMsg[1]]; //获取目标IP端口Socket destSocket = null; //与目标IP端口对应的Socketforeach (var item in connSockets){if (item.Key == destIP){destSocket = item.Value;break;}}if (destSocket == null){MessageBox.Show("当前客户端不在线");return;}//调用转发方法,传入连接Socket和信息,以及发送消息的客户端IPTranspondMsg(destSocket, message, socketClient.RemoteEndPoint.ToString());}}else if (arrRecMsg[0] == 1) //当前数据传送给服务器的文件 {SendFiles(arrRecMsg, length);}}}/// <summary>/// 转发消息/// </summary>/// <param name="socket">与目标客户端相连接的Socket</param>/// <param name="message">转发的消息</param>/// <param name="ip">目标客户端的IP端口</param>private void TranspondMsg(Socket socket, string message,string ip){//用于存储转发信息所转换成的字节,不能超过2Mbyte[] currIP = new byte[1024 * 1024 * 2]; //第一个位置为0表示传输的是文字currIP[0] = 0; //第二个位置为IP端口在字典集合中的标记currIP[1] = (byte)GetSignInIPStr(ip);//获取实际消息的字节长度int length = Encoding.UTF8.GetBytes(message, 0, message.Length, currIP, 2);//发送消息socket.Send(currIP, 0, length + 2, SocketFlags.None);}/// <summary>/// 当服务端监听到有客户端上线时,会给其它在线的客户端一个回馈信息/// </summary>/// <param name="currSocket">当前上线的客户端</param>private void FeedbackOnline(Socket currSocket){if (connSockets.Count <= 0){return;}//获取当前上线客户端的IP端口string onlineIP = currSocket.RemoteEndPoint.ToString();//先向其它客户端发送消息,告知它们当前客户端上线了,并把当前客户端的IP发给它们foreach (Socket item in connSockets.Values){SendIPMsg(item, onlineIP);}//然后再获取除当前上线的客户端之外的已经在线的客户端的IP端口,并发送给当前上线的客户端 foreach (Socket item in connSockets.Values){if (!onlineIP.Equals(item.RemoteEndPoint.ToString())) //除当前上线客户端之外{//这里要把当前线程挂起一个很短的时间,让程序能够有时间将前面的信息发送出去,//不然的话,会造成多个消息合在一起发送的情况Thread.Sleep(300);SendIPMsg(currSocket, item.RemoteEndPoint.ToString());}}}/// <summary>/// 向指定的客户端发送指定的消息/// </summary>/// <param name="socket">与消息目标客户端相连接的Socket</param>/// <param name="onlineIP">要发送的具体消息,也就是在线客户端的IP端口</param>private void SendIPMsg(Socket socket, string onlineIP){byte[] currIP = new byte[1024 * 1024 * 1];currIP[0] = 2; //字节数组的第一个位置为2表示发送的是当前上线的IP端口currIP[1] = (byte)GetSignInIPStr(onlineIP); //当前上线的IP端口的标记int length = Encoding.UTF8.GetBytes(onlineIP, 0, onlineIP.Length, currIP, 2);socket.Send(currIP, 0, length + 2, SocketFlags.None);}/// <summary>/// 获取给定IP端口的标记/// </summary>/// <param name="ip">给定的IP</param>/// <returns>标记Key值</returns>private int GetSignInIPStr(string ip){int index = -1;foreach (var item in IPSign){if (item.Value == ip){index = item.Key;break;}}return index;}
在以上代码的FeedbackOnline()方法中,有一行代码Thread.Sleep(300),它的意思是让当前处理这段代码的线程挂起300毫秒。这样做是为了让反馈的IP端口能够一条一条的发送出去,可能由于代码执行得太快,如果不让线程挂起一段时间,那程序就会把两个IP端口放在同一个字节数组里面发送出去,这是不允许的。
当在客户端接收到信息后,我们也要进行判定,主要代码如下://存储并标记在线客户端的IP端口字符串Dictionary<int, string> IPSign = new Dictionary<int, string>();/// <summary>/// 循环接收服务端发送过来的数据/// </summary>private void ReciveMessage(){while (true){//声明一个2M空间的字节数组byte[] arrRecMsg = new byte[1024 * 1024 * 2];//把接收到的字节存入字节数组中,并获取接收到的字节长度int length = socketClient.Receive(arrRecMsg);if (arrRecMsg[0] == 0){//按照接收到的实际字节数获取发送过来的消息ShowMsg(IPSign[arrRecMsg[1]] + ":\t" + Encoding.UTF8.GetString(arrRecMsg, 2, length - 2));//ShowMsg("\t" + Encoding.UTF8.GetString(arrRecMsg, 2, length - 2));}else if (arrRecMsg[0] == 1){//接收文件 }else{string transIP = Encoding.UTF8.GetString(arrRecMsg, 2, length - 2); IPSign.Add(arrRecMsg[1], transIP);lbOnlineClient.Items.Add(transIP); }}}/// <summary>/// 发送消息按钮/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void btnSendMsg_Click(object sender, EventArgs e){byte[] arrSendMsg = new byte[1024 * 1024 * 2];string msg = txtMessage.Text.Trim();int length = Encoding.UTF8.GetBytes(msg, 0, msg.Length, arrSendMsg, 1);if (lbOnlineClient.SelectedItem == null){arrSendMsg[0] = 0; //表示当前发送的是文字arrSendMsg[1] = 0; //表示当前是发送给服务端的数据socketClient.Send(arrSendMsg, 0, length + 1, SocketFlags.None);ShowMsg("我说:\t" + txtMessage.Text.Trim());}else{arrSendMsg[0] = 0; //表示当前发送的是文字//表示当前是发送给客户端的数据arrSendMsg[1] = (byte)GetSignInIPStr(lbOnlineClient.SelectedItem.ToString());socketClient.Send(arrSendMsg, 0, length + 2, SocketFlags.None);ShowMsg("我说:\t" + txtMessage.Text.Trim()); }}/// <summary>/// 获取给定IP端口的标记/// </summary>/// <param name="ip">给定的IP</param>/// <returns>标记Key值</returns>private int GetSignInIPStr(string ip){int index = -1;foreach (var item in IPSign){if (item.Value == ip){index = item.Key;break;}}return index;}
上述代码只是简单地实现的多线程聊天室的功能,没有添加异常处理方法,也有很多地方出现代码的冗余。比如说,标记IP端口的字典集合IPSign,我们可以把它单独放在一个类里面,这样就不用在服务端和客户端都进行赋值和取值的操作。程序运行结果如图所示:
---------------------- ASP.Net+Android+IOS开发、.Net培训、期待与您交流! ----------------------
这篇关于【黑马程序员】视频拓展——多线程聊天室客户端与客户端的交互的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!