【黑马程序员】视频拓展——多线程聊天室客户端与客户端的交互

本文主要是介绍【黑马程序员】视频拓展——多线程聊天室客户端与客户端的交互,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

---------------------- 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培训、期待与您交流! ----------------------




这篇关于【黑马程序员】视频拓展——多线程聊天室客户端与客户端的交互的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1039874

相关文章

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2

Java多线程父线程向子线程传值问题及解决

《Java多线程父线程向子线程传值问题及解决》文章总结了5种解决父子之间数据传递困扰的解决方案,包括ThreadLocal+TaskDecorator、UserUtils、CustomTaskDeco... 目录1 背景2 ThreadLocal+TaskDecorator3 RequestContextH

Java如何获取视频文件的视频时长

《Java如何获取视频文件的视频时长》文章介绍了如何使用Java获取视频文件的视频时长,包括导入maven依赖和代码案例,同时,也讨论了在运行过程中遇到的SLF4J加载问题,并给出了解决方案... 目录Java获取视频文件的视频时长1、导入maven依赖2、代码案例3、SLF4J: Failed to lo

Python实现多路视频多窗口播放功能

《Python实现多路视频多窗口播放功能》这篇文章主要为大家详细介绍了Python实现多路视频多窗口播放功能的相关知识,文中的示例代码讲解详细,有需要的小伙伴可以跟随小编一起学习一下... 目录一、python实现多路视频播放功能二、代码实现三、打包代码实现总结一、python实现多路视频播放功能服务端开

Python实现视频转换为音频的方法详解

《Python实现视频转换为音频的方法详解》这篇文章主要为大家详细Python如何将视频转换为音频并将音频文件保存到特定文件夹下,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. python需求的任务2. Python代码的实现3. 代码修改的位置4. 运行结果5. 注意事项

使用Java实现获取客户端IP地址

《使用Java实现获取客户端IP地址》这篇文章主要为大家详细介绍了如何使用Java实现获取客户端IP地址,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 首先是获取 IP,直接上代码import org.springframework.web.context.request.Requ

C#多线程编程中导致死锁的常见陷阱和避免方法

《C#多线程编程中导致死锁的常见陷阱和避免方法》在C#多线程编程中,死锁(Deadlock)是一种常见的、令人头疼的错误,死锁通常发生在多个线程试图获取多个资源的锁时,导致相互等待对方释放资源,最终形... 目录引言1. 什么是死锁?死锁的典型条件:2. 导致死锁的常见原因2.1 锁的顺序问题错误示例:不同

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可

Python手搓邮件发送客户端

《Python手搓邮件发送客户端》这篇文章主要为大家详细介绍了如何使用Python手搓邮件发送客户端,支持发送邮件,附件,定时发送以及个性化邮件正文,感兴趣的可以了解下... 目录1. 简介2.主要功能2.1.邮件发送功能2.2.个性签名功能2.3.定时发送功能2. 4.附件管理2.5.配置加载功能2.6.

基于WinForm+Halcon实现图像缩放与交互功能

《基于WinForm+Halcon实现图像缩放与交互功能》本文主要讲述在WinForm中结合Halcon实现图像缩放、平移及实时显示灰度值等交互功能,包括初始化窗口的不同方式,以及通过特定事件添加相应... 目录前言初始化窗口添加图像缩放功能添加图像平移功能添加实时显示灰度值功能示例代码总结最后前言本文将