首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 實作 P2P 網路通訊程式:從區域網路到 NAT 穿透

     編輯:關於C#

在網上看過許多程式(QQ、Azureus、Ants、PPStream)都實作了 P2P,以前覺得技術很高深。透過這段時間學習才發現,單純實作 P2P 在區域網路通訊很容易,但要實現外網穿透 (NAT) 感覺很困難。最近看了 Azureus 和 eMule 原始碼,分別是 Java 和 C++ 版,對這兩門語言都不熟悉,看起來很吃力。最後只好根據 VC++ 實作的 P2PDemo 程式改版,依設計思路用 C# 寫了一個 Demo。經測試,多個客戶端在區域網路能脫離服務端實現端到端工作。外網情況要透過路由器,看 Azureus 要用 uPnP 進行連接埠對應,在 CodeProject 下載 uPnP 原始碼測試,結果沒啟用 uPnP 路由器。現在郁悶了,不知道下一步怎麼測試,是否用 uPnP 實現連接埠自動對應成功就能像 QQ 那樣通訊。

程式架構說明

1. 公共類別:封包結構定義

Packet.cs - 基礎封包類別

[Serializable()]
public abstract class Packet {
    public virtual int GetCommandType() { return -1; }
    public string UserName { get; set; }
    public Packet() { }
    public Packet(string username) { this.UserName = username; }
}

MassTextPacket.cs - 分片傳輸類別

[Serializable()]
public class MassTextPacket : TextPacket {
    public int SeqID { get; set; }     // 封包序列
    public int SeqCount { get; set; }  // 封包數量
    public int CLSD { get; set; }      // 識別碼
}

2. 客戶端核心邏輯

P2P 訊息發送

private bool SendMessageTo(string toUserName, Packet packet) {
    PeerEntity toUser = userList.Single(c => c.UserName == toUserName);
    if (toUser == null) return false;
    ReceivedACK = false;

    for (int i = 0; i < MAXRETRY; i++) {
        // 若對方 P2P 位址有效,嘗試直接發送
        if (toUser.P2PAddress != null && toUser.P2PAddress.Port != 0) {
            if (packet.GetType() == typeof(TextPacket)) {
                // 文字訊息發送邏輯,支援大訊息分片
                TextPacket msgPacket = new TextPacket(toUserName, (packet as TextPacket).Message);
                byte[] buffer = UtilityHelper.Serialize(msgPacket);
                if (buffer.Length > MAXBUFFERSIZE) {
                    // 分片傳輸邏輯
                    MassTextPacket mtp = new MassTextPacket();
                    mtp.SeqID = 0;
                    mtp.SeqCount = (int)Math.Ceiling(buffer.Length / (decimal)MAXBUFFERSIZE);
                    mtp.CLSD = mtp.GetHashCode();
                    long pos = 0;
                    long count = buffer.Length < MAXBUFFERSIZE ? buffer.Length : MAXBUFFERSIZE;
                    while (pos < buffer.Length && pos > 0) {
                        byte[] bytes = new byte[count];
                        for (int k = 0; k < count; k++)
                            bytes[k] = buffer[pos + k];
                        mtp.SeqID++;
                        mtp.Message = Convert.ToBase64String(bytes);
                        byte[] buf = UtilityHelper.Serialize(mtp);
                        client.Send(buf, buf.Length, toUser.P2PAddress);
                        Thread.Sleep(100);
                    }
                } else {
                    client.Send(buffer, buffer.Length, toUser.P2PAddress);
                }
            } else if (packet.GetType() == typeof(FileStreamPacket)) {
                // 檔案傳輸邏輯,支援分片與進度控制
                FileStreamPacket fsp = packet as FileStreamPacket;
                using (FileStream fs = new FileStream(fsp.FileName, FileMode.Open, FileAccess.Read, FileShare.Read)) {
                    handle1.Reset();
                    fsp.SeqID = 0;
                    fsp.SeqCount = (int)Math.Ceiling(fs.Length / (decimal)MAXBUFFERSIZE);
                    fsp.CLSD = fsp.GetHashCode();
                    long pos = 0;
                    long count = fs.Length < MAXBUFFERSIZE ? fs.Length : MAXBUFFERSIZE;
                    while (pos < fs.Length && count > 0) {
                        byte[] buffer = new byte[count];
                        fs.Seek(pos, SeekOrigin.Begin);
                        fs.Read(buffer, 0, (int)count);
                        pos += count;
                        count = pos + MAXBUFFERSIZE < fs.Length ? MAXBUFFERSIZE : fs.Length - pos;
                        fsp.SeqID++;
                        fsp.Message = Convert.ToBase64String(buffer);
                        byte[] buf = UtilityHelper.Serialize(fsp);
                        client.Send(buf, buf.Length, toUser.P2PAddress);
                        Thread.Sleep(300);
                    }
                    handle1.Set();
                }
            }

            // 等待 ACK 確認
            for (int j = 0; j < 10; j++) {
                if (this.ReceivedACK) {
                    this.ReceivedACK = false;
                    return true;
                }
                Thread.Sleep(300);
            }
        }

        // 建立 P2P 打洞封包,透過伺服器轉發請求對方向自己打洞
        P2PConnectionPacket transMsg = new P2PConnectionPacket(UserName, toUserName);
        byte[] msgBuffer = UtilityHelper.Serialize(transMsg);
        client.Send(msgBuffer, msgBuffer.Length, hostPoint);

        // 等待對方 P2PCONNECTACK 訊息
        for (int j = 0; j < 10; ++j) {
            toUser = userList.Single(c => c.UserName == toUserName);
            if (toUser.P2PAddress != null && toUser.P2PAddress.Port != 0)
                break;
            Thread.Sleep(300);
        }
    }
    return false;
}

訊息接收執行緒

private void RecvThreadProc() {
    byte[] buffer;
    while (true) {
        buffer = client.Receive(ref remotePoint);
        Packet packet = UtilityHelper.Deserialize(buffer) as Packet;
        Type msgType = packet.GetType();

        if (msgType == typeof(UserListAckPacket)) {
            // 更新使用者列表
            UserListAckPacket usersMsg = (UserListAckPacket)packet;
            userList.Clear();
            foreach (PeerEntity user in usersMsg.Users)
                userList.Add(user);
            bUserListComplete = true;
        }
        else if (msgType == typeof(UserLoginAckPacket)) {
            ProcUserLogAckMsg(packet);
        }
        else if (msgType == typeof(TextPacket)) {
            TextPacket txtPacket = (TextPacket)packet;
            printf("Receive a message: {0}", txtPacket.Message);
            // 發送 ACK
            P2PAckPacket ackMsg = new P2PAckPacket();
            buffer = UtilityHelper.Serialize(ackMsg);
            client.Send(buffer, buffer.Length, remotePoint);
        }
        else if (msgType == typeof(MassTextPacket)) {
            lock (this) {
                MassTextPacket fPacket = (MassTextPacket)packet;
                if (packets.ContainsKey(fPacket.CLSD))
                    packets[fPacket.CLSD].Add(fPacket);
                else
                    packets.Add(fPacket.CLSD, new List() { fPacket });
                printf("PacketID:{0}--SeqNo:{1}--progress: {2}%", fPacket.CLSD, fPacket.SeqID,
                    (int)(Math.Round(packets[fPacket.CLSD].Count / (decimal)(fPacket as MassTextPacket).SeqCount, 2) * 100));
                // 組裝完整訊息
                if ((fPacket as MassTextPacket).SeqCount == packets[fPacket.CLSD].Count) {
                    List temp = packets[fPacket.CLSD].OrderBy(c => c.SeqID).ToList();
                    List values = new List();
                    foreach (MassTextPacket mt in temp) {
                        byte[] buf = Convert.FromBase64String(mt.Message);
                        values.AddRange(buf);
                    }
                    MassTextPacket value = UtilityHelper.Deserialize(values.ToArray()) as MassTextPacket;
                    printf("Receive a message: {0}", value.Message);
                    // 發送 ACK
                    P2PAckPacket ackMsg = new P2PAckPacket();
                    buffer = UtilityHelper.Serialize(ackMsg);
                    client.Send(buffer, buffer.Length, remotePoint);
                }
            }
        }
        else if (msgType == typeof(FileStreamPacket)) {
            // 檔案接收與重組邏輯
            lock (this) {
                FileStreamPacket fPacket = (FileStreamPacket)packet;
                if (packets.ContainsKey(fPacket.CLSD))
                    packets[fPacket.CLSD].Add(fPacket);
                else
                    packets.Add(fPacket.CLSD, new List() { fPacket });
                // 進度顯示與檔案重組...
                if ((fPacket as FileStreamPacket).SeqCount == packets[fPacket.CLSD].Count) {
                    // 寫入檔案、發送 ACK 等
                }
            }
        }
        else if (msgType == typeof(P2PAckPacket)) {
            this.ReceivedACK = true;
        }
        else if (msgType == typeof(P2PPurchHolePacket)) {
            ProcP2PPurchHoleMsg(packet, remotePoint);
        }
        else if (msgType == typeof(P2PPurchHoleAckPacket)) {
            PeerEntity touser = userList.SingleOrDefault(c => c.UserName == (packet as P2PPurchHoleAckPacket).ToUserName);
            touser.P2PAddress = touser.RemoteEndPoint;
        }
        Thread.Sleep(100);
    }
}

建立 P2P 會話(打洞邏輯)

private void ProcP2PPurchHoleMsg(Packet packet, IPEndPoint remoteEP) {
    P2PPurchHolePacket purchReqMsg = (P2PPurchHolePacket)packet;
    PeerEntity toUser = userList.Single(c => c.UserName == purchReqMsg.ToUserName);
    PeerEntity user = userList.Single(c => c.UserName == purchReqMsg.UserName);
    toUser.P2PAddress = toUser.RemoteEndPoint;
    printf("Set P2P Address for {0}->[{1}]", user.UserName, toUser.P2PAddress.ToString());

    // uPnP 連接埠對應
    if (NAT.AddPortMapping(toUser.P2PAddress.Port, ProtocolType.Udp, "AddPortMapping"))
        printf("Port mapping successed!");

    // 發送打洞訊息到遠端主機
    P2PPurchHoleAckPacket trashMsg = new P2PPurchHoleAckPacket(purchReqMsg.UserName, purchReqMsg.ToUserName);
    byte[] buffer = UtilityHelper.Serialize(trashMsg);
    client.Send(buffer, buffer.Length, user.RemoteEndPoint);
}

3. 伺服端邏輯

訊息處理執行緒

private void RecvThreadProc() {
    IPEndPoint remotePoint = null;
    byte[] msgBuffer = null;
    while (true) {
        msgBuffer = server.Receive(ref remotePoint);
        try {
            object msgObj = UtilityHelper.Deserialize(msgBuffer);
            switch ((msgObj as Packet).GetCommandType()) {
                case Command.MSG_USERLOGIN:
                    ProcUserLoginMsg(msgObj as UserLoginPacket, remotePoint);
                    break;
                case Command.MSG_USERLOGOUT:
                    ProcUserLogoutMsg(msgObj as UserLogoutPacket, remotePoint);
                    break;
                case Command.MSG_GETUSERLIST:
                    ProcGetUserListMsg(msgObj as UserListPacket, remotePoint);
                    break;
                case Command.MSG_P2PCONNECT:
                    ProcP2PConnectMsg(msgObj as P2PConnectionPacket, remotePoint);
                    break;
                case Command.MSG_USERACTIVEQUERY:
                    ProcUserActiveQueryMsg(msgObj as UserActiveQueryPacket, remotePoint);
                    break;
            }
            Thread.Sleep(100);
        } catch { }
    }
}

伺服端轉發 P2P 連線請求

private void ProcP2PConnectMsg(Packet packet, IPEndPoint remoteEP) {
    P2PConnectionPacket transMsg = (P2PConnectionPacket)packet;
    printf("{0}({1}) wants to p2p {2}", remoteEP.Address.ToString(), transMsg.UserName, transMsg.ToUserName);
    PeerEntity toUser = userList.SingleOrDefault(c => c.UserName == transMsg.ToUserName);
    // 轉發打洞請求
    P2PPurchHolePacket transMsg2 = new P2PPurchHolePacket(transMsg.UserName, toUser.UserName);
    byte[] buffer = UtilityHelper.Serialize(transMsg2);
    server.Send(buffer, buffer.Length, toUser.RemoteEndPoint);
}

困惑與待解決問題

  1. 能否實現外網通訊?要像 QQ 那樣通訊需做哪些改進?
  2. 檔案斷點續傳如何實現?
  3. C# 封裝的網路操作類(如 QQ.NET 原始碼的 Net 實作)
  4. 遠端協助功能實現

希望大家共同討論、共同進步!本文附配套原始碼供下載參考。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved