Время на прочтение5 мин
Количество просмотров19K
Эта статья является продолжением моей статьи Простейший кросcплатформенный сервер с поддержкой ssl.
Поэтому для того, чтобы читать дальше очень желательно прочитать хотя бы часть предыдущей статьи. Но если не хочется, то вот краткое содержание: я взял из исходников OpenSSL файл-пример «serv.cpp» и сделал из него простейший кроссплатформенный сервер, который умеет принимать от клиента один символ.
Теперь я хочу пойти дальше и заставить сервер:
1. Принять от браузера весь http заголовок.
2. Отправить браузеру html страницу на которую будет выведен http заголовок.
3. Кроме этого, я хочу чтобы сокеты не блокировали процесс сервера и для этого я переведу их в так называемый «неблокирующий режим».
Для начала мне понадобится модифицированный в предыдущей статье файл serv.cpp.
Первое, что нужно сделать — написать кроссплатформенные макросы для перевода сокетов в неблокирующий режим:
для этого строки кода
#ifndef WIN32
#define closesocket close
#endif
меняем на следующие:
#ifdef WIN32
#define SET_NONBLOCK(socket) \
if (true) \
{ \
DWORD dw = true; \
ioctlsocket(socket, FIONBIO, &dw); \
}
#else
#include <fcntl.h>
#define SET_NONBLOCK(socket) \
if (fcntl( socket, F_SETFL, fcntl( socket, F_GETFL, 0 ) | O_NONBLOCK ) < 0) \
printf("error in fcntl errno=%i\n", errno);
#define closesocket(socket) close(socket)
#endif
Готово! Теперь, чтобы перевести «слушающий» сокет в неблокирующий режим, достаточно сразу после строки
listen_sd = socket (AF_INET, SOCK_STREAM, 0); CHK_ERR(listen_sd, "socket");
вставить строку:
SET_NONBLOCK(listen_sd);
Tеперь «слушающий» сокет неблокирующий и функция accept вернет управление программе сразу же после вызова.
Вместо дескриптора сокета accept теперь вернет значение (-1).
Таким образом, в неблокирующем режиме нам нужно вызывать функцию accept в бесконечном цикле, пока она не вернет дескриптор сокета
int sd = -1;
while(sd == -1)
{
Sleep(1);
#ifdef WIN32
sd = accept (listen_sd, (struct sockaddr*) &sa_cli, (int *)&client_len);
#else
sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len);
#endif
}
Чтобы программа не грузила на 100% процессор, я добавил в цикле Sleep(1). В Windows это означает перерыв на 1 миллисекунду. Чтобы это работало в Linux, добавьте в начале файла:
#ifndef WIN32
#define Sleep(a) usleep(a*1000)
#endif
Теоретически, вместо бесконечного цикла, можно с помощью функции select и ее более мощных аналогов, ждать пока сокет listen_sd станет доступен для чтения, а лишь потом один раз вызвать accept. Но лично я не вижу в моем способе с циклом никаких особых недостатков.
Итак, программа выйдет из цикла когда клиент подключится. Сокет sd в теории должен автоматически стать неблокирующим, но практика показывает, что для надежности лучше в конце цикла все-таки вызвать макрос
SET_NONBLOCK(sd);
Теперь, когда сокет для общения с клиентом неблокирующий, функция
err = SSL_accept (ssl);
не будет подвешивать процесс, а вернется сразу после вызова с значением err = SSL_ERROR_WANT_READ или SSL_ERROR_WANT_WRITE
чтобы принять зашифрованное сообщение, нам понадобится еще один бесконечный цикл:
while(1)
{
Sleep(1);
err = SSL_accept (ssl);
const int nCode = SSL_get_error(ssl, err);
if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE))
break;
}
CHK_SSL(err);
Лишь когда программа выйдет из этого цикла, можно быть уверенными, что зашифрованное соединение установлено и можно начинать прием и отправку сообщений.
Мы будем подключаться к серверу с помощью браузера, поэтому сообщения клиента состоят из http заголовка и тела запроса.
При этом http заголовок должен заканчиваться строкой «\r\n\r\n».
Исправим наш код так, чтобы сервер читал весь http заголовок, а не только его первую букву.
Для того, чтобы сократить код, я предлагаю воспользоваться замечательной библиотекой STL:
1. Добавим три заголовочных файла:
#include <vector>
#include <string>
#include <sstream>
2. Заменим строки
err = SSL_read (ssl, buf, sizeof(buf) - 1); CHK_SSL(err);
buf[err] = '\0';
printf ("Got %d chars:'%s'\n", err, buf);
на следующий код:
std::vector<unsigned char> vBuffer(4096); //выделяем буфер для входных данных
memset(&vBuffer[0], 0, vBuffer.size()); //заполняем буфер нулями
size_t nCurrentPos = 0;
while (nCurrentPos < vBuffer.size()-1)
{
err = SSL_read (ssl, &vBuffer[nCurrentPos], vBuffer.size() - nCurrentPos - 1); //читаем в цикле данные от клиента в буфер
if (err > 0)
{
nCurrentPos += err;
const std::string strInputString((const char *)&vBuffer[0]);
if (strInputString.find("\r\n\r\n") != -1) //Если найден конец http заголовка, то выходим из цикла
break;
continue;
}
const int nCode = SSL_get_error(ssl, err);
if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE))
break;
}
В этом цикле сервер читает данные от клиента до тех пор, пока не получит символы конца http заголовка «\r\n\r\n», либо пока место в буфере не кончится.
Буфер мне удобно выделять как std::vector хотя бы потому, что не нужно отдельной переменной для запоминания его длины.
После выхода из цикла в буфере должен храниться весь http заголовок и, возможно, часть тела запроса.
3. Отправим браузеру html страницу, в которую напишем http заголовок его запроса.
Заменим строку
err = SSL_write (ssl, "I hear you.", strlen("I hear you.")); CHK_SSL(err);
на следующий код:
//Преобразуем буфер в строку для удобства
const std::string strInputString((const char *)&vBuffer[0]);
//Формируем html страницу с ответом сервера
const std::string strHTML =
"<html><body><h2>Hello! Your HTTP headers is:</h2><br><pre>" +
strInputString.substr(0, strInputString.find("\r\n\r\n")) +
"</pre></body></html>";
//Добавляем в начало ответа http заголовок
std::ostringstream strStream;
strStream <<
"HTTP/1.1 200 OK\r\n"
<< "Content-Type: text/html; charset=utf-8\r\n"
<< "Content-Length: " << strHTML.length() << "\r\n" <<
"\r\n" <<
strHTML.c_str();
//Цикл для отправки ответа клиенту.
nCurrentPos = 0;
while(nCurrentPos < strStream.str().length())
{
err = SSL_write (ssl, strStream.str().c_str(), strStream.str().length());
if (err > 0)
{
nCurrentPos += err;
continue;
}
const int nCode = SSL_get_error(ssl, err);
if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE))
break;
}
Поскольку сокеты у нас неблокирующие то нет гарантии, что ответ отправится полностью с первого раза. Поэтому нужно вызывать SSL_write в цикле.
Вот и все. Теперь можно запустить наш сервер, а в браузере набрать localhost:1111
В ответ браузер покажет страницу со своим http запросом.
Проект для Visual Studio 2012 в архиве 3_.3s3s.org.
Чтобы скомпилировать под Linux, скопируйте из архива файлы «ca-cert.pem» и «serv.cpp» в один каталог и запустите компилятор: «g++ -L/usr/lib -lssl -lcrypto serv.cpp»
ПС: написал продолжение этой статьи
Данная версия документа является черновиком, и по большей части является переводом с небольшими поправками и добавлениями. Выкладывается с целью услышать критику, замечания и пожелания, которые помогут в доработке и вывода в свет конечного документа.
Здесь пойдет речь о использовании моделей ввода — вывода в программировании Winsock приложений. Winsock предостовляет возможность управления режимами и модели ввода — вывода сокета, для того чтоб определить как операции ввода — вывода будут обработанны. Режим сокета в сущности определяет поведение вызванных Winsock функций. Модель ввода — вывода в свою очередь определяет как приложение будет обрабатывать операции ввода — вывода для определённого сокета.
Winsock предостовляет два режима для сокетов: блокирующий и неблокирующий, а также несколько интересных моделей ввода — вывода которые помогают приложениям в управлении оерациями ввода — вывода нескольких сокетов одновременно асинхронным способом: блокирование, select, WSAAsyncSelect, WSAEventSelect, перекрытый ввод — вывод (overlapped I/O), и порт завершения (completion port). Все Windows платформы предоставляют блокирующий и неблокирующий режим работы для сокетов. И всеже не все модели ввода — вывода доступны на всех платформах. Следующая таблица показывает доступность моделей на разных Windows платформах.
Таблица 1. Доступные Модели ввода — вывода сокетов | ||||||
Платформа | Блокирующий | Неблокирующий Select | WSAAsync Select | WSAEvent Select | Over-lapped | Completion Port |
Windows CE | Да | Да | Нет | Нет | Нет | Нет |
Windows 95(Winsock 1) | Да | Да | Да | Нет | Нет | Нет |
Windows 95(Winsock 2) | Да | Да | Да | Да | Да | Нет |
Windows 98 | Да | Да | Да | Да | Да | Нет |
Windows МЕ | Да | Да | Да | Да | Да | Нет |
Windows NT | Да | Да | Да | Да | Да | Да |
Windows 2000 | Да | Да | Да | Да | Да | Да |
Windows XP | Да | Да | Да | Да | Да | Да |
1. Режимы сокетов
Как мы уже упомянали, Windows сокеты могут выполнять операции ввода — вывода в двух режимах: блокирующий и неблокирующий. В блокирующем режиме вызовы Winsock функций которые выполняют операции ввода — вывода, таких как send и recv — ждут пока операция завершится прежде чем отдать управление приложению. В неблокирующем режиме Winsock функции отдают управление приложению сразу. Приложения которые работают на Windows CE и Windows 95 (Winsock 1) платформах, которые потдерживают только некоторые модели ввода — вывода, вынуждены выполнять оперделенные дейвсвия с блокирующими и неблокирующими сокетами для коректной отработки разных ситуаций.
1.1. Блокирующий режим
Блокирующие сокеты создают некоторые неудобства, потомуч то вызов любой из Winsock API функций блокируют на некоторое время. Большинство Winsock приложений следуют модели «производитель — потребитель» в которой приложение считывает либо записывает определенное количество байт и выполняет их обработку. Следующий отрывок кода иллюстрирует эту модель:
SOCKET sock;
char buffer[256];
int done = 0,
err;
while(!done)
{
// прием данных
err = recv(sock, buffer, sizeof (buffer));
if (err == SOCKET_ERROR)
{
// отработка ошибки приема
printf(«recv failed with error %dn»,
WSAGetLastError());
return;
}
// обработка данных
ProcessReceivedData(buffer);
}
Проблема в приведенном коде состоит в том, что функция recv может никогда не отдать управление приложению если на данный сокет не прийдут какието данные. Некоторые прграммисты проверяют наличие ожидающих данных на сокете вызовом тогоже recv с флагом MSG_PEEK либо ioctlsocket с FIONREAD опцией. Проверка наличия ожидающих данных на сокете без ихнего приема считается в прогаммировании плохим тоном, это надо избегать любой ценной чтением данных из системного буфера. Для избежания этого метода, мы должны не дать приложению полностью застыть из-за отсутсвия ожидающих данных без вызова проверки наличия таковых. Решением данной проблемы может быть разделить приложение на два потока: читаюший и обрабатывающий данные, оба разделяя общий буфер данных. Доступ к которому осуществляется синхронизирующим обьектом как событие(event) или мьютекс(mutex). Задача читающего потока состоит в считвыании поступающих данных из сети на сокет в общий буфер. Когда читающий поток считал минимальное необходимое количество данных преднозначенных для обрабатывающего потока, он переключает событие в отсигналенное состояние, таким образом давая знать обрабатывающему потоку о наличии в общем буфере данных для обработки. Обрабатывающий поток в свою очередь забирает из буфера данные и обрабатывает их.
Следующий кусок кода показывает реализацию данного метода, реализуя две функции: одна обеспечивая чтение данных из сети (ReadingThread), другая обработку данных(ProcessingThread):
#define MAX_BUFFER_SIZE 4096
// Инициализация critical section
// и события с автосбросом перед инициализацией потоков
CRITICAL_SECTION data;
HANDLE hEvent;
SOCKET sock;
CHAR buffer[MAX_BUFFER_SIZE];
// создание читающего сокета
// читающий поток
void ReadingThread(void)
{
int nTotal = 0,
nRead = 0,
nLeft = 0,
nBytes = 0;
while (true)
{
nTotal = 0;
nLeft = NUM_BYTES_REQUIRED;
while (nTotal < NUM_BYTES_REQUIRED)
{
EnterCriticalSection(&data);
nRead = recv(sock, &(buffer[MAX_BUFFER_SIZE —
nBytes]), nLeft, 0);
if (nRead == -1)
{
printf(«errorn»);
ExitThread();
}
nTotal += nRead;
nLeft -= nRead;
nBytes += nRead;
LeaveCriticalSection(&data);
}
SetEvent(hEvent);
}
}
// обрабатывающий поток
void ProcessingThread(void)
{
while (true)
{
// ждем данных
WaitForSingleObject(hEvent);
EnterCriticalSection(&data);
DoSomeComputationOnData(buffer);
// удаляем из буфера обработанные данные
nBytes -= NUM_BYTES_REQUIRED;
LeaveCriticalSection(&data);
}
}
Основная трудность в програмировании блокирующих сокетов состоит в подержке передачи — приёма данных для более одного сокета. Используя предыдущую реализацию, приложение должно быть изменено для того чтоб иметь по одной паре читающего и обрабатывающего потока на каждый сокет. Это добавляет некоторую рутинную работу для програмиста и осложнение кода. Единственный недостаток состоит в том, что приложение плохо маштабируется при большом количестве сокетов.
1.2. Неблокирующий режим
Альтернативой блокирующим сокетам является неблокирующие. Неблокирующие сокеты являются более перспективными, но ихнее приемущество над блокирующими не велико. Следующий пример показывет как создать сокет и переключить его в неблокирующий режим:
SOCKET sock;
unsigned long nb = 1;
int err;
sock = socket(AF_INET, SOCK_STREAM, 0);
err = ioctlsocket(sock, FIONBIO, (unsigned long *) &nb);
if (err == SOCKET_ERROR)
{
//ошибка при переключении сокета в неблокирующий режим
}
После переключения сокета в неблокирующий режим, вызовы Winsock API связанные с приемом, передачей данных либо управлением соединений будут сразу возвращять управление прилоежению, не ожидая завершения текущей операции. В большинстве случаев данные вызовы возвращают ошибку типа WSAEWOULDBLOCK, что означает, что операция не имела времени закончится в период вызова функции. К примеру функция recv вернет WSAEWOULDBLOCK если нет ожидающих данных в системном буфере для данного сокета. Часто нужны дополнительные вызовы функции пока она не вернет сообшение об удачном завершение операции. Следующая таблица описывает значение WSAEWOULDBLOCK при вызове разных Winsock API функций:
Описание WSAEWOULDBLOCK ошибки для неблокирующих сокетов | |
Имя функции | Описание |
WSAAccept и accept | Нет запросов на установление связи, вызовите опять для провеки наличия запросов. |
closesocket | В большинстве случаев, это означает что setsockopt была вызвана с опцией SO_LINGER отличной от нуля тайм аут был установлен. |
WSAConnect и connect | Установка связи начата. вызовите снова, чтобы проверить завершение операции. |
WSARecv, recv, WSARecvFrom, и recvfrom | Данные не были получены. Проверьте снова позже. |
WSASend, send, WSASendTo, и sendto | Нет места в системном буфере отсылаемых данных. Пробуйте снова позже. |
Потому как большинство неблокирующих вызовов функции терпят неудачу с ошибкой WSAEWOULDBLOCK, вы должны проверять все коды возвратов и быть готовыми к неудачному вызову в любое время. Многие программисты совершают большую ошибку все время вызывая функцию пока она не вернет удачный код возврата. К примеру постоянный вызов recv в цикле в ожидании прочтения 100 байт данных ничем не лучше чем вызов recv в блокирующем режиме с параметром MSG_PEEK. Winsock модели ввода — вывода могут помоч приложению, определьть когда сокет готов к чтению, либо передаче данных.
Каждый из режимов — блокирующий и неблокирующий — имеют свои недостатки и преимущества. Блокирующие сокеты более легки в использовании с концептуальной точки зрения, но есть затруднения в управлении большого количества соединений, либо когда передаются данные разных обьемов и в разные периоды времени. С другой стороны неблокирующие сокеты более сложны, так как существует необходимость в написание более сложного кода для управления возможностью приема кодов возврата типа WSAEWOULDBLOCK при каждом вызове Winsock API функций. Сокетные модели ввода — вывода помогают приложению справится с управлением передачей данных на одном или более соединений одновременно асинхронным способом.
Автор: ixania
Что значит блокирующие и неблок. Socket?
, Ну как мне программировать под бл. или небло. ?
|
|
Не как с головы не уходит прочитал заголовок |
s-mike |
|
Arazel, неплохо было бы почитать элементарные справочники по сокетам, чем сразу на форум идти. Точно об этом написано у Марко Кэнту. Сообщение отредактировано: Rouse_ — |
Rouse_ |
|
Moderator Рейтинг (т): 320 |
Когда то давно я уже обьяснял — чтож, повторюсь пожалуй Блокирующий режим: Неблокирующий режим: Какой из режимов применять, каждый выбирает по себе. Не хочешь работать с потоками — работай с семафорами (Event). Не хочешь работать с ними — тогда работай через потоки Я обычно придерживаюсь Winsock расширения сокетов Беркли под названием «Асинхронный режим сетевого транспорта». |
Oleg2004 |
|
Arazel Сообщение отредактировано: Oleg2004 — |
Introduction:
In the realm of network programming with C, mastering the intricacies of socket operations is paramount. Among the fundamental concepts in this domain are blocking and non-blocking sockets, which significantly influence the behavior and performance of networked applications. In this comprehensive guide, we delve into the nuanced differences between blocking and non-blocking sockets, explore their respective advantages and disadvantages, and provide practical examples to illustrate their usage in C programming.
Blocking Sockets:
Blocking sockets, also known as synchronous sockets, adhere to a straightforward paradigm: I/O operations halt the execution of the program until they are completed. When you read from or write to a blocking socket, your program will pause until data is available to be read or the write operation finishes. This synchronous behavior simplifies the flow of the program, making it intuitive for developers, especially those new to network programming.
Key characteristics of blocking sockets include:
Blocking Behavior: I/O operations block the program’s execution until they conclude.
Synchronous Operation: Operations are performed in a synchronous manner, meaning the program waits until each operation finishes before proceeding.
Simplicity: Blocking sockets offer simplicity and ease of understanding, making them an attractive choice for beginners in network programming.
However, the simplicity of blocking sockets comes at a cost. Consider a scenario where a blocking socket is used to communicate with multiple clients simultaneously. If one client’s operation takes an unexpectedly long time to complete, it may block the entire program, potentially causing delays in serving other clients.
To illustrate, let’s consider a basic example of using blocking sockets in a TCP client-server application:
Server (TCP Server)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define MAX_PENDING_CONNECTIONS 5
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from server";
// Create socket file descriptor
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// Set server address parameters
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// Bind the socket to the specified port
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// Listen for incoming connections
if (listen(server_fd, MAX_PENDING_CONNECTIONS) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// Accept incoming connection
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
// Read client message
read(new_socket, buffer, BUFFER_SIZE);
printf("Client message: %s\n", buffer);
// Send response to client
send(new_socket, message, strlen(message), 0);
printf("Response sent to client.\n");
// Close sockets
close(new_socket);
close(server_fd);
return 0;
}
Enter fullscreen mode
Exit fullscreen mode
Client (TCP Client)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define SERVER_ADDRESS "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from client";
// Create socket file descriptor
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// Set server address parameters
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Convert IPv4 and IPv6 addresses from text to binary form
if (inet_pton(AF_INET, SERVER_ADDRESS, &serv_addr.sin_addr) <= 0) {
perror("invalid address / address not supported");
exit(EXIT_FAILURE);
}
// Connect to server
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
// Send message to server
send(sock, message, strlen(message), 0);
printf("Message sent to server.\n");
// Read response from server
read(sock, buffer, BUFFER_SIZE);
printf("Server response: %s\n", buffer);
// Close socket
close(sock);
return 0;
}
Enter fullscreen mode
Exit fullscreen mode
Non-blocking Sockets:
In contrast to blocking sockets, non-blocking sockets operate asynchronously. When an I/O operation is initiated on a non-blocking socket, the program continues its execution immediately, regardless of whether the operation succeeds or not. This asynchronous behavior allows the program to perform other tasks while waiting for I/O operations to complete, enhancing overall efficiency and responsiveness.
Key characteristics of non-blocking sockets include:
Non-blocking Behavior: I/O operations return immediately, even if they cannot be completed immediately.
Asynchronous Operation: Operations are performed asynchronously, enabling the program to continue executing without waiting for each operation to finish.
Increased Complexity: Non-blocking sockets introduce additional complexity into the program logic, as it needs to handle situations where operations may not complete immediately.
While non-blocking sockets offer improved responsiveness and better resource utilization, they require careful handling of asynchronous events. Developers must implement mechanisms to manage the asynchronous nature of non-blocking sockets effectively, such as employing event loops or using multiplexing techniques like select() or poll().
Let’s examine a practical example demonstrating the use of non-blocking sockets in a TCP client-server application:
Server (Non-blocking TCP Server)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#define PORT 8080
#define MAX_PENDING_CONNECTIONS 5
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from server";
// Create socket file descriptor
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// Set server address parameters
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// Bind the socket to the specified port
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// Set the server socket to non-blocking mode
if (fcntl(server_fd, F_SETFL, O_NONBLOCK) < 0) {
perror("fcntl failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// Listen for incoming connections
if (listen(server_fd, MAX_PENDING_CONNECTIONS) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// Accept new connections
while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) >= 0) {
printf("New connection, socket fd is %d\n", new_socket);
// Send message to client
int bytes_sent = send(new_socket, message, strlen(message), 0);
if (bytes_sent < 0) {
perror("send failed");
}
close(new_socket); // Close the connection
}
// Handle non-blocking accept errors
if (new_socket < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
perror("accept failed");
break;
}
// Simulate some non-blocking work (e.g., checking timers, other tasks)
usleep(1000); // Sleep briefly to avoid busy-waiting
}
close(server_fd);
return 0;
}
Enter fullscreen mode
Exit fullscreen mode
Client (Non-blocking TCP Client)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#define PORT 8080
#define SERVER_ADDRESS "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from client";
// Create socket file descriptor
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// Set server address parameters
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Convert IPv4 and IPv6 addresses from text to binary form
if (inet_pton(AF_INET, SERVER_ADDRESS, &serv_addr.sin_addr) <= 0) {
perror("invalid address / address not supported");
exit(EXIT_FAILURE);
}
// Set the socket to non-blocking mode
if (fcntl(sock, F_SETFL, O_NONBLOCK) < 0) {
perror("fcntl failed");
exit(EXIT_FAILURE);
}
// Connect to server
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
// Non-blocking connect will return immediately
// Check errno to distinguish between connection in progress and connection failed
if (errno != EINPROGRESS) {
perror("connection failed");
exit(EXIT_FAILURE);
}
}
// Wait for connection to complete
sleep(1);
// Send message to server
send(sock, message, strlen(message), 0);
printf("Message sent to server.\n");
// Read response from server
read(sock, buffer, BUFFER_SIZE);
printf("Server response: %s\n", buffer);
// Close socket
close(sock);
return 0;
}
Enter fullscreen mode
Exit fullscreen mode
Conclusion:
In conclusion, understanding the distinctions between blocking and non-blocking sockets is essential for proficient network programming in C. While blocking sockets offer simplicity and straightforward operation, non-blocking sockets provide greater flexibility and efficiency by enabling asynchronous I/O operations. When selecting the appropriate socket mode for your application, consider the specific requirements, scalability, and performance constraints. With a solid grasp of blocking and non-blocking socket concepts, developers can architect robust and responsive networked applications tailored to their unique needs.
Прежде всего для использования 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» после чего закроет соединение.