Недавно у меня появилось немного свободного времени, поэтому я решил улучшить свои знания языка С. Пока что они ограничиваются уровнем простых институтских лабораторных работ и книгой С. Прата «Язык программирования C», поэтому для начала я поставил себе простую задачу — написать консольную змейку. К тому моменту, когда отображение игрового поля и самой змейки на экране и часть, отвечающая за передвижение ее по полю была написана, появилось две проблемы:
Можно было решить их при помощи использования getch() из curses, но это было скучно и не интересно.
О том как были решены эти проблемы под катом.
Для начала нужно было справиться со считыванием нажатия клавиш пользователя. Реализовано это у меня через вызов функции getc(), но при вызове этой функции программа начинала считывать символы, только после того, как был нажат Enter. В ходе недолго поиска было обнаружено, что это не проблема getc(), а проблема терминала Linux, точнее драйвера терминала, который по-умолчанию работает в каноническом режиме, т.е. это драйвер терминала ожидает окончания ввода строки нажатием Enter, после чего отправляет данную строку в stdin программы. Так же было найдено решение данной проблемы: перевод драйвера в неканонический режим. Реализуется это очень просто — подключается заголовочный файл termios.h и пишутся две функции:
Первая функция для перевода в неканонический режим:
Вторая функция для возвращения в первоначальное состояние:
Переменная stored_settings должна быть объявлена как глобальная:
Затем в начале программы вызывается первая функция, а в конце, соответственно, вызывается вторая.
В готовом виде тестовый пример выглядит так:
Теперь моя змейка умела считывать посимвольно пользовательский ввод. Но появилась вторая проблема: программа все равно останавливалась и ждала пока пользователь нажмет какую-либо клавишу. Снова начался поиск в интернете, в процессе которого я часто натыкался на «решение» этой проблеммы в виде того, что было описано выше. Результатом поиска стал совет использовать функцию select().
Из параметров, принимаемых этой функцией нас интересуют только n, readfds и utimeout:
Поэтому вызов select() будет выглядеть слудеющим образом:
Для указания, что события будут рассматриваться только для stdin нужно вызвать
Структура struct timeval определена следующим образом:
В итоге у нас получается вот такой тестовый пример (не забудем перевести терминал в неканонический режим, что уже было рассмотрено выше):
Использованный материал:
- данные отправлялись программе только после нажатия Enter
- программа останавливалась на считывании пользовательского ввода
Можно было решить их при помощи использования getch() из curses, но это было скучно и не интересно.
О том как были решены эти проблемы под катом.
Для начала нужно было справиться со считыванием нажатия клавиш пользователя. Реализовано это у меня через вызов функции getc(), но при вызове этой функции программа начинала считывать символы, только после того, как был нажат Enter. В ходе недолго поиска было обнаружено, что это не проблема getc(), а проблема терминала Linux, точнее драйвера терминала, который по-умолчанию работает в каноническом режиме, т.е. это драйвер терминала ожидает окончания ввода строки нажатием Enter, после чего отправляет данную строку в stdin программы. Так же было найдено решение данной проблемы: перевод драйвера в неканонический режим. Реализуется это очень просто — подключается заголовочный файл termios.h и пишутся две функции:
Первая функция для перевода в неканонический режим:
void set_keypress(void)
{
struct termios new_settings;
tcgetattr(0,&stored_settings);
new_settings = stored_settings;
/*
Отключение канонического режима и вывода на экран
и установка буфера ввода размером в 1 байт
*/
new_settings.c_lflag &= (~ICANON);
new_settings.c_lflag &= (~ECHO);
new_settings.c_cc[VTIME] = 0;
new_settings.c_cc[VMIN] = 1;
tcsetattr(0,TCSANOW,&new_settings);
return;
}
Вторая функция для возвращения в первоначальное состояние:
void reset_keypress(void)
{
tcsetattr(0,TCSANOW,&stored_settings);
return;
}
Переменная stored_settings должна быть объявлена как глобальная:
static struct termios stored_settings;
Затем в начале программы вызывается первая функция, а в конце, соответственно, вызывается вторая.
В готовом виде тестовый пример выглядит так:
#include <stdlib.h>
#include <stdio.h>
#include <termios.h>
#include <string.h>
static struct termios stored_settings;
void set_keypress(void)
{
struct termios new_settings;
tcgetattr(0,&stored_settings);
new_settings = stored_settings;
new_settings.c_lflag &= (~ICANON & ~ECHO);
new_settings.c_cc[VTIME] = 0;
new_settings.c_cc[VMIN] = 1;
tcsetattr(0,TCSANOW,&new_settings);
return;
}
void reset_keypress(void)
{
tcsetattr(0,TCSANOW,&stored_settings);
return;
}
int main(void)
{
set_keypress();
printf("Test: ");
while(1)
{
// putchar здесь вызывается для того, чтобы проверить работоспособность
putchar(getchar());
}
return 0;
}
Теперь моя змейка умела считывать посимвольно пользовательский ввод. Но появилась вторая проблема: программа все равно останавливалась и ждала пока пользователь нажмет какую-либо клавишу. Снова начался поиск в интернете, в процессе которого я часто натыкался на «решение» этой проблеммы в виде того, что было описано выше. Результатом поиска стал совет использовать функцию select().
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout);
Из параметров, принимаемых этой функцией нас интересуют только n, readfds и utimeout:
- n — этот параметр должен превышать не единицу максимальный файловый дескриптор в любом из наборов. Другими словами, вы должны определить максимальное целое значение для всех ваших дескрипторов, увеличить его на 1, и передать результат в качестве параметра n.
- readfds — этот набор мониторится на наличие данных для чтения в одном или нескольких дескрипторах. После возврата из select набор readfs будет очищен от всех дескрипторов, кроме тех, в которых есть данные, доступные для немедленного чтения функциями recv() (для сокетов) или read() (для каналов pipe, файлов и сокетов).
- utimeout — максимальное время, в течение которого select будет ожидать смены статуса. Если этот параметр установлен в NULL, select будет заблокирован бесконечно, ожидая событий в дескрипторах. При установке параметра в 0 секунд select возвратится немедленно.
Поэтому вызов select() будет выглядеть слудеющим образом:
select(1, &rfds, NULL, NULL, &tv);
Для указания, что события будут рассматриваться только для stdin нужно вызвать
FD_SET(0, &rfds);
Структура struct timeval определена следующим образом:
struct timeval {
time_t tv_sec; /* секунды */
long tv_usec; /* микросекунды */
};
В итоге у нас получается вот такой тестовый пример (не забудем перевести терминал в неканонический режим, что уже было рассмотрено выше):
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <termios.h>
static struct termios stored_settings;
void set_keypress(void)
{
struct termios new_settings;
tcgetattr(0,&stored_settings);
new_settings = stored_settings;
new_settings.c_lflag &= (~ICANON & ~ECHO);
new_settings.c_cc[VTIME] = 0;
new_settings.c_cc[VMIN] = 1;
tcsetattr(0,TCSANOW,&new_settings);
return;
}
void reset_keypress(void)
{
tcsetattr(0,TCSANOW,&stored_settings);
return;
}
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
set_keypress();
while(1)
{
FD_ZERO(&rfds);
FD_SET(0, &rfds);
tv.tv_sec = 0;
tv.tv_usec = 0;
retval = select(2, &rfds, NULL, NULL, &tv);
if (retval)
{
printf("Data is available now.\n");
getc(stdin);
}
else
{
printf("No data available.\n");
}
usleep(100000);
}
reset_keypress();
exit(0);
}
Использованный материал: