コード例 #1
0
ファイル: P2PGroupClient.cs プロジェクト: bssthu/CSP2P
 /// <summary>
 /// 准备关闭与对方的Socket连接,关闭之前通知对方
 /// </summary>
 public void CloseSocket()
 {
     // 通知对方关闭Socket连接
     try
     {
         ProtocalHandler protocalHandler =
             new ProtocalHandler(main.MyName);
         string protocalText       = protocalHandler.Pack("ctl_closesocket");
         byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
         SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
         saEA.Completed +=
             new EventHandler <SocketAsyncEventArgs>(
                 closeSocketCompletedEventHandler);
         saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
         socket.SendAsync(saEA);
     }
     catch   // 异常则已经关闭
     {
         socket = null;
     }
     try
     {
         owner.removeClientDelegate(targetName);
     }
     catch (Exception ex)
     {
         Trace.WriteLine("异常位置:P2PGroupClient.closeSocket");
         Trace.WriteLine(ex.Message);
     }
 }
コード例 #2
0
        /// <summary>
        /// 发起方:
        /// 成功建立Socket连接,准备发送1次确认
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void createSocketCompletedEventHandler(
            object sender, EventArgs ea)
        {
            // 取得新建的Socket(如果连接成功)
            // 此Socket应等于this.socket
            SocketAsyncEventArgs socketAsyncEA = (SocketAsyncEventArgs)ea;
            Socket createdSocket = socketAsyncEA.ConnectSocket;

            if (createdSocket != null && createdSocket == socket)  // 连接成功
            {
                // 1次确认,发送name=自己(发起方)用户名
                ProtocalHandler protocalHandler =
                    new ProtocalHandler(main.MyName);
                string protocalText = protocalHandler.Pack("init_gp_request");
                // 发送群中所有好友
                foreach (string friend in owner.friends)
                {
                    string friendBase64 =
                        protocalHandler.StringToBase64string(friend);
                    protocalText = protocalHandler.Append(
                        protocalText, "target", friendBase64);
                }
                byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
                SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
                saEA.Completed +=
                    new EventHandler <SocketAsyncEventArgs>(
                        createdSocketSendNamesCompletedEventHandler);
                saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
                socket.SendAsync(saEA);
            }
        }
コード例 #3
0
ファイル: P2PChatClient_File.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 中途主动取消文件收发
        /// </summary>
        public void StopToSendFile()
        {
            ProtocalHandler protocalHandler =
                new ProtocalHandler(owner.targetName);

            try
            {
                // 封装协议文本
                string protocalText = protocalHandler.Pack("file_abort");
                protocalText =
                    protocalHandler.Append(protocalText, "file", safeFileNameBase64);
                // 发送
                byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
                SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
                saEA.Completed +=
                    new EventHandler <SocketAsyncEventArgs>(
                        SendCompletedDoNothing);
                saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
                socket.SendAsync(saEA);
            }
            catch (Exception ex)
            {
                Trace.WriteLine("异常位置:StopToSendFile");
                Trace.WriteLine(ex.Message);
            }
        }
コード例 #4
0
ファイル: P2PChatClient_Create.cs プロジェクト: bssthu/CSP2P
 /// <summary>
 /// 接受方:
 /// 收到的1次确认为私聊,记录对方的身份信息
 /// 此时确定owner
 /// 发送2次确认
 /// </summary>
 /// <param name="protocalHandler">接收到的协议的封装</param>
 public void newSocketChatNameReceived(ProtocalHandler protocalHandler)
 {
     try
     {
         // 对方用户名
         string targetNameBase64 =
             protocalHandler.GetElementTextByTag("name");
         string targetName =
             protocalHandler.Base64stringToString(targetNameBase64);
         // 打开私聊窗口
         main.SetChatFormClient(this, targetName);
         // 2次确认,发送you = 对方(发起方)用户名
         string protocalText = protocalHandler.Pack("init_chat_ok");
         protocalText = protocalHandler.Append(
             protocalText, "you", targetNameBase64);
         byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
         SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
         saEA.Completed +=
             new EventHandler <SocketAsyncEventArgs>(
                 newSocketUsernameSendEventHandler);
         saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
         socket.SendAsync(saEA);
     }
     catch (Exception ex)
     {
         Trace.WriteLine("异常位置:" +
                         "newSocketChatNameReceived");
         Trace.WriteLine(ex.Message);
         CloseSocket();
     }
 }
コード例 #5
0
ファイル: P2PChatClient_File.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 发送方:
        /// 发送文件的某个片段
        /// </summary>
        /// <param name="seq">当前序号</param>
        private void sendingFilePack(uint seq)
        {
            ProtocalHandler protocalHandler =
                new ProtocalHandler(owner.targetName);
            // 封装协议文本
            string protocalText = protocalHandler.Pack("file_data");

            protocalText =
                protocalHandler.Append(protocalText, "file", safeFileNameBase64);
            protocalText =
                protocalHandler.Append(protocalText, "seq", seq.ToString());
            uint len = FilePacker.BytesOfPack;

            if (seq == maxSeq - 1)
            {
                len = filePacker.GetSizeOfLastPack();
            }
            protocalText =
                protocalHandler.Append(protocalText, "len", len.ToString());
            // 文件内容
            byte[] buf        = filePacker.GetPack(seq);
            string dataBase64 = Convert.ToBase64String(buf, 0, (int)len);

            protocalText =
                protocalHandler.Append(protocalText, "data", dataBase64);
            int checksum = 0;

            for (int i = 0; i < len; i++)
            {
                checksum += buf[i];
            }
            checksum     = -checksum;
            protocalText =
                protocalHandler.Append(protocalText, "checksum",
                                       checksum.ToString());
            try
            {
                // 发送
                byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
                SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
                saEA.Completed +=
                    new EventHandler <SocketAsyncEventArgs>(
                        SendCompletedDoNothing);
                saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
                socket.SendAsync(saEA);
            }
            catch (Exception ex)
            {
                Trace.WriteLine("异常位置:SendingFilePack");
                Trace.WriteLine(ex.Message);
            }
        }
コード例 #6
0
ファイル: P2PChatClient_Chat.cs プロジェクト: bssthu/CSP2P
 /// <summary>
 /// 发送RTF文本
 /// </summary>
 /// <param name="rtfToSend"></param>
 public void SendRtfText(string rtfToSend)
 {
     try
     {
         ProtocalHandler protocalHandler = new ProtocalHandler(main.MyName);
         string          protocalText    = protocalHandler.Pack(
             "chat_rtf", protocalHandler.StringToBase64string(rtfToSend));
         byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
         SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
         saEA.Completed +=
             new EventHandler <SocketAsyncEventArgs>(
                 SendCompletedDoNothing);
         saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
         socket.SendAsync(saEA);
     }
     catch (Exception ex)
     {
         Trace.WriteLine("异常位置:SendRtfText");
         Trace.WriteLine(ex.Message);
     }
 }
コード例 #7
0
ファイル: P2PChatClient_File.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 完成文件收发
        /// </summary>
        private void finishSendingOrReceivingFile()
        {
            // 关定时器
            sendTimeOutTimer.Stop();
            // 关文件
            if (filePacker != null)
            {
                filePacker.Close();
                filePacker = null;
            }
            if (fileWriter != null)
            {
                fileWriter.Close();
                fileWriter = null;
            }
            ProtocalHandler protocalHandler = new ProtocalHandler();

            owner.BeginInvoke(owner.stopSendingOrReceivingFileDelegate,
                              String.Format("文件{0}的收发已完成。",
                                            protocalHandler.Base64stringToString(safeFileNameBase64)));
            safeFileNameBase64 = null;
        }
コード例 #8
0
ファイル: P2PChatClient_Create.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 发起方:
        /// Socket连接完成的事件处理程序
        /// 发送1次确认
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="ea"></param>
        public void createSocketCompletedEventHandler(
            object sender, EventArgs ea)
        {
            // 取得新建的Socket(如果连接成功)
            // 此Socket应等于this.socket
            SocketAsyncEventArgs socketAsyncEA = (SocketAsyncEventArgs)ea;
            Socket createdSocket = socketAsyncEA.ConnectSocket;

            if (createdSocket != null && createdSocket == socket)  // 连接成功
            {
                // 1次确认,发送name=自己(发起方)用户名
                ProtocalHandler protocalHandler =
                    new ProtocalHandler(main.MyName);
                string protocalText       = protocalHandler.Pack("init_chat_request");
                byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
                SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
                saEA.Completed +=
                    new EventHandler <SocketAsyncEventArgs>(
                        createdSocketSendNameCompletedEventHandler);
                saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
                createdSocket.SendAsync(saEA);
            }
        }
コード例 #9
0
ファイル: P2PChatClient_File.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 发起方:
        /// 申请发送文件
        /// </summary>
        /// <param name="fileName">文件名(含路径)</param>
        public void RequestToSendFile(string fileName)
        {
            // 打开文件
            filePacker = new FilePacker(owner.fileName);
            maxSeq     = filePacker.GetPackNumbers();
            owner.BeginInvoke(owner.setProgressDelegate, (int)maxSeq);
            // 取得文件名,并将文件名编码以便使用
            safeFileNameBase64 = owner.GetSafeFileName(fileName);
            ProtocalHandler protocalHandler =
                new ProtocalHandler(owner.targetName);

            safeFileNameBase64 = protocalHandler.StringToBase64string(safeFileNameBase64);
            // 封装协议文本
            string protocalText = protocalHandler.Pack("file_requesttosend");

            protocalText =
                protocalHandler.Append(protocalText, "file", safeFileNameBase64);
            protocalText =
                protocalHandler.Append(protocalText, "maxseq", maxSeq.ToString());
            try
            {
                // 发送
                byte[] sendbuf            = Encoding.ASCII.GetBytes(protocalText);
                SocketAsyncEventArgs saEA = new SocketAsyncEventArgs();
                saEA.Completed +=
                    new EventHandler <SocketAsyncEventArgs>(
                        SendCompletedDoNothing);
                saEA.SetBuffer(sendbuf, 0, sendbuf.Length);
                socket.SendAsync(saEA);
            }
            catch (Exception ex)
            {
                Trace.WriteLine("异常位置:RequestToSendFile");
                Trace.WriteLine(ex.Message);
            }
        }
コード例 #10
0
 /// <summary>
 /// 发起方:
 /// 收到2次确认,核对对方的身份信息
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="ea"></param>
 private void createSocketNameReceivedEventHandler(
     object sender, EventArgs ea)
 {
     try
     {
         SocketAsyncEventArgs socketAsyncEA =
             (SocketAsyncEventArgs)ea;
         // 获取文本
         string rcvString =
             Encoding.ASCII.GetString(socketAsyncEA.Buffer);
         ProtocalHandler protocalHandler =
             new ProtocalHandler(main.MyName);
         // 判断是否是协议
         if (!protocalHandler.SetXmlText(rcvString))
         {
             throw new MyProtocalException(
                       "不是有效的协议文本,找不到CSP2P标签");
         }
         // 数据包类型
         string type = protocalHandler.GetElementTextByTag("type");
         if (type.Equals("closesocket"))     // 对方要求关闭Socket
         {
             closeSocketWithoutSend();
         }
         if (!type.Equals("init_gp_ok"))
         {
             throw new MyProtocalException(
                       "不是有效的2次确认," +
                       "找不到type标签或type不为init_gp_ok");
         }
         // 对方用户名
         string key = protocalHandler.GetElementTextByTag("name");
         key = protocalHandler.Base64stringToString(key);
         if (!key.Equals(targetName))
         {
             throw new MyProtocalException(
                       "不是有效的2次确认," +
                       "找不到name标签或接受方用户名不符");
         }
         // 己方用户名
         string myName = protocalHandler.GetElementTextByTag("you");
         myName = protocalHandler.Base64stringToString(myName);
         if (!myName.Equals(main.MyName))
         {
             throw new MyProtocalException(
                       "2次确认发现错误,找不到you标签" +
                       "或接受方返回的发送方用户名不符");
         }
         // 开始通信
         owner.BeginInvoke(owner.sendRTFDelegate, rtfToSend);
         rtfToSend = null;
         beginReceive();
     }
     catch (Exception ex)
     {
         Trace.WriteLine("异常位置:" +
                         "createSocketNameReceivedEventHandler");
         Trace.WriteLine(ex.Message);
         CloseSocket();
     }
 }
コード例 #11
0
ファイル: P2PGroupClient.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 收到一条数据
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="ea"></param>
        private void receiveOneEventHandler(object sender, EventArgs ea)
        {
            try
            {
                SocketAsyncEventArgs socketAsyncEA =
                    (SocketAsyncEventArgs)ea;
                // 获取文本
                string rcvString =
                    Encoding.ASCII.GetString(socketAsyncEA.Buffer);
                ProtocalHandler protocalHandler = new ProtocalHandler();
                // 判断是否是协议
                protocalHandler.SetXmlText(rcvString);
                // 对方用户名
                string targetNameBase64 =
                    protocalHandler.GetElementTextByTag("name");
                string targetName =
                    protocalHandler.Base64stringToString(targetNameBase64);
                if (targetName == null)
                {
                    throw new MyProtocalException(
                              "不是有效协议," +
                              "找不到name标签或无法解码");
                }
                // 类型
                string type = protocalHandler.GetElementTextByTag("type");
                if (type == null)
                {
                    throw new MyProtocalException(
                              "不是有效的协议文本,找不到type标签");
                }
                else
                {
                    switch (type)
                    {
                    case "ctl_closesocket":         // 对方要求关闭Socket
                        closeSocketWithoutSend();
                        break;

                    case "gp_rtf":         // 收到聊天消息(RTF文本)
                        string rtfText = protocalHandler.GetElementTextByTag("data");
                        if (rtfText == null)
                        {
                            throw new MyProtocalException(
                                      "不是有效的协议文本,type为gp_rtf但找不到data标签");
                        }
                        rtfText = protocalHandler.Base64stringToString(rtfText);
                        if (rtfText == null)
                        {
                            throw new MyProtocalException(
                                      "不是有效的协议文本,rtf文本解码错误");
                        }
                        // 显示收到的消息
                        owner.BeginInvoke(owner.receiveRTFDelegate,
                                          new object[] { rtfText });
                        break;

                    default:
                        throw new MyProtocalException("type类型未知: " + type);
                    }   // End Switch
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine("异常位置:P2PGroupClient.receiveOneEventHandler");
                Trace.WriteLine(ex.Message);
            }
            finally
            {
                // 继续接收数据
                if (socket != null)
                {
                    beginReceive();
                }
            }
        }
コード例 #12
0
ファイル: P2PChatClient_File.cs プロジェクト: bssthu/CSP2P
        /// <summary>
        /// 收到的协议是file类型时的判断
        /// </summary>
        /// <param name="type">类型</param>
        /// <param name="protocalHandler">协议处理类</param>
        protected void ReceivedTypeFile(string type, ProtocalHandler protocalHandler)
        {
            // 文件收发时接收到的文件名
            string receivedSafeFileNameBase64 =
                protocalHandler.GetElementTextByTag("file");

            try
            {
                switch (type)
                {
                case "file_requesttosend":         // 收到接收文件的请求
                    if (owner.showPanelFile)
                    {
                        // 直接拒收
                        DeniedToFile();
                        break;
                    }
                    if (receivedSafeFileNameBase64 == null)
                    {
                        throw new MyProtocalException(
                                  "不是有效的协议文本,type为file但找不到file标签");
                    }
                    string receivedSafeFileName =
                        protocalHandler.Base64stringToString(receivedSafeFileNameBase64);
                    if (receivedSafeFileName == null)
                    {
                        throw new MyProtocalException(
                                  "不是有效的协议文本,文件名解码错误");
                    }
                    safeFileNameBase64 = receivedSafeFileNameBase64;
                    maxSeq             = Convert.ToUInt32(
                        protocalHandler.GetElementTextByTag("maxseq"));
                    owner.BeginInvoke(owner.setProgressDelegate, (int)maxSeq);
                    currentSeq = 0;
                    // 让用户选择是否接收文件
                    owner.BeginInvoke(owner.askedForReceivingFileDelegate,
                                      receivedSafeFileName);
                    break;

                case "file_cleartosend":
                    if (!owner.showPanelFile)
                    {
                        // 拒收
                        StopToSendFile();
                        break;
                    }
                    // 检查文件名匹配情况
                    CheckFileName(receivedSafeFileNameBase64);
                    // 开始发送
                    try
                    {
                        owner.sendingOrReceivingFile = true;
                        owner.BeginInvoke(owner.startTimingDelegate);
                        sendingFilePack(0);
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine("无法开始发送文件。");
                        throw ex;
                    }
                    break;

                case "file_denied":          // 发文件的请求被拒绝
                    if (!owner.showPanelFile)
                    {
                        // 没有在准备收发文件的状态
                        break;
                    }
                    if (owner.sendingOrReceivingFile)
                    {
                        // 已经在收发文件,取消的type应该为file_abort
                        StopToSendFile();
                        break;
                    }
                    // 检查文件名匹配情况
                    CheckFileName(receivedSafeFileNameBase64);
                    // 取消文件发送
                    fileDenied();
                    break;

                case "file_data":
                    // 检查是否正在收发文件
                    CheckWhileSendingOrReceiving();
                    // 超时定时器重置
                    sendTimeOutTimer.Start();
                    // 检查文件名匹配情况
                    CheckFileName(receivedSafeFileNameBase64);
                    string seqString   = protocalHandler.GetElementTextByTag("seq");
                    uint   receivedSeq = Convert.ToUInt32(seqString);
                    string data        = protocalHandler.GetElementTextByTag("data");
                    byte[] buf         = Convert.FromBase64String(data);
                    // 检查checksum
                    uint len = Convert.ToUInt32(
                        protocalHandler.GetElementTextByTag("len"));
                    int checksum = Convert.ToInt32(
                        protocalHandler.GetElementTextByTag("checksum"));
                    for (uint i = 0; i < len; i++)
                    {
                        checksum += buf[i];
                    }
                    // 顺序正确 && 校验正确
                    if (receivedSeq == currentSeq && checksum == 0)
                    {
                        owner.BeginInvoke(owner.showProgressDelegate, (int)currentSeq);
                        fileWriter.WriteBytes(currentSeq, buf, len);
                        sendAck(++currentSeq);
                        if (currentSeq >= maxSeq)
                        {
                            finishSendingOrReceivingFile();
                        }
                    }
                    else
                    {
                        sendAck(currentSeq);
                    }
                    break;

                case "file_ack":
                    // 检查是否正在收发文件
                    CheckWhileSendingOrReceiving();
                    // 检查文件名匹配情况
                    CheckFileName(receivedSafeFileNameBase64);
                    string ackString = protocalHandler.GetElementTextByTag("ack");
                    receivedAck(Convert.ToUInt32(ackString));
                    break;

                case "file_abort":
                    if (!owner.showPanelFile)
                    {
                        // 没有在准备收发文件的状态
                        break;
                    }
                    if (!owner.sendingOrReceivingFile)
                    {
                        // 没有正在收发文件
                        break;
                    }
                    // 取消文件发送
                    owner.BeginInvoke(owner.stopSendingOrReceivingFileDelegate,
                                      String.Format("文件{0}的收发已被取消。",
                                                    protocalHandler.Base64stringToString(safeFileNameBase64)));
                    fileAborted();
                    break;

                default:
                    throw new MyProtocalException("协议类型未知");
                    //break;
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine("异常位置:ReceivedTypeFile");
                Trace.WriteLine(ex.Message);
                sendErrCount++;
                if (owner.sendingOrReceivingFile)
                {
                    sendAck(currentSeq);
                }

                //StopToSendFile();
            }
        }
コード例 #13
0
        /// <summary>
        /// 接受方
        /// 收到1次确认,判断连接的类型
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="ea"></param>
        private void firstAckReceivedEventHandler(
            object sender, EventArgs ea)
        {
            Socket rcvSocket = sender as Socket;

            try
            {
                // 接收到的异步Socket事件参数
                SocketAsyncEventArgs socketAsyncEA = (SocketAsyncEventArgs)ea;
                // 提取接收到的文本
                string rcvString =
                    Encoding.ASCII.GetString(socketAsyncEA.Buffer);
                ProtocalHandler protocalHandler =
                    new ProtocalHandler(MyName);
                // 是否是协议
                if (!protocalHandler.SetXmlText(rcvString))
                {
                    throw new MyProtocalException(
                              "不是有效的协议文本,找不到CSP2P标签");
                }
                // 数据包类型
                string type = protocalHandler.GetElementTextByTag("type");
                if (type == null)
                {
                    throw new MyProtocalException(
                              "不是有效的1次确认,找不到type标签");
                }
                if (type.Equals("closesocket"))     // 对方要求关闭Socket
                {
                    rcvSocket.Close();
                }
                // 对方用户名
                string targetNameBase64 =
                    protocalHandler.GetElementTextByTag("name");
                string targetName =
                    protocalHandler.Base64stringToString(targetNameBase64);
                if (targetName == null)
                {
                    throw new MyProtocalException(
                              "不是有效的1次确认," +
                              "找不到name标签");
                }
                if (type.Equals("init_chat_request"))   // 私聊
                {
                    P2PChatClient socketHandler =
                        new P2PChatClient(this, rcvSocket);
                    socketHandler.newSocketChatNameReceived(protocalHandler);
                }
                else if (type.Equals("init_gp_request"))     // 群聊
                {
                    P2PGroupClient socketHandler =
                        new P2PGroupClient(this, rcvSocket);
                    socketHandler.newSocketGroupNamesReceived(protocalHandler);
                }
                else
                {
                    throw new MyProtocalException(
                              "不是有效的1次确认," +
                              "type不为init_chat_request或init_gp_request");
                }
                // 记录对方地址
                if (remoteIPaddress.ContainsKey(targetName))
                {
                    remoteIPaddress[targetName] =
                        new IPInfo((IPEndPoint)rcvSocket.RemoteEndPoint);
                }
                else
                {
                    remoteIPaddress.Add(targetName,
                                        new IPInfo((IPEndPoint)rcvSocket.RemoteEndPoint));
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine("异常位置:" +
                                "firstAckReceivedEventHandler");
                Trace.WriteLine(ex.Message);
                // 直接关闭socket
                try
                {
                    rcvSocket.Close();
                }
                catch (Exception ex2)
                {
                    Trace.WriteLine("异常位置:" +
                                    "firstAckReceivedEventHandler出错关闭rcvSocket时");
                    Trace.WriteLine(ex2.Message);
                }
            }
        }