В сети есть много примеров клиент-серверных приложений как по протоколу 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'у удалить нужную нам запись. Самый простой способ - каждые несколько секунд отправлять от клиента пустые сообщения серверу.