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

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

---------------------- 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

相关文章

MYSQL查询结果实现发送给客户端

《MYSQL查询结果实现发送给客户端》:本文主要介绍MYSQL查询结果实现发送给客户端方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录mysql取数据和发数据的流程(边读边发)Sending to clientSending DataLRU(Least Rec

Python FastMCP构建MCP服务端与客户端的详细步骤

《PythonFastMCP构建MCP服务端与客户端的详细步骤》MCP(Multi-ClientProtocol)是一种用于构建可扩展服务的通信协议框架,本文将使用FastMCP搭建一个支持St... 目录简介环境准备服务端实现(server.py)客户端实现(client.py)运行效果扩展方向常见问题结

python多线程并发测试过程

《python多线程并发测试过程》:本文主要介绍python多线程并发测试过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、并发与并行?二、同步与异步的概念?三、线程与进程的区别?需求1:多线程执行不同任务需求2:多线程执行相同任务总结一、并发与并行?1、

C#使用MQTTnet实现服务端与客户端的通讯的示例

《C#使用MQTTnet实现服务端与客户端的通讯的示例》本文主要介绍了C#使用MQTTnet实现服务端与客户端的通讯的示例,包括协议特性、连接管理、QoS机制和安全策略,具有一定的参考价值,感兴趣的可... 目录一、MQTT 协议简介二、MQTT 协议核心特性三、MQTTNET 库的核心功能四、服务端(BR

Python多进程、多线程、协程典型示例解析(最新推荐)

《Python多进程、多线程、协程典型示例解析(最新推荐)》:本文主要介绍Python多进程、多线程、协程典型示例解析(最新推荐),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定... 目录一、multiprocessing(多进程)1. 模块简介2. 案例详解:并行计算平方和3. 实现逻

SpringBoot快速搭建TCP服务端和客户端全过程

《SpringBoot快速搭建TCP服务端和客户端全过程》:本文主要介绍SpringBoot快速搭建TCP服务端和客户端全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录TCPServerTCPClient总结由于工作需要,研究了SpringBoot搭建TCP通信的过程

Python与Java交互出现乱码的问题解决

《Python与Java交互出现乱码的问题解决》在现代软件开发中,跨语言系统的集成已经成为日常工作的一部分,特别是当Python和Java之间进行交互时,编码问题往往会成为导致数据传输错误、乱码以及难... 目录背景:为什么会出现乱码问题产生的场景解决方案:确保统一的UTF-8编码完整代码示例总结在现代软件

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

基于Python和MoviePy实现照片管理和视频合成工具

《基于Python和MoviePy实现照片管理和视频合成工具》在这篇博客中,我们将详细剖析一个基于Python的图形界面应用程序,该程序使用wxPython构建用户界面,并结合MoviePy、Pill... 目录引言项目概述代码结构分析1. 导入和依赖2. 主类:PhotoManager初始化方法:__in

用js控制视频播放进度基本示例代码

《用js控制视频播放进度基本示例代码》写前端的时候,很多的时候是需要支持要网页视频播放的功能,下面这篇文章主要给大家介绍了关于用js控制视频播放进度的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言html部分:JavaScript部分:注意:总结前言在javascript中控制视频播放