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

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

---------------------- 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实现websocket服务端及客户端的详细过程

《SpringBoot实现websocket服务端及客户端的详细过程》文章介绍了WebSocket通信过程、服务端和客户端的实现,以及可能遇到的问题及解决方案,感兴趣的朋友一起看看吧... 目录一、WebSocket通信过程二、服务端实现1.pom文件添加依赖2.启用Springboot对WebSocket

QT实现TCP客户端自动连接

《QT实现TCP客户端自动连接》这篇文章主要为大家详细介绍了QT中一个TCP客户端自动连接的测试模型,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录版本 1:没有取消按钮 测试效果测试代码版本 2:有取消按钮测试效果测试代码版本 1:没有取消按钮 测试效果缺陷:无法手动停

Nacos客户端本地缓存和故障转移方式

《Nacos客户端本地缓存和故障转移方式》Nacos客户端在从Server获得服务时,若出现故障,会通过ServiceInfoHolder和FailoverReactor进行故障转移,ServiceI... 目录1. ServiceInfoHolder本地缓存目录2. FailoverReactorinit

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

多线程解析报表

假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。 Way1 join import java.time.LocalTime;public class Main {public static void main(String[] args) thro

《x86汇编语言:从实模式到保护模式》视频来了

《x86汇编语言:从实模式到保护模式》视频来了 很多朋友留言,说我的专栏《x86汇编语言:从实模式到保护模式》写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节。 毕竟我不是作者,只有作者的解读才是最权威的。 当初我学习这本书的时候,只能靠自己摸索,网上搜不到什么好资源。 如果你正在学这本书或者汇编语言,那你有福气了。 本书作者李忠老师,以此书为蓝本,录制了全套视频。 试

Java Websocket实例【服务端与客户端实现全双工通讯】

Java Websocket实例【服务端与客户端实现全双工通讯】 现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发 出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏 览器需要不断的向服务器发出请求,然而HTTP

Java 多线程概述

多线程技术概述   1.线程与进程 进程:内存中运行的应用程序,每个进程都拥有一个独立的内存空间。线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换、并发执行,一个进程最少有一个线程,线程实际数是在进程基础之上的进一步划分,一个进程启动之后,进程之中的若干执行路径又可以划分成若干个线程 2.线程的调度 分时调度:所有线程轮流使用CPU的使用权,平均分配时间抢占式调度