В сети есть много примеров клиент-серверных приложений как по протоколу TCP, так и по протоколу UDP. Проблема в том, что все они предполагают общение между двумя компьютерами с публичными ip-адресами или внутри интрасети:
Рис.1 Интрасеть
Рис.2 Интернет + публичные ip-адреса
В этой статье рассмотрим передачу текстовой информации по протоколу UDP и передачу файлов по протоколу TCP в случае, когда клиентский компьютер находится за НАТом
Рис.3 Клиентский компьютер находится за НАТом
Что будем делать?
Сделаем файловый сервер с возможностью редактировать список доступных для передачи файлов и клиент, получающий список доступных файлов с возможностью загрузить выбранный файл. Запрос списка и сам список будет передаваться от клиента серверу и от сервера клиенту по протоколу UDP. Запрос файла и сам файл будем передавать по протоколу TCP. Сделаем также, чтобы сервер запоминал своих клиентов и при обновлении списка файлов передавал его клиенту или клиентам (в случае, если клиентов несколько). Клиент постоянно поддерживает связь с сервером путём отправки пустых сообщений.
Схема сети, где будем это всё отлаживать
Рис.4 Схема сети для тестирования
То есть, в роли сервера будет реальная машина с ip 192.168.222.1, в роли клиента - виртуальная машина с ip 192.168.100.101, находящаяса за NAT во внутренней сети 192.168.100.0/24 реальной машины с ip 192.168.222.2. Сервер слушает порты UDP 0.0.0.0:10024 и TCP 0.0.0.0:10025.
Проблемы, с которыми столкнёмся: NAT не хранит таблицу трансляции адресов вечно, так что надо каким-то образом её поддерживать, не давая NAT'у удалить нужную нам запись. Самый простой способ - каждые несколько секунд отправлять от клиента пустые сообщения серверу.
Сервер
Слушаем UDP. При получении команды "list" (NetService.FILESLISTCOMMAND), отдаем список файлов в виде XML на тот ip-адрес и порт, с которого пришел запрос
///<summary>
/// Прослушка интерфейса по протоколу UDP
///</summary>
private void ListenUDP(CancellationToken cancelToken)
{
IPEndPoint ipEP;
UdpClient client = null;
try
{
ipEP = new IPEndPoint(IPAddress.Any, NetService.SERVERUDPPORT);
client = new UdpClient(ipEP);
while (true)
{
cancelToken.ThrowIfCancellationRequested();
IPEndPoint ipep = null;
byte[] messageBytes = client.Receive(ref ipep);
IPAddress clientIp = ipep.Address;
int clientPort = ipep.Port;
// запомним ип и порт с которого пришел запрос, понадобится для отправки многоадресных сообщений
if (!this.udpClients.ContainsKey(clientIp))
this.udpClients.Add(clientIp, clientPort);
else
this.udpClients[clientIp] = clientPort;
System.Diagnostics.Debug.WriteLine("{0:hh:mm:ss} NetService.ListenUDP => message from {1}:{2}", DateTime.Now, clientIp, clientPort);
string message = Encoding.UTF8.GetString(messageBytes);
if (message == NetService.PUSHCOMMAND) // поддержка связи
continue;
if (message == NetService.FILESLISTCOMMAND) // запрос списка файлов
Task.Factory.StartNew(() => NetService.Instance.SendMessage(clientIp, clientPort, FileService.Instance.GetFilesList()));
}
}
catch (OperationCanceledException) { }
catch (SocketException ex)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.ListenUDP => Exception: {1}", DateTime.Now, ex.Message));
Environment.Exit(1);
}
finally
{
if (client != null)
client.Close();
}
}
Слушаем TCP. Сообщение, читаемое из потока ( NetworkStream ) - имя файла. Проверяем его наличие в списке доступных файлов и, в случае если файл в списке есть, пишем в поток сам файл
///<summary>
/// Прослушка интерфейса по протоколу TCP
///</summary>
private void ListenTCP(CancellationToken cancelToken)
{
TcpClient client = null;
try
{
while (true)
{
cancelToken.ThrowIfCancellationRequested();
client = this.tcpListener.AcceptTcpClient(); // приём запроса
NetworkStream s = client.GetStream();
// получаем имя запрошенного файла
byte[] buffer = new byte[256];
int fileNameLength = s.Read(buffer, 0, buffer.Length);
string fileName = System.Text.Encoding.UTF8.GetString(buffer, 0, fileNameLength);
string filePath = string.Empty;
if (string.IsNullOrWhiteSpace(fileName))
continue;
// проверяем, есть ли в списке доступных файлов запрошенный
FileItem fi = FileService.Instance.Files.FirstOrDefault(x => x.Name == fileName);
if (fi != null)
filePath = fi.Path;
if (!string.IsNullOrEmpty(filePath))
{ // файл есть, отдаём
using (var fileIO = File.OpenRead(filePath))
{
//s.Write(BitConverter.GetBytes(fileIO.Length), 0, 8);
s.Write(BitConverter.GetBytes(fileIO.Length), 0, fileIO.Length.ToString().Length);
buffer = new byte[1024 * 8];
int count;
while ((count = fileIO.Read(buffer, 0, buffer.Length)) > 0)
s.Write(buffer, 0, count);
}
}
s.Close();
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.ListenTCP => Exception: {1}", DateTime.Now, ex.Message));
Environment.Exit(1);
}
finally
{
if (client != null)
client.Close();
}
}
Отправка сообщения по протоколу UDP
///<summary>
/// Отправить сообщение по протоколу UDP
///</summary>
///<param name="clientIp">IP-адрес получателя</param>
///<param name="clientPort">Порт получателя</param>
///<param name="message">Сообщение</param>
public void SendMessage(IPAddress clientIp, int clientPort, string message)
{
UdpClient client = new UdpClient();
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IPEndPoint ipep = new IPEndPoint(clientIp, clientPort);
client.Send(messageBytes, messageBytes.Length, ipep);
}
Клиент
Запрос списка файлов
///<summary>
/// Запрос списка файлов
///</summary>
public void RequestList()
{
if (this.serverIp == null)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.RequestList: не установлен IP-адрес сервера", DateTime.Now));
return;
}
IPEndPoint ipep = new IPEndPoint(this.serverIp, NetService.SERVERUDPPORT);
// если это первое сообщение, запустим сначала прослушку портов
if (this.udpClient.Client == null)
this.udpClient = new UdpClient();
if (this.udpClient.Client.LocalEndPoint == null)
{
byte[] pushCmdBytes = Encoding.UTF8.GetBytes(NetService.PUSHCOMMAND);
this.udpClient.Send(pushCmdBytes, pushCmdBytes.Length, ipep);
Task.Factory.StartNew(() => this.ListenUDP(this.cancelTokenSource.Token, this.udpClient));
Task.Factory.StartNew(() => this.HoldUDPConnection());
}
byte[] messageBytes = Encoding.UTF8.GetBytes(NetService.FILESLISTCOMMAND);
this.udpClient.Send(messageBytes, messageBytes.Length, ipep);
}
Немного пояснений:
this.udpClient.Send(pushCmdBytes, pushCmdBytes.Length, ipep) - система автоматически выбирает неиспользуемый в данный момент порт для отправки сообщения
this.ListenUDP(this.cancelTokenSource.Token, this.udpClient) - начинаем прослушку по данному порту, ответ будет приходить на него
this.HoldUDPConnection() - каждые 20 секунд отправляем пустые сообщения серверу для поддержания записи в таблице трансляции адресов NAT
Слушаем UDP. При получении сообщения - вызываем событие OnMessageReceived
///<summary>
/// Прослушка интерфейса по протоколу UDP
///</summary>
///<param name="client">UDP client</param>
private void ListenUDP(CancellationToken cancelToken, UdpClient client)
{
try
{
while (true)
{
cancelToken.ThrowIfCancellationRequested();
IPEndPoint ipep = null;
byte[] messageBytes = client.Receive(ref ipep);
IPAddress senderIp = ipep.Address; // ip отправителя
string message = Encoding.UTF8.GetString(messageBytes);
if (!string.IsNullOrWhiteSpace(message))
{
if (this.OnMessageReceived != null)
this.OnMessageReceived.Invoke(message);
}
}
}
catch (OperationCanceledException) { }
catch (SocketException ex)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.Listen: {1}", DateTime.Now, ex.Message));
}
finally
{
if (client != null)
client.Close();
}
}
Поддерживаем связь с сервером
///<summary>
/// Отправка UDP PUSH сообщений серверу для поддержки связи
///</summary>
private void HoldUDPConnection()
{
byte[] messageBytes = Encoding.UTF8.GetBytes(NetService.PUSHCOMMAND);
while (true)
{
Thread.Sleep(20000);
if (udpClient.Client == null)
break; // соединение разорвано
IPEndPoint ipep = new IPEndPoint(this.serverIp, NetService.SERVERUDPPORT);
this.udpClient.Send(messageBytes, messageBytes.Length, ipep);
}
}
Запрос файла
///<summary>
/// Запрос файла
///</summary>
///<param name="fileName">Имя файла</param>
public void RequestFile(string fileName)
{
if (this.serverIp == null)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.RequestFile: не установлен IP-адрес сервера", DateTime.Now));
return;
}
string filePath = Path.Combine(new DirectoryInfo(App.FILESPATH).FullName, fileName);
System.Net.IPEndPoint serverEP = new IPEndPoint(this.serverIp, NetService.SERVERTCPPORT);
byte[] messageBytes = Encoding.UTF8.GetBytes(fileName);
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(serverEP);
NetworkStream s = new NetworkStream(socket);
s.Write(messageBytes, 0, messageBytes.Length);
Int64 bytesReceived = 0;
int count;
byte[] buffer = new byte[1024 * 8];
s.Read(buffer, 0, 1024);
Int64 numberOfBytes = BitConverter.ToInt64(buffer, 0);
using (var fileIO = File.Create(filePath))
{
while(bytesReceived < numberOfBytes && (count = s.Read(buffer, 0, buffer.Length)) > 0)
{
fileIO.Write(buffer, 0, count);
bytesReceived += count;
}
}
socket.Close();
if (this.OnFileReceived != null)
this.OnFileReceived.Invoke(filePath);
}
Интерфейс серверной части
Интерфейс клиентской части
Полный код серверной части
///<summary>
/// Сервис для работы с сетью
///</summary>
public class NetService
{
#region Свойства
private static int SERVERUDPPORT = 10024;
private static int SERVERTCPPORT = 10025;
private static string FILESLISTCOMMAND = "list";
private static string PUSHCOMMAND = "push";
private static NetService instance;
private static object syncRoot = new object();
private Dictionary<IPAddress, int> udpClients; // клиенты текстовых сообщений
private CancellationTokenSource cancelTokenSource;
private TcpListener tcpListener;
#endregion
#region Конструктор
private NetService()
{
this.udpClients = new Dictionary<IPAddress, int>();
this.StartListen();
}
#endregion
#region Методы
///<summary>
/// Начать прослушку интерфесов
///</summary>
private void StartListen()
{
this.cancelTokenSource = new CancellationTokenSource();
Task.Factory.StartNew(() => this.ListenUDP(this.cancelTokenSource.Token), this.cancelTokenSource.Token);
this.tcpListener = new TcpListener(IPAddress.Any, NetService.SERVERTCPPORT);
this.tcpListener.Start();
for (int i = 0; i < 5; i++) // принимаем до 5 входящих подключений одновременно
Task.Factory.StartNew(() => this.ListenTCP(this.cancelTokenSource.Token), this.cancelTokenSource.Token);
}
///<summary>
/// Прослушка интерфейса по протоколу UDP
///</summary>
private void ListenUDP(CancellationToken cancelToken)
{
IPEndPoint ipEP;
UdpClient client = null;
try
{
ipEP = new IPEndPoint(IPAddress.Any, NetService.SERVERUDPPORT);
client = new UdpClient(ipEP);
while (true)
{
cancelToken.ThrowIfCancellationRequested();
IPEndPoint ipep = null;
byte[] messageBytes = client.Receive(ref ipep);
IPAddress clientIp = ipep.Address;
int clientPort = ipep.Port;
// запомним ип и порт с которого пришел запрос, понадобится для отправки многоадресных сообщений
if (!this.udpClients.ContainsKey(clientIp))
this.udpClients.Add(clientIp, clientPort);
else
this.udpClients[clientIp] = clientPort;
System.Diagnostics.Debug.WriteLine("{0:hh:mm:ss} NetService.ListenUDP => message from {1}:{2}", DateTime.Now, clientIp, clientPort);
string message = Encoding.UTF8.GetString(messageBytes);
if (message == NetService.PUSHCOMMAND) // поддержка связи
continue;
if (message == NetService.FILESLISTCOMMAND) // запрос списка файлов
Task.Factory.StartNew(() => NetService.Instance.SendMessage(clientIp, clientPort, FileService.Instance.GetFilesList()));
}
}
catch (OperationCanceledException) { }
catch (SocketException ex)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.ListenUDP => Exception: {1}", DateTime.Now, ex.Message));
Environment.Exit(1);
}
finally
{
if (client != null)
client.Close();
}
}
///<summary>
/// Прослушка интерфейса по протоколу TCP
///</summary>
private void ListenTCP(CancellationToken cancelToken)
{
TcpClient client = null;
try
{
while (true)
{
cancelToken.ThrowIfCancellationRequested();
client = this.tcpListener.AcceptTcpClient(); // приём запроса
NetworkStream s = client.GetStream();
// получаем имя запрошенного файла
byte[] buffer = new byte[256];
int fileNameLength = s.Read(buffer, 0, buffer.Length);
string fileName = System.Text.Encoding.UTF8.GetString(buffer, 0, fileNameLength);
string filePath = string.Empty;
if (string.IsNullOrWhiteSpace(fileName))
continue;
// проверяем, есть ли в списке доступных файлов запрошенный
FileItem fi = FileService.Instance.Files.FirstOrDefault(x => x.Name == fileName);
if (fi != null)
filePath = fi.Path;
if (!string.IsNullOrEmpty(filePath))
{ // файл есть, отдаём
using (var fileIO = File.OpenRead(filePath))
{
//s.Write(BitConverter.GetBytes(fileIO.Length), 0, 8);
s.Write(BitConverter.GetBytes(fileIO.Length), 0, fileIO.Length.ToString().Length);
buffer = new byte[1024 * 8];
int count;
while ((count = fileIO.Read(buffer, 0, buffer.Length)) > 0)
s.Write(buffer, 0, count);
}
}
s.Close();
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.ListenTCP => Exception: {1}", DateTime.Now, ex.Message));
Environment.Exit(1);
}
finally
{
if (client != null)
client.Close();
}
}
#endregion
#region Общие свойства
///<summary>
/// NetService Instance
///</summary>
public static NetService Instance
{
get
{
if (NetService.instance == null)
lock (NetService.syncRoot)
if (NetService.instance == null)
NetService.instance = new NetService();
return NetService.instance;
}
}
#endregion
#region Общие методы
///<summary>
/// Отправить сообщение всем известным адресатам по протоколу UDP
///</summary>
///<param name="message">Сообщение</param>
public void SendMessage(string message)
{
foreach (KeyValuePair<IPAddress, int> client in this.udpClients)
this.SendMessage(client.Key, client.Value, message);
}
///<summary>
/// Отправить сообщение по протоколу UDP
///</summary>
///<param name="clientIp">IP-адрес получателя</param>
///<param name="clientPort">Порт получателя</param>
///<param name="message">Сообщение</param>
public void SendMessage(IPAddress clientIp, int clientPort, string message)
{
UdpClient client = new UdpClient();
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IPEndPoint ipep = new IPEndPoint(clientIp, clientPort);
client.Send(messageBytes, messageBytes.Length, ipep);
}
///<summary>
/// Остановить все процессы прослушки интерфейсов
///</summary>
public void Cancel()
{
if (this.cancelTokenSource != null)
this.cancelTokenSource.Cancel();
}
#endregion
}
Полный код клиентской части
///<summary>
/// Сервис для работы с сетью
///</summary>
public class NetService
{
#region Свойства
private static int SERVERUDPPORT = 10024;
private static int SERVERTCPPORT = 10025;
private static string FILESLISTCOMMAND = "list";
private static string PUSHCOMMAND = "push";
private static NetService instance;
private static object syncRoot = new object();
private CancellationTokenSource cancelTokenSource; // для остановки всех процессов
private IPAddress serverIp; // ip-адрес сервера
private UdpClient udpClient; // udp-клиент
private Status connectionStatus = Status.NoConnection; // состояние подключения к серверу
#endregion
#region Конструктор
private NetService()
{
this.udpClient = new UdpClient();
this.cancelTokenSource = new CancellationTokenSource();
// проверка связи до сервера
Task.Factory.StartNew(() => this.CheckConnection());
}
#endregion
#region Методы
///<summary>
/// Прослушка интерфейса по протоколу UDP
///</summary>
///<param name="client">UDP client</param>
private void ListenUDP(CancellationToken cancelToken, UdpClient client)
{
try
{
while (true)
{
cancelToken.ThrowIfCancellationRequested();
IPEndPoint ipep = null;
byte[] messageBytes = client.Receive(ref ipep);
IPAddress senderIp = ipep.Address; // ip отправителя
string message = Encoding.UTF8.GetString(messageBytes);
if (!string.IsNullOrWhiteSpace(message))
{
if (this.OnMessageReceived != null)
this.OnMessageReceived.Invoke(message);
}
}
}
catch (OperationCanceledException) { }
catch (SocketException ex)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.Listen: {1}", DateTime.Now, ex.Message));
}
finally
{
if (client != null)
client.Close();
}
}
///<summary>
/// Проверка связи до сервера
///</summary>
private void CheckConnection()
{
Ping ping = new Ping();
while (true)
{
Thread.Sleep(5000);
if (this.serverIp == null)
continue;
try
{
PingReply pr = ping.Send(this.serverIp);
if (pr.Status == IPStatus.Success)
{
if (this.connectionStatus != Status.Connected)
{
this.connectionStatus = Status.Connected;
this.OnStatusChanged.Invoke(new StatusChangedEventArgs(this.connectionStatus));
}
}
else
{
if (this.connectionStatus != Status.NoConnection)
{
this.connectionStatus = Status.NoConnection;
this.OnStatusChanged.Invoke(new StatusChangedEventArgs(this.connectionStatus));
}
}
}
catch (PingException)
{
if (this.connectionStatus != Status.NoConnection)
{
this.connectionStatus = Status.NoConnection;
this.OnStatusChanged.Invoke(new StatusChangedEventArgs(this.connectionStatus));
}
}
}
}
///<summary>
/// Отправка UDP PUSH сообщений серверу для поддержки связи
///</summary>
private void HoldUDPConnection()
{
byte[] messageBytes = Encoding.UTF8.GetBytes(NetService.PUSHCOMMAND);
while (true)
{
Thread.Sleep(20000);
if (udpClient.Client == null)
break; // соединение разорвано
IPEndPoint ipep = new IPEndPoint(this.serverIp, NetService.SERVERUDPPORT);
this.udpClient.Send(messageBytes, messageBytes.Length, ipep);
}
}
#endregion
#region Общие свойства
///<summary>
/// NetService Instance
///</summary>
public static NetService Instance
{
get
{
if (NetService.instance == null)
lock (NetService.syncRoot)
if (NetService.instance == null)
NetService.instance = new NetService();
return NetService.instance;
}
}
///<summary>
/// IP-адрес сервера
///</summary>
public IPAddress ServerIp { get { return this.serverIp; } }
public delegate void NetMessageReceivedEventHandler(string message);
///<summary>
/// Возникает при получении сообщения
///</summary>
public event NetMessageReceivedEventHandler OnMessageReceived;
public delegate void NetFileReceivedEventHandler(string filePath);
///<summary>
/// Возникает при получении файла
///</summary>
public event NetFileReceivedEventHandler OnFileReceived;
public delegate void StatusChangedEventHandler(StatusChangedEventArgs e);
///<summary>
/// При изменении статуса
///</summary>
public event StatusChangedEventHandler OnStatusChanged;
#endregion
#region Общие методы
///<summary>
/// Устанавливает IP-адрес сервера
///</summary>
///<param name="ip">IP-адрес</param>
public void SetServerIp(IPAddress ip)
{
this.udpClient.Close();
this.connectionStatus = Status.NoConnection;
this.OnStatusChanged.Invoke(new StatusChangedEventArgs(this.connectionStatus));
this.serverIp = ip;
this.udpClient = new UdpClient();
}
///<summary>
/// Запрос списка файлов
///</summary>
public void RequestList()
{
if (this.serverIp == null)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.RequestList: не установлен IP-адрес сервера", DateTime.Now));
return;
}
IPEndPoint ipep = new IPEndPoint(this.serverIp, NetService.SERVERUDPPORT);
// если это первое сообщение, запустим сначала прослушку портов
if (this.udpClient.Client == null)
this.udpClient = new UdpClient();
if (this.udpClient.Client.LocalEndPoint == null)
{
byte[] pushCmdBytes = Encoding.UTF8.GetBytes(NetService.PUSHCOMMAND);
this.udpClient.Send(pushCmdBytes, pushCmdBytes.Length, ipep);
Task.Factory.StartNew(() => this.ListenUDP(this.cancelTokenSource.Token, this.udpClient));
Task.Factory.StartNew(() => this.HoldUDPConnection());
}
byte[] messageBytes = Encoding.UTF8.GetBytes(NetService.FILESLISTCOMMAND);
this.udpClient.Send(messageBytes, messageBytes.Length, ipep);
}
///<summary>
/// Запрос файла
///</summary>
///<param name="fileName">Имя файла</param>
public void RequestFile(string fileName)
{
if (this.serverIp == null)
{
System.Diagnostics.Debug.WriteLine(string.Format("{0:hh:mm:ss} NetService.RequestFile: не установлен IP-адрес сервера", DateTime.Now));
return;
}
string filePath = Path.Combine(new DirectoryInfo(App.FILESPATH).FullName, fileName);
System.Net.IPEndPoint serverEP = new IPEndPoint(this.serverIp, NetService.SERVERTCPPORT);
byte[] messageBytes = Encoding.UTF8.GetBytes(fileName);
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(serverEP);
NetworkStream s = new NetworkStream(socket);
s.Write(messageBytes, 0, messageBytes.Length);
Int64 bytesReceived = 0;
int count;
byte[] buffer = new byte[1024 * 8];
s.Read(buffer, 0, 1024);
Int64 numberOfBytes = BitConverter.ToInt64(buffer, 0);
using (var fileIO = File.Create(filePath))
{
while(bytesReceived < numberOfBytes && (count = s.Read(buffer, 0, buffer.Length)) > 0)
{
fileIO.Write(buffer, 0, count);
bytesReceived += count;
}
}
socket.Close();
if (this.OnFileReceived != null)
this.OnFileReceived.Invoke(filePath);
}
///<summary>
/// Остановить все процессы прослушки интерфейсов
///</summary>
public void Cancel()
{
if (this.cancelTokenSource != null)
this.cancelTokenSource.Cancel();
}
#endregion
#region OnStatusChanged event
public class StatusChangedEventArgs
{
///<summary>
/// Статус подключения
///</summary>
public Status Status { get; set; }
public StatusChangedEventArgs(Status status)
{
this.Status = status;
}
}
public enum Status
{
NoConnection,
Connected
}
#endregion
}
Проект клиента, сервера и скомпилированные приложения находятся во вложении.