Triển khai giao thức Udp đáng tin cậy cho .Net

Internet đã thay đổi từ lâu. Một trong những giao thức chính của Internet - UDP được các ứng dụng sử dụng không chỉ để phân phát các gói dữ liệu và quảng bá mà còn cung cấp các kết nối "ngang hàng" giữa các nút mạng. Do thiết kế đơn giản, giao thức này có nhiều mục đích sử dụng không có kế hoạch trước đây, tuy nhiên, những thiếu sót của giao thức, chẳng hạn như thiếu phân phối được đảm bảo, vẫn chưa biến mất ở bất cứ đâu. Bài viết này mô tả việc triển khai giao thức phân phối được đảm bảo qua UDP.
Содержание:Nhập
Yêu cầu giao thức
Tiêu đề UDP đáng tin cậy
Nguyên tắc chung của giao thức
Thời gian chờ và bộ hẹn giờ giao thức
Sơ đồ trạng thái truyền dẫn UDP đáng tin cậy
Đi sâu hơn vào mã. bộ điều khiển truyền
Đi sâu hơn vào mã. Những trạng thái

Đi sâu hơn vào mã. Tạo và thiết lập kết nối
Đi sâu hơn vào mã. Đóng kết nối khi hết thời gian chờ
Đi sâu hơn vào mã. Khôi phục truyền dữ liệu
API UDP đáng tin cậy
Kết luận
Các liên kết và bài viết hữu ích

Nhập

Kiến trúc ban đầu của Internet giả định một không gian địa chỉ đồng nhất, trong đó mỗi nút có một địa chỉ IP toàn cầu và duy nhất và có thể giao tiếp trực tiếp với các nút khác. Trên thực tế, Internet giờ đây có một kiến ​​​​trúc khác - một khu vực địa chỉ IP toàn cầu và nhiều khu vực có địa chỉ riêng ẩn sau các thiết bị NAT.Trong kiến ​​trúc này, chỉ những thiết bị trong không gian địa chỉ toàn cầu mới có thể dễ dàng giao tiếp với bất kỳ ai trên mạng vì chúng có một địa chỉ IP duy nhất, có thể định tuyến toàn cầu. Một nút trên mạng riêng có thể kết nối với các nút khác trên cùng một mạng và cũng có thể kết nối với các nút nổi tiếng khác trong không gian địa chỉ toàn cầu. Sự tương tác này đạt được phần lớn nhờ cơ chế dịch địa chỉ mạng. Các thiết bị NAT, chẳng hạn như bộ định tuyến Wi-Fi, tạo các mục nhập bảng dịch đặc biệt cho các kết nối gửi đi và sửa đổi địa chỉ IP cũng như số cổng trong các gói. Điều này cho phép các kết nối gửi đi từ mạng riêng đến các máy chủ trong không gian địa chỉ toàn cầu. Nhưng đồng thời, các thiết bị NAT thường chặn tất cả lưu lượng đến trừ khi các quy tắc riêng cho các kết nối đến được đặt.

Kiến trúc này của Internet đủ chính xác để giao tiếp giữa máy khách và máy chủ, nơi máy khách có thể ở trong mạng riêng và máy chủ có địa chỉ toàn cầu. Nhưng nó gây khó khăn cho việc kết nối trực tiếp hai nút giữa đa dạng các mạng riêng. Kết nối trực tiếp giữa hai nút rất quan trọng đối với các ứng dụng ngang hàng như truyền giọng nói (Skype), giành quyền truy cập từ xa vào máy tính (TeamViewer) hoặc chơi trò chơi trực tuyến.

Một trong những phương pháp hiệu quả nhất để thiết lập kết nối ngang hàng giữa các thiết bị trên các mạng riêng khác nhau được gọi là đục lỗ. Kỹ thuật này được sử dụng phổ biến nhất với các ứng dụng dựa trên giao thức UDP.

Nhưng nếu ứng dụng của bạn cần phân phối dữ liệu được đảm bảo, chẳng hạn như bạn truyền tệp giữa các máy tính, thì việc sử dụng UDP sẽ gặp nhiều khó khăn do UDP không phải là giao thức phân phối được đảm bảo và không cung cấp phân phối gói theo thứ tự, không giống như TCP giao thức.

Trong trường hợp này, để đảm bảo phân phối gói được đảm bảo, cần triển khai giao thức lớp ứng dụng cung cấp chức năng cần thiết và hoạt động trên UDP.

Tôi muốn lưu ý ngay rằng có một kỹ thuật đục lỗ TCP để thiết lập kết nối TCP giữa các nút trong các mạng riêng khác nhau, nhưng do nhiều thiết bị NAT không hỗ trợ kỹ thuật này nên nó thường không được coi là cách chính để kết nối các nút như vậy.

Trong phần còn lại của bài viết này, tôi sẽ chỉ tập trung vào việc triển khai giao thức phân phối đảm bảo. Việc triển khai kỹ thuật đục lỗ UDP sẽ được mô tả trong các bài viết sau.

Yêu cầu giao thức

  1. Phân phối gói đáng tin cậy được thực hiện thông qua cơ chế phản hồi tích cực (cái gọi là xác nhận tích cực)
  2. Nhu cầu chuyển dữ liệu lớn hiệu quả, tức là giao thức phải tránh chuyển tiếp gói không cần thiết
  3. Có thể hủy cơ chế xác nhận giao hàng (khả năng hoạt động như một giao thức UDP "thuần túy")
  4. Khả năng thực hiện chế độ lệnh, với xác nhận của từng thông báo
  5. Đơn vị truyền dữ liệu cơ bản qua giao thức phải là một thông điệp

Các yêu cầu này phần lớn trùng khớp với các yêu cầu của Giao thức dữ liệu đáng tin cậy được mô tả trong rfc908 и rfc1151và tôi đã dựa vào các tiêu chuẩn đó khi phát triển giao thức này.

Để hiểu các yêu cầu này, chúng ta hãy xem xét thời gian truyền dữ liệu giữa hai nút mạng bằng giao thức TCP và UDP. Hãy để trong cả hai trường hợp, chúng tôi sẽ có một gói bị mất.
Truyền dữ liệu không tương tác qua TCP:Triển khai giao thức Udp đáng tin cậy cho .Net

Như bạn có thể thấy từ sơ đồ, trong trường hợp mất gói, TCP sẽ phát hiện gói bị mất và báo cáo cho người gửi bằng cách hỏi số của phân đoạn bị mất.
Truyền dữ liệu qua giao thức UDP:Triển khai giao thức Udp đáng tin cậy cho .Net

UDP không thực hiện bất kỳ bước phát hiện mất mát nào. Kiểm soát lỗi truyền trong giao thức UDP hoàn toàn do ứng dụng chịu trách nhiệm.

Phát hiện lỗi trong giao thức TCP đạt được bằng cách thiết lập kết nối với nút cuối, lưu trữ trạng thái của kết nối đó, cho biết số byte được gửi trong mỗi tiêu đề gói và thông báo biên nhận bằng số xác nhận.

Ngoài ra, để cải thiện hiệu suất (nghĩa là gửi nhiều hơn một phân đoạn mà không nhận được xác nhận), giao thức TCP sử dụng cái gọi là cửa sổ truyền - số byte dữ liệu mà người gửi phân đoạn mong muốn nhận được.

Để biết thêm thông tin về giao thức TCP, xem rfc793, từ UDP đến rfc768trong đó, trên thực tế, chúng được xác định.

Từ những điều trên, rõ ràng để tạo ra một giao thức gửi tin nhắn đáng tin cậy qua UDP (sau đây gọi tắt là UDP đáng tin cậy), nó được yêu cầu thực hiện các cơ chế truyền dữ liệu tương tự như TCP. Cụ thể là:

  • lưu trạng thái kết nối
  • sử dụng đánh số phân đoạn
  • sử dụng các gói xác nhận đặc biệt
  • sử dụng cơ chế cửa sổ đơn giản hóa để tăng thông lượng giao thức

Ngoài ra, bạn cần:

  • báo hiệu bắt đầu một tin nhắn, để phân bổ tài nguyên cho kết nối
  • báo hiệu kết thúc một tin nhắn, để chuyển tin nhắn đã nhận tới ứng dụng ngược dòng và giải phóng tài nguyên giao thức
  • cho phép giao thức dành riêng cho kết nối tắt cơ chế xác nhận gửi để hoạt động như UDP "thuần túy"

Tiêu đề UDP đáng tin cậy

Nhớ lại rằng một datagram UDP được đóng gói trong một datagram IP. Gói UDP đáng tin cậy được "gói" một cách thích hợp vào một datagram UDP.
Đóng gói tiêu đề UDP đáng tin cậy:Triển khai giao thức Udp đáng tin cậy cho .Net

Cấu trúc của Reliable UDP header khá đơn giản:

Triển khai giao thức Udp đáng tin cậy cho .Net

  • Cờ - cờ kiểm soát gói
  • MessageType - loại thông báo được sử dụng bởi các ứng dụng ngược dòng để đăng ký các thông báo cụ thể
  • TransmissionId - số truyền, cùng với địa chỉ và cổng của người nhận, xác định duy nhất kết nối
  • PacketNumber - số gói tin
  • Tùy chọn - tùy chọn giao thức bổ sung. Trong trường hợp của gói đầu tiên, nó được sử dụng để chỉ ra kích thước của tin nhắn

Cờ như sau:

  • FirstPacket - gói tin đầu tiên
  • NoAsk - thông báo không yêu cầu bật cơ chế xác nhận
  • LastPacket - gói cuối cùng của tin nhắn
  • RequestForPacket - gói xác nhận hoặc yêu cầu gói bị mất

Nguyên tắc chung của giao thức

Vì UDP đáng tin cậy tập trung vào việc truyền thông báo được đảm bảo giữa hai nút nên nó phải có khả năng thiết lập kết nối với phía bên kia. Để thiết lập kết nối, người gửi sẽ gửi một gói có cờ FirstPacket, phản hồi có nghĩa là kết nối được thiết lập. Tất cả các gói phản hồi, hay nói cách khác, các gói xác nhận, luôn đặt giá trị của trường Số gói thành nhiều hơn giá trị Số gói lớn nhất của các gói đã nhận thành công. Trường Tùy chọn cho gói đầu tiên được gửi là kích thước của tin nhắn.

Một cơ chế tương tự được sử dụng để chấm dứt kết nối. Cờ LastPacket được đặt trên gói tin cuối cùng. Trong gói phản hồi, số của gói cuối cùng + 1 được chỉ định, đối với bên nhận có nghĩa là gửi tin nhắn thành công.
Sơ đồ thiết lập và kết thúc kết nối:Triển khai giao thức Udp đáng tin cậy cho .Net

Khi kết nối được thiết lập, quá trình truyền dữ liệu sẽ bắt đầu. Dữ liệu được truyền trong các khối gói. Mỗi khối, ngoại trừ khối cuối cùng, chứa một số gói cố định. Nó bằng với kích thước cửa sổ nhận/truyền. Khối dữ liệu cuối cùng có thể có ít gói hơn. Sau khi gửi từng khối, bên gửi chờ xác nhận gửi hoặc yêu cầu gửi lại các gói bị mất, để mở cửa sổ nhận/truyền để nhận phản hồi. Sau khi nhận được xác nhận gửi khối, cửa sổ nhận/truyền sẽ thay đổi và khối dữ liệu tiếp theo được gửi.

Bên nhận nhận các gói tin. Mỗi gói được kiểm tra xem nó có nằm trong cửa sổ truyền hay không. Các gói và bản sao không rơi vào cửa sổ sẽ được lọc ra. Bởi vì Nếu kích thước của cửa sổ là cố định và giống nhau đối với người nhận và người gửi, thì trong trường hợp một khối gói được gửi mà không bị mất, cửa sổ sẽ được dịch chuyển để nhận các gói của khối dữ liệu tiếp theo và xác nhận gửi là đã gửi. Nếu cửa sổ không đầy trong khoảng thời gian do bộ hẹn giờ làm việc đặt, thì quá trình kiểm tra xem gói nào chưa được gửi sẽ được bắt đầu và yêu cầu gửi lại sẽ được gửi.
Sơ đồ truyền lại:Triển khai giao thức Udp đáng tin cậy cho .Net

Thời gian chờ và bộ hẹn giờ giao thức

Có một số lý do tại sao một kết nối không thể được thiết lập. Ví dụ: nếu bên nhận đang ngoại tuyến. Trong trường hợp này, khi cố gắng thiết lập kết nối, kết nối sẽ bị đóng khi hết thời gian chờ. Việc triển khai UDP đáng tin cậy sử dụng hai bộ hẹn giờ để đặt thời gian chờ. Đầu tiên, bộ hẹn giờ làm việc, được sử dụng để chờ phản hồi từ máy chủ từ xa. Nếu nó kích hoạt ở phía người gửi, thì gói đã gửi cuối cùng sẽ được gửi lại. Nếu bộ hẹn giờ hết hạn ở người nhận, thì việc kiểm tra các gói bị mất sẽ được thực hiện và yêu cầu gửi lại được gửi.

Bộ đếm thời gian thứ hai là cần thiết để đóng kết nối trong trường hợp thiếu liên lạc giữa các nút. Đối với bên gửi, nó bắt đầu ngay sau khi hết giờ làm việc và chờ phản hồi từ nút từ xa. Nếu không có phản hồi trong khoảng thời gian đã chỉ định, kết nối sẽ bị ngắt và tài nguyên được giải phóng. Đối với bên nhận, bộ hẹn giờ đóng kết nối được bắt đầu sau khi bộ hẹn giờ làm việc hết hạn hai lần. Điều này là cần thiết để đảm bảo không bị mất gói xác nhận. Khi bộ đếm thời gian hết hạn, kết nối cũng bị ngắt và tài nguyên được giải phóng.

Sơ đồ trạng thái truyền dẫn UDP đáng tin cậy

Các nguyên tắc của giao thức được thực hiện trong một máy trạng thái hữu hạn, mỗi trạng thái chịu trách nhiệm cho một logic xử lý gói nhất định.
Sơ đồ trạng thái UDP đáng tin cậy:

Triển khai giao thức Udp đáng tin cậy cho .Net

Đóng - không thực sự là một trạng thái, nó là điểm bắt đầu và điểm kết thúc của máy tự động. cho nhà nước Đóng một khối điều khiển truyền được nhận, khối này triển khai máy chủ UDP không đồng bộ, chuyển tiếp các gói đến các kết nối thích hợp và bắt đầu xử lý trạng thái.

Gói đầu tiênGửi – trạng thái ban đầu của kết nối gửi đi khi tin nhắn được gửi đi.

Ở trạng thái này, gói đầu tiên cho các tin nhắn bình thường được gửi đi. Đối với các tin nhắn không có xác nhận gửi, đây là trạng thái duy nhất mà toàn bộ tin nhắn được gửi.

Chu kỳ gửi – trạng thái cơ bản để truyền các gói tin.

Chuyển sang nó từ trạng thái Gói đầu tiênGửi được thực hiện sau khi gói tin đầu tiên được gửi đi. Tất cả các báo nhận và yêu cầu truyền lại đều ở trạng thái này. Có thể thoát khỏi nó trong hai trường hợp - trong trường hợp gửi tin nhắn thành công hoặc khi hết thời gian chờ.

Gói tin đầu tiên đã nhận – trạng thái ban đầu cho người nhận tin nhắn.

Nó kiểm tra tính chính xác của việc bắt đầu truyền, tạo các cấu trúc cần thiết và gửi xác nhận đã nhận gói đầu tiên.

Đối với một tin nhắn bao gồm một gói duy nhất và được gửi mà không sử dụng bằng chứng gửi, đây là trạng thái duy nhất. Sau khi xử lý một thông báo như vậy, kết nối sẽ bị đóng.

Lắp ráp – trạng thái cơ bản để nhận các gói tin nhắn.

Nó ghi các gói vào bộ lưu trữ tạm thời, kiểm tra xem có mất gói không, gửi xác nhận cho việc gửi một khối gói và toàn bộ tin nhắn, đồng thời gửi yêu cầu gửi lại các gói bị mất. Trong trường hợp nhận thành công toàn bộ tin nhắn, kết nối sẽ chuyển sang trạng thái Hoàn thành, nếu không, sẽ hết thời gian chờ.

Hoàn thành – đóng kết nối trong trường hợp nhận thành công toàn bộ tin nhắn.

Trạng thái này là cần thiết để lắp ráp tin nhắn và trong trường hợp xác nhận gửi tin nhắn bị thất lạc trên đường đến người gửi. Trạng thái này được thoát khi hết thời gian chờ, nhưng kết nối được coi là đã đóng thành công.

Đi sâu hơn vào mã. bộ điều khiển truyền

Một trong những yếu tố chính của UDP đáng tin cậy là khối điều khiển truyền dẫn. Nhiệm vụ của khối này là lưu trữ các kết nối hiện tại và các phần tử phụ trợ, phân phối các gói đến cho các kết nối tương ứng, cung cấp giao diện để gửi các gói đến kết nối và triển khai API giao thức. Khối điều khiển truyền nhận các gói từ lớp UDP và chuyển tiếp chúng đến máy trạng thái để xử lý. Để nhận các gói, nó triển khai một máy chủ UDP không đồng bộ.
Một số thành viên của lớp ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Triển khai máy chủ UDP không đồng bộ:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Đối với mỗi lần truyền tin nhắn, một cấu trúc được tạo có chứa thông tin về kết nối. Một cấu trúc như vậy được gọi là hồ sơ kết nối.
Một số thành viên của lớp ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

Đi sâu hơn vào mã. Những trạng thái

Các trạng thái triển khai máy trạng thái của giao thức UDP đáng tin cậy, nơi diễn ra quá trình xử lý chính các gói. Lớp trừu tượng ReliableUdpState cung cấp giao diện cho trạng thái:

Triển khai giao thức Udp đáng tin cậy cho .Net

Toàn bộ logic của giao thức được thực hiện bởi các lớp được trình bày ở trên, cùng với một lớp phụ trợ cung cấp các phương thức tĩnh, chẳng hạn như xây dựng tiêu đề ReliableUdp từ bản ghi kết nối.

Tiếp theo, chúng tôi sẽ xem xét chi tiết việc triển khai các phương thức giao diện xác định các thuật toán cơ bản của giao thức.

Phương thức DisposeByTimeout

Phương thức DisposeByTimeout chịu trách nhiệm giải phóng tài nguyên kết nối sau khi hết thời gian chờ và báo hiệu gửi tin nhắn thành công/không thành công.
Đáng tin cậyUdpState.DisposeByTimeout:

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;      
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

Nó chỉ bị ghi đè trong trạng thái Hoàn thành.
Đã hoàn thành.DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

phương thức ProcessPackets

Phương thức ProcessPackets chịu trách nhiệm xử lý bổ sung một gói hoặc nhiều gói. Được gọi trực tiếp hoặc thông qua bộ đếm thời gian chờ gói.

Trong điều kiện Lắp ráp phương thức bị ghi đè và chịu trách nhiệm kiểm tra các gói bị mất và chuyển sang trạng thái Hoàn thành, trong trường hợp nhận được gói cuối cùng và vượt qua kiểm tra thành công
Lắp ráp.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Trong điều kiện Chu kỳ gửi phương pháp này chỉ được gọi trên bộ hẹn giờ và chịu trách nhiệm gửi lại tin nhắn cuối cùng, cũng như kích hoạt bộ hẹn giờ đóng kết nối.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Trong điều kiện Hoàn thành phương pháp dừng bộ đếm thời gian đang chạy và gửi tin nhắn đến người đăng ký.
Đã hoàn thành.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Phương thức nhận gói

Trong điều kiện Gói tin đầu tiên đã nhận nhiệm vụ chính của phương pháp là xác định xem gói tin nhắn đầu tiên có thực sự đến giao diện hay không và cũng để thu thập một tin nhắn bao gồm một gói tin duy nhất.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Trong điều kiện Chu kỳ gửi phương pháp này được ghi đè để chấp nhận xác nhận gửi và yêu cầu truyền lại.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Trong điều kiện Lắp ráp trong phương thức ReceivePacket, công việc chính là tập hợp một thông báo từ các gói tin đến sẽ diễn ra.
Lắp ráp.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

Trong điều kiện Hoàn thành nhiệm vụ duy nhất của phương thức là gửi lại xác nhận về việc gửi tin nhắn thành công.
Đã hoàn thành.Nhận gói tin:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Phương thức gửi gói tin

Trong điều kiện Gói đầu tiênGửi phương thức này sẽ gửi gói dữ liệu đầu tiên hoặc toàn bộ tin nhắn nếu tin nhắn không yêu cầu xác nhận gửi.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Trong điều kiện Chu kỳ gửi trong phương thức này, một khối gói tin được gửi đi.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Đi sâu hơn vào mã. Tạo và thiết lập kết nối

Bây giờ chúng ta đã thấy các trạng thái cơ bản và các phương thức được sử dụng để xử lý các trạng thái, hãy chia nhỏ một vài ví dụ về cách thức hoạt động của giao thức một cách chi tiết hơn.
Sơ đồ truyền dữ liệu trong điều kiện bình thường:Triển khai giao thức Udp đáng tin cậy cho .Net

Xem xét chi tiết việc tạo ra hồ sơ kết nối để kết nối và gửi gói tin đầu tiên. Quá trình chuyển luôn được bắt đầu bởi ứng dụng gọi API gửi tin nhắn. Tiếp theo, phương thức StartTransmission của khối điều khiển truyền được gọi, bắt đầu truyền dữ liệu cho thông báo mới.
Tạo kết nối gửi đi:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Gửi gói đầu tiên (trạng thái FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Sau khi gửi gói tin đầu tiên, bên gửi chuyển sang trạng thái Chu kỳ gửi – chờ xác nhận giao hàng trọn gói.
Phía nhận, sử dụng phương thức EndReceive, nhận gói đã gửi, tạo một gói mới hồ sơ kết nối và chuyển gói này, với tiêu đề được phân tích cú pháp trước, tới phương thức ReceivePacket của trạng thái để xử lý Gói tin đầu tiên đã nhận
Tạo kết nối ở bên nhận:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Nhận gói đầu tiên và gửi xác nhận (trạng thái FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Đi sâu hơn vào mã. Đóng kết nối khi hết thời gian chờ

Xử lý thời gian chờ là một phần quan trọng của UDP đáng tin cậy. Hãy xem xét một ví dụ trong đó một nút trung gian bị lỗi và việc phân phối dữ liệu theo cả hai hướng trở nên không thể.
Sơ đồ đóng kết nối khi hết thời gian chờ:Triển khai giao thức Udp đáng tin cậy cho .Net

Như có thể thấy từ sơ đồ, bộ đếm thời gian làm việc của người gửi bắt đầu ngay lập tức sau khi gửi một khối gói tin. Điều này xảy ra trong phương thức SendPacket của trạng thái Chu kỳ gửi.
Kích hoạt bộ đếm thời gian làm việc (trạng thái SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

Khoảng thời gian hẹn giờ được đặt khi kết nối được tạo. ShortTimerPeriod mặc định là 5 giây. Trong ví dụ, nó được đặt thành 1,5 giây.

Đối với một kết nối đến, bộ đếm thời gian bắt đầu sau khi nhận được gói dữ liệu đến cuối cùng, điều này xảy ra trong phương thức ReceivePacket của trạng thái Lắp ráp
Kích hoạt bộ hẹn giờ làm việc (Trạng thái lắp ráp):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Không có thêm gói nào đến trên kết nối đến trong khi chờ bộ đếm thời gian làm việc. Bộ đếm thời gian đã tắt và được gọi là phương thức ProcessPackets, nơi các gói bị mất được tìm thấy và các yêu cầu phân phối lại được gửi lần đầu tiên.
Đang gửi yêu cầu gửi lại (Trạng thái lắp ráp):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Biến TimerSecondTry được đặt thành đúng. Biến này chịu trách nhiệm khởi động lại bộ đếm thời gian làm việc.

Về phía người gửi, bộ đếm thời gian làm việc cũng được kích hoạt và gói đã gửi cuối cùng được gửi lại.
Kích hoạt hẹn giờ đóng kết nối (trạng thái SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Sau đó, bộ hẹn giờ đóng kết nối sẽ bắt đầu trong kết nối gửi đi.
Đáng tin cậyUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

Thời gian chờ của bộ hẹn giờ đóng kết nối theo mặc định là 30 giây.

Sau một thời gian ngắn, bộ hẹn giờ làm việc ở phía người nhận sẽ kích hoạt lại, yêu cầu được gửi lại, sau đó bộ hẹn giờ đóng kết nối bắt đầu cho kết nối đến

Khi bộ hẹn giờ đóng kích hoạt, tất cả tài nguyên của cả hai bản ghi kết nối sẽ được giải phóng. Người gửi báo cáo lỗi gửi cho ứng dụng ngược dòng (xem API UDP đáng tin cậy).
Giải phóng tài nguyên bản ghi kết nối:

public void Dispose()
{
  try
  {
    System.Threading.Monitor.Enter(this.LockerReceive);
  }
  finally
  {
    Interlocked.Increment(ref this.IsDone);
    if (WaitForPacketsTimer != null)
    {
      WaitForPacketsTimer.Dispose();
    }
    if (CloseWaitTimer != null)
    {
      CloseWaitTimer.Dispose();
    }
    byte[] stream;
    Tcb.IncomingStreams.TryRemove(Key, out stream);
    stream = null;
    Tcb.OutcomingStreams.TryRemove(Key, out stream);
    stream = null;
    System.Threading.Monitor.Exit(this.LockerReceive);
  }
}

Đi sâu hơn vào mã. Khôi phục truyền dữ liệu

Sơ đồ khôi phục đường truyền dữ liệu trong trường hợp mất gói tin:Triển khai giao thức Udp đáng tin cậy cho .Net

Như đã thảo luận trong phần đóng kết nối khi hết thời gian chờ, khi hết thời gian làm việc, bên nhận sẽ kiểm tra các gói bị mất. Trong trường hợp mất gói, một danh sách số lượng gói không đến được người nhận sẽ được tổng hợp. Những số này được nhập vào mảng LostPackets của một kết nối cụ thể và các yêu cầu gửi lại được gửi đi.
Gửi yêu cầu tới các gói phân phối lại (Trạng thái lắp ráp):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Người gửi sẽ chấp nhận yêu cầu gửi lại và gửi các gói bị thiếu. Điều đáng chú ý là tại thời điểm này, người gửi đã bắt đầu hẹn giờ đóng kết nối và khi nhận được yêu cầu, nó sẽ được đặt lại.
Gửi lại các gói bị mất (trạng thái SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Gói gửi lại (gói số 3 trong sơ đồ) được nhận bởi kết nối đến. Kiểm tra được thực hiện để xem liệu cửa sổ nhận đã đầy chưa và quá trình truyền dữ liệu bình thường đã được khôi phục chưa.
Kiểm tra các lần truy cập trong cửa sổ nhận (Trạng thái lắp ráp):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API UDP đáng tin cậy

Để tương tác với giao thức truyền dữ liệu, có một lớp Udp đáng tin cậy đang mở, lớp này là lớp bao bọc bên ngoài khối điều khiển truyền. Dưới đây là những thành viên quan trọng nhất của lớp:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

Tin nhắn được nhận bởi thuê bao. Chữ ký ủy quyền cho phương thức gọi lại:

public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );

Tin nhắn:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

Để đăng ký một loại tin nhắn cụ thể và/hoặc một người gửi cụ thể, hai tham số tùy chọn được sử dụng: ReliableUdpMessageTypes messageType và IPEndPoint ipEndPoint.

Các loại tin nhắn:

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

Tin nhắn được gửi không đồng bộ; đối với điều này, giao thức thực hiện một mô hình lập trình không đồng bộ:

public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)

Kết quả gửi tin nhắn sẽ là đúng - nếu tin nhắn đến được người nhận thành công và sai - nếu kết nối bị đóng do hết thời gian chờ:

public bool EndSendMessage(IAsyncResult asyncResult)

Kết luận

Nhiều điều đã không được mô tả trong bài viết này. Các cơ chế khớp luồng, xử lý ngoại lệ và lỗi, triển khai các phương thức gửi tin nhắn không đồng bộ. Nhưng cốt lõi của giao thức, mô tả logic để xử lý các gói, thiết lập kết nối và xử lý thời gian chờ, sẽ rõ ràng đối với bạn.

Phiên bản được chứng minh của giao thức phân phối đáng tin cậy đủ mạnh và linh hoạt để đáp ứng các yêu cầu đã xác định trước đó. Nhưng tôi muốn nói thêm rằng việc triển khai được mô tả có thể được cải thiện. Ví dụ: để tăng thông lượng và thay đổi linh hoạt các khoảng thời gian hẹn giờ, các cơ chế như cửa sổ trượt và RTT có thể được thêm vào giao thức, cũng sẽ hữu ích khi triển khai cơ chế xác định MTU giữa các nút kết nối (nhưng chỉ khi các tin nhắn lớn được gửi) .

Cảm ơn các bạn đã quan tâm theo dõi, rất mong nhận được ý kiến ​​đóng góp của các bạn.

PS Đối với những người quan tâm đến chi tiết hoặc chỉ muốn thử nghiệm giao thức, liên kết đến dự án trên GitHube:
Dự án UDP đáng tin cậy

Các liên kết và bài viết hữu ích

  1. Đặc tả giao thức TCP: bằng tiếng anh и ở Nga
  2. Đặc tả giao thức UDP: bằng tiếng anh и ở Nga
  3. Thảo luận về giao thức RUDP: dự thảo-ietf-sigtran-đáng tin cậy-udp-00
  4. Giao thức dữ liệu đáng tin cậy: rfc908 и rfc1151
  5. Một triển khai đơn giản xác nhận giao hàng qua UDP: Kiểm soát hoàn toàn mạng của bạn với .NET và UDP
  6. Bài viết mô tả cơ chế truyền tải NAT: Giao tiếp ngang hàng qua trình biên dịch địa chỉ mạng
  7. Triển khai mô hình lập trình không đồng bộ: Triển khai mô hình lập trình không đồng bộ CLR и Cách triển khai mẫu thiết kế IAsyncResult
  8. Chuyển mô hình lập trình không đồng bộ sang mẫu không đồng bộ dựa trên tác vụ (APM trong TAP):
    TPL và Lập trình không đồng bộ .NET truyền thống
    Tương tác với các kiểu và kiểu không đồng bộ khác

Cập nhật: Cảm ơn bạn thị trưởng и sidristij cho ý tưởng thêm một tác vụ vào giao diện. Khả năng tương thích của thư viện với các hệ điều hành cũ không bị vi phạm, bởi vì Khung thứ 4 hỗ trợ cả máy chủ XP và 2003.

Nguồn: www.habr.com

Thêm một lời nhận xét