18.07.2009

Простой однопоточный TCP сервер на Win Sockets.

Прежде всего для использования Winsock необходимо подключить заголовочный winsock2.h .
Так же для того чтобы проект слинковался без ошибок необходимо указать библиотеку wsock32.lib .

Подробный алгоритм программы
1. Установка обработчика консольного окна SetConsoleCtrlHandler();
2. Инициализация Win Socks WSAStartup();
3. Создание сокета сервера socket();
4. Установка сокета в неблокирующий режим ioctlsocket();
5. Привязывание сокета к адресу и порту bind();
6. Установка сокета в режим прослушивания listen();
7. Цикл работы сервера (см Алгоритм цикла работы сервера).
8. Закрытие прослушивающего сокета closesocket();
9. Закрытие открытых клиентских сокетов shutdown(); + closesocket();
10. Деинициализаци WniSocks WSACleanup();
11. Снятие обработчика консольного окна SetConsoleCtrlHandler();

Алгоритм цикла работы сервера
1. Принятие подключений от клиентов accept();
2. Добавление вновь подключенных клиентов в общий спиок.
3. Получение данных от подключенных клиентов recv();
4. Закрытие сокетов с ошибкой либо отключившихся shutdown(); + closesocket();
5. Удаление отключенных клиентских сокетов из общего списка.
6. Обработка данных полученных от клиентов.
7. Отсылка данных клиентам send();
8. Закрытие сокетов с ошибкой либо отключившихся shutdown(); + closesocket();
9. Удаление отключенных клиентских сокетов из общего списка.
10. Ожидание (даем время остальным процессам и потокам) Sleep();
11. Выход из цикла если сервер заканчивает работу иначе переход к шагу 1.

Что такое обработчик консольного окна и зачем он нужен в этой программе
Так как наше приложение является однопоточным сервером, то при закрытии консольного окна клиенты подключенные к серверу могут зависнуть на некоторое время, так как не клиентские сокеты не были закрыты. Получается ситуация что клиенты ждут ответа сервера не зная что сервер завершил свою работу (либо просто "упал"). Поэтому мы сделаем так что при закрытии консольного окна все клиенты будут корректно отключены, произойдет деинициализация Winsocks и т.д (Корректное завершение). Затем произойдет выход из программы.

Инициализировать/деинициализация Winsocks
Для инициализации и деинициализации используются функции WSAStartup() и WSACleanup() соответственно. Если в программе не был инициализирован Winsocks то все функции Winsocks будут завершаться с ошибкой а WSAGetLastError() вернет WSANOTINITIALISED. Проще говоря инициализация Winsocks обязательна если вам нужна сеть будь то ваше приложение клиентом или сервером.

Создание серверного (прослушивающего) сокета
Сокет создается функцией socket().
Установка сокета в неблокирующий режим функцией ioctlsocket().
Привязка сокета к интерфейсу (конкретному доступному локальному IP адресу) и порту bind().
Установка сокета в режим прослушивания listen().

Что такое блокирующие и не блокирующие сокеты
При использовании блокирующих сокетов многие функции как бы "зависают" и не дают исполнятся программе до тех пор пока они не получат конкретный результат либо ошибку.
К примеру при выполнении функции recv() если клиент не посылал данные то сервер будет ожидать когда клиент их пришлет либо пока клиент отключится. Соответственно если было подключено несколько других клиентов то они тоже будут ждать пока не произойдет завершение функции recv(). Для решения данной проблемы применяется многопоточность (для каждого клиента выделяется отдельный поток) либо при помощи функции select().
Не блокирующие же сокеты в случае если операция не может завершиться сразу (например еще не пришло никаких данных от клиента при вызове recv()) просто возвращается с ошибкой. При этом эта ошибка возвращаемая функцией WSAGetLastError() устанавливается в значение WSAWOULDBLOCK.

Почему использованы неблокирующие сокеты
Неблокирующие сокеты использованы специально для того чтобы приложение было однопоточным и максимально простым, но тем не менее функциональным. Новички смогут разобраться без вникания в то, что такое многопоточность и как происходит синхронизация между потоками.

Подключение клиентов
После того как создали серверный сокет клиенты уже могут подключаться к серверу. Все подключенные клиенты будут помещаться в очередь. Функция accept() вернет дескриптор сокета клиента и удалит его из очереди. Через него наше приложение и будет общаться с конкретным клиентом. Так же accept() вернет данные о клиенте IP адрес и порт с которого произошло подключение, эту информацию к примеру можно использовать для отключения неугодных клиентов (забаненных IP арресов).

Работа с подключенными клиентами
После того как клиент подключен можно посылать данные либо получать от него. Посылают данные клиенту при помощи функции send(). При этом функция возвращает количество посланных байт и если это значение отличается от заданного нами то придется остатки послать позже. Если же функция вернет SOCKET_ERROR то необходимо проверить код ошибки при помощи WSAGetLastError().
Функция recv() получает данные от подключенного клиента. Функция возвращает количество полученных байт, но не более чем размер буффера для данных. Так же функция возвращает SOCKET_ERROR в случае ошибке (ошибку можно получить функцией WSAGetLastError()). Если же функция recv() возвращает значение 0 это означает что клиент решил закрыть подключение (вызвал shutdown с SD_SEND). После этого (если к примеру положено по протоколу) он может дополнительно послать данные (принять от клиента уже ничего не получится) и закрыть сокет.
Приложение в примере к статье при появлении ошибки отличной от WSAEWOULDBLOCK при вызовах функций send() и recv() считает что клиент отключился и закрывает сокет. Однако лучше всего обрабатывать эти ошибки так как некоторые из них могут быть не критические.

Закрытие сокетов
"Неправильное" закрытие сокета
Сокет можно закрыть просто функцией closesocket(), при этом противоположная сторона при выполнении функции recv() получит сообщение об ошибке ECONNRESET.
"Правильное" закрытие сокета (graceful close).
Если же перед закрытием используется функция shutdown() с параметром SD_SEND то при выполнении функции recv() программа не получит ошибки но функция вернет значение 0. Рекомендуется использовать именно "правильное закрытие", хотя при "Неправильном" закрытии никаких утечек системных ресурсов не происходит.

Приложение пример
Открывает на всех интерфейсах порт 8000. К серверу можно подключиться например используя браузер (http://127.0.0.1:8000, http://localhost:8000 либо http://<ваш ип адрес>:8000 ). При подключении сервер сгенерирует простейшую HTML страницу с текстом "test" после чего закроет соединение.