新年第一天 恭祝大家新年快乐
一直有朋友问P2P相关的问题,最近有时间在微风IM的基础上,实现了P2P通信,共享给大家,希望大家批评指正。
源码下载 (只包含源码,无插入式广告:) 数据库下载 数据库与第一版相同没有变化
我们知道在网络通信中,如果所有的通信都通过服务器转发,会增加服务器的负担,如果实现了P2P,客户端之间直接通讯,比如聊天或者传送文件时不再通过服务器,而是客户端之间直接通信,将会有效的减轻服务器的负担,提高程序的效率。
本节相关的P2P,指的是通过TCP协议,在局域网中实现的P2P,广域网中的P2P暂时没有涉及。
本Demo基于来自英国的networkComms2.3.1开源通信框架
工作原理-通过服务器,在客户端之间建立P2P通道,之后客户端之间的通讯可以脱离服务器
流程如下:
NetworkComms通信框架的内在通信机制,使得我们实现P2P通信非常的简单。
(1):服务器开始监听
(2) :客户端,开始连接服务器,然后也开始监听工作,其实成为一个服务器。连接的过程中,系统会给客户端随机分派一个端口,以便完成与服务器的通信。连接完成后,我们获取到客户端的IP和与服务器通信的端口,客户端在此端口上展开监听,也就是说每个客户端都会展开监听,具备作为服务器的所有特质。
模拟代码:
ConnectionInfo connInfo = new ConnectionInfo("服务器IP", "服务器端口"); //客户端与服务器进行连接 Connection newTcpConnection = TCPConnection.GetConnection(connInfo); //客户端与服务器连接成功后,开始监听本地端口,客户端也称为可以监听的服务器 TCPConnection.StartListening(connInfo.LocalEndPoint);
(3):每个客户端需要维护一个“P2P通信的连接”表
我们用一个静态类来实现,具体可查看Common类
//字典中存储 用户ID 和相应的连接引用
public static Dictionary<string, Connection> UserConnList = new Dictionary<string, Connection>();
public static void AddUserConn(string userID, Connection conn) { lock (dictLocker) { if (UserConnList.ContainsKey(userID)) { UserConnList.Remove(userID); } UserConnList.Add(userID,conn); } } public static bool ContainsUserConn(string userID) { lock (dictLocker) { if (UserConnList.ContainsKey(userID)) { return true; } else { return false; } } } public static Connection GetUserConn(string userID) { lock(dictLocker) { if(UserConnList.ContainsKey(userID)) { return UserConnList[userID]; } else { return null; } } } public static void RemoveUserConn(string userID) { lock (dictLocker) { if (UserConnList.ContainsKey(userID)) { UserConnList.Remove(userID); } } }
相关操作方法
(4):客户端成功登陆后,从服务器获取所有在线其他客户端用户的本地端点(IP和端口)(即在其他客户端在步骤一中展开监听的端点),并进行连接
《1》客户端甲与其他客户端逐个进行连接,连接成功后,客户端甲添加对方用户ID和连接引用到本地P2P通道字典中
《2》客户端甲发送一个消息类型为”setupP2PMessage"的消息,给对方,以便于对方添加相应的记录到对方的P2P字典中
《3》客户端甲与其他用户进行连接时,客户端甲为“客户端”,其他的客户端为“服务器端”,所以在P2P通道的2端,总有一端为“客户端”,另一端为“服务器”。
配合NetworkComms通信框架,此种概念上的区分,并不影响P2P通道的通信。
客户端甲与其他客户端通信时,无论是作为”客户端“或者”服务器“均可,只要与对方存在TCP长连接即可。
《4》 这种由客户端之间彼此通信而建成的”服务器“,具备真正服务器的所有功能,会进行相应的”心跳检测“与”连接“维护等。
下面的代码:某客户端登陆后,获取所有已在线用户,并与之连接,连接完成后,发送”SetupP2PMessag"类型消息给对方。通过此过程,彼此双方的“P2P连接”都会建立完成。
private void GetP2PInfo() { //从服务器端,获取所有在线用户的信息 (用户ID,相对应的本地端点,在第一步中,客户端与服务器连接成功后,已经在此端点上开始监听了) IList<UserIDEndPoint> userInfoList = Common.TcpConn.SendReceiveObject<IList<UserIDEndPoint>>("GetP2PInfo", "ResP2pInfo", 5000, "GetP2P"); //遍历所有的在线用户 foreach (UserIDEndPoint userInfo in userInfoList) { try { if (userInfo.UserID != Common.UserID) { //在根目录下写入日志 LogInfo.LogMessage("准备建立" + userInfo.UserID + ":" + userInfo.IPAddress + ":" + userInfo.Port.ToString(), "P2PInfo"); //创建连接信息类 ConnectionInfo connInfo = new ConnectionInfo(userInfo.IPAddress, userInfo.Port); //把对方客户端当成服务器对应连接 Connection newTcpConnection = TCPConnection.GetConnection(connInfo); Common.AddUserConn(userInfo.UserID, newTcpConnection); SetUpP2PContract contract = new SetUpP2PContract(); contract.UserID = Common.UserID; //P2p通道打通后,发送一个消息给对方用户,以便于对方用户收到消息后,建立P2P通道 newTcpConnection.SendObject("SetupP2PMessage", contract); //在根目录下写入日志 LogInfo.LogMessage("已经建立" + userInfo.UserID + ":" + userInfo.IPAddress + ":" + userInfo.Port.ToString(), "P2PInfo"); } } catch { } } }
上面的代码中,我们把相关的P2P通道建立消息写入程序文件夹下“P2PINFO.txt文件”,以便于观察P2P消息通道的建立。和通过P2P通道发送消息
(5):通过P2P通道发送消息
客户端发送消息时,查看是否与对方存在 P2P通道,如果存在通过P2P连接发送消息,否则通过服务器发送
举例说明,发送聊天消息时,先查看是否有 p2p 通道
private void chatControl1_BeginToSend(string content) { this.chatControl1.ShowMessage(Common.UserName, DateTime.Now, content, true); //从客户端 Common中获取相应P2P通道 Connection p2pConnection = Common.GetUserConn(this.friendID); if (p2pConnection != null) { ChatContract chatContract = new ChatContract(); chatContract.UserID = Common.UserID; chatContract.UserName = Common.UserName; chatContract.DestUserID = this.friendID; chatContract.DestUserName = this.friendID; chatContract.Content = content; chatContract.SendTime = DateTime.Now; p2pConnection.SendObject("ClientChatMessage", chatContract); this.chatControl1.Focus(); LogInfo.LogMessage("通过p2p通道发送消息,当前用户ID为"+Common.UserID, "P2PINFO"); } else { ChatContract chatContract = new ChatContract(); chatContract.UserID = Common.UserID; chatContract.UserName = Common.UserName; chatContract.DestUserID = this.friendID; chatContract.DestUserName = this.friendID; chatContract.Content = content; chatContract.SendTime = DateTime.Now; Common.TcpConn.SendObject("ChatMessage", chatContract); this.chatControl1.Focus(); LogInfo.LogMessage("服务器转发消息", "P2PINFO"); } }
(6)P2P通道的注销
当某个客户端掉线后,我们要把其从其他相应客户端的P2P通道注销掉。
方法:
服务器通过心跳检测,知道某连接掉线后,发送消息给其他所有客户端。
private void UserStateNotify(string userID, bool onLine) { try { //用户状态契约类 UserStateContract userState = new UserStateContract(); userState.UserID = userID; userState.OnLine = onLine; IList<ShortGuid> allUserID; lock (syncLocker) { //获取所有用户字典中的用户ID allUserID = new List<ShortGuid>(userManager.Values); } //给所有用户发送某用户的在线状态 foreach (ShortGuid netID in allUserID) { List<Connection> result = NetworkComms.GetExistingConnection(netID, ConnectionType.TCP); if (result.Count > 0 && result[0].ConnectionInfo.NetworkIdentifier == netID) { result[0].SendObject("UserStateNotify", userState); } } } catch (Exception ex) { LogTools.LogException(ex, "MainForm.UserStateNotify"); } }
服务器端代码,发送用户上线或下线消息
客户端代码:
NetworkComms.AppendGlobalIncomingPacketHandler<UserStateContract>("UserStateNotify", IncomingUserStateNotify);
private void IncomingUserStateNotify(PacketHeader header, Connection connection, UserStateContract userStateContract) { if (userStateContract.OnLine) { lock (syncLocker) { //此部分,处理用户上线,与P2p通道无关 Common.GetDicUser(userStateContract.UserID).State = OnlineState.Online; } } else { lock (syncLocker) { Common.GetDicUser(userStateContract.UserID).State = OnlineState.Offline; //当某用户下线后,删除此用户相关的p2p 通道 Common.RemoveUserConn(userStateContract.UserID); } } }
P2P通信暂时介绍到这里,希望大家喜欢。