---------------------- 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端口对应的Socket
foreach (var item in connSockets)
{
if (item.Key == destIP)
{
destSocket = item.Value;
break;
}
}
if (destSocket == null)
{
MessageBox.Show("当前客户端不在线");
return;
}
//调用转发方法,传入连接Socket和信息,以及发送消息的客户端IP
TranspondMsg(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)
{
//用于存储转发信息所转换成的字节,不能超过2M
byte[] 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培训、期待与您交流! ----------------------