Vladislav Shpilevoy PRO
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.
Системное программирование
Лекция 8:
Сеть. Модели TCP/IP, OSI. Связь с ядром. Интерфейсы и примеры.
The presentation is outdated and not maintained anymore. Please, refer to the English version for the most actual slides.
Дедлайны:
Анонимные
int
pipe2(int pipefd[2], int flags);
void *
mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int
mkfifo(const char *pathname, mode_t mode);
Именованные, стандарт XSI
int
semget(key_t key, int nsems, int semflg);
int
msgget(key_t key, int msgflg);
int
shmget(key_t key, size_t size, int shmflg);
Именованные, стандарт POSIX
sem_t *
sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int
socket(int domain, int type, int protocol);
int
bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
int
listen(int sockfd, int backlog);
int
connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
int
accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
Создание сокета с заданными доменом, протоколом, типом
Привязка имени к сокету
Прослушка входящих подключений через connect()
Способ подключить сокет к другому уже именованному сокету. Тогда bind не нужен
Если же сделали bind + listen, то сокет принимает клиентов через accept
Способы использования
int fd = socket();
connect(fd, remote_addr);
/* Ready to read/write fd. */
Подключение к уже именованному
Создание именованного без соединения
int fd = socket();
bind(fd, addr);
/** Ready to read/write fd. */
Создание именованного с соединением
int fd = socket();
bind(fd, addr);
listen(fd);
while(1) {
int remote_fd = accept(fd);
/*
* Ready to read/write
* remote_fd.
*/
}
Connect() создает на сервере парный сокет, и эти пара может общаться как socketpair()
int fd2 = socket();
bind(fd2, addr2);
/** Ready to read/write fd2. */
read/write
send/recv
sendto/recvfrom
Без connect() работают только пакетные типы сокетов, и нужно указывать адресатов руками
1963
Возникновение идеи
1965
Первое сетевое взаимодействие
1990
Популяризация сетей и закрытие ARPANET
Джозеф Карл Робнетт
Ликлайдер с работой "Галактическая сеть"
Начало ARPANET в DARPA - Defense Advanced Research Projects Agency
0/100
0/200
0/70
0/50
50
50/100
50/200
50/50
50/70
0/50
50/50
Как работает
Результат
0/100
0/200
0/70
0/50
25
25/100
25/200
10/50
25/70
0/50
10/50
25
10
0/100
15/100
0/100
15/100
0/100
15/100
0/100
25/100
15
15
15
10
10
15
25
25
25
Как работает
Результат
Метаданные физической среды
Метаданные маршрутизации
Метаданные корректора ошибок
Пользовательские данные
<html>...</html>
File size, blocks ...
Game data ...
User space
Kernel space
Приложения, веб-сервера, игры, файловые менеджеры, почта ...
Протоколы надежности доставки
Протоколы приема/продвижения пакетов дальше по сети
Протоколы взаимодействия с проводами, радио-средой
Полезная
нагрузка
Служебный
заголовок
Служебный
заголовок
Служебный
заголовок
User data
Приложение
Packet 1
Подсистема надежности
доставки
Header
Packet 2
Header
Подсистема маршрутизации
Packet 1
Header
Packet 2
Header
Header
Header
Packet 1
Header
Packet 2
Header
Header
Header
Header
Header
Драйвер сетевого устройства
OSI - Open System Interconnection
7 уровней:
Веб-страницы
Фильмы
Документы
Сетевые интерфейсы баз данных
Задачи:
Особенности:
осведомленность про смысл данных, их назначение
Протоколы:
FTP, SMTP, DNS, HTTP
Пример протокола уровня приложений - формат ответа на SQL запрос в Tarantool
+----------------------------------------------+
| IPROTO_BODY: { |
| IPROTO_METADATA: [ |
| { |
| IPROTO_FIELD_NAME: string, |
| IPROTO_FIELD_TYPE: number, |
| IPROTO_FIELD_FLAGS: number, |
| }, |
| ... |
| ], |
| |
| IPROTO_SQL_INFO: { |
| SQL_INFO_ROW_COUNT: number, |
| SQL_INFO_LAST_ID: number, |
| ... |
| }, |
| |
| IPROTO_DATA: [ |
| tuple/scalar, |
| ... |
| ] |
| } |
+----------------------------------------------+
Пример HTTP запроса
:authority: clc.stackoverflow.com
:method: GET
:path: /markup.js?...
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
cache-control: no-cache
cookie: prov=702db90b-56ab-53f7-3894-c3733607b954;...
pragma: no-cache
referer: https://stackoverflow.com/questions/...
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X ...
<xml>
</xml>
<html>
</html>
--
- 'YAML'
...
{
"json":
}
0xa7 MsgPack
Задачи:
Особенности:
Языки, форматы:
JSON, XML, YAML, HTML, MessagePack
JSON представление ответа GitHub API
{
"action": "opened",
"issue": {
"url": "https://api.github.com/repos/octocat/Hello-World/issues/1347",
"number": 1347,
...
},
"repository" : {
"id": 1296269,
"full_name": "octocat/Hello-World",
"owner": {
"login": "octocat",
"id": 1,
...
},
...
},
"sender": {
"login": "octocat",
"id": 1,
...
}
}
XML представление ответа с opengis
<WFS_Capabilities xmlns="http://www.opengis.net/wfs" version="1.0.0">
<Service>
<Name> Oracle WFS </Name>
<Title> Oracle Web Feature Service </Title>
<Abstract> Web Feature Service maintained by Oracle </Abstract>
<OnlineResource>http://localhost:8888/SpatialWS-</OnlineResource>
</Service>
<Capability>
<GetCapabilities>
<DCPType>
<HTTP>
<Get onlineResource="http://localhost:8888/SpatialWS-"/>
</HTTP>
</DCPType>
<DCPType>
<HTTP>
<Post onlineResource="http://localhost:8888/SpatialWS-"/>
</HTTP>
</DCPType>
</GetCapabilities>
</FeatureType>
</WFS_Capabilities>
Выбор уровня надежности
Аутентификация
Нужна ли установка соединения
Тайминги
Задачи:
Особенности:
Почти ничего не реализует, кроме аутентификации. Выбирает, надо ли устанавливать соединение, его настройки.
Протоколы:
PAP, CHAP
?
Приложения
Представление
Сессии
Какой что делает? Какие особенности у каждого?
Приложения: создать данные, знают их смысл.
Представление: форматировать данные, знают только вид данных.
Сессии: выбрать настройки связи, аутентификацию. Видит данные, как просто байты.
2 балла
Надежность: порядок, дублирования, потери, перегрузки
Фрагментация и батчинг
Установка виртуального соединения
Вид данных: пакеты или сплошной поток
Задачи:
Особенности:
Протоколы:
TCP, UDP, SCTP, RDP
Маршрутизация
Продвижение чужих пакетов
Борьба с некоторыми перегрузками
Задачи:
Особенности:
Протоколы:
IP, DDP, ICMP, RIP, EGP
Сети иерархичны
На сетевом уровне взаимодействуют разные подсети
Маршрутизация в одной подсети
Продвижение чужих пакетов в одной подсети
Борьба с некоторыми перегрузками
Задачи:
Особенности:
Протоколы:
Ethernet, MPLS, PPP, TokenRing
Область работы канального уровня
Область работы сетевого уровня
Модуляция сигнала
Взаимодействие со средой передачи
Борьба с коллизиями
Задачи:
Особенности:
Технологии:
Bluetooth, Wi-Fi, OTN, Ethernet cable, USB
Проверка и исправление ошибок
Область работы канального уровня
Область работы физического уровня
Приложение
Представление
Сессии
Транспорт
Канал
Сеть
Физика
Data
4 уровня:
TCP/IP
OSI
7 уровней:
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
struct iphdr {
__u8 version:4,
ihl:4;
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
};
struct ethhdr {
unsigned char h_dest[ETH_ALEN];
unsigned char h_source[ETH_ALEN];
__be16 h_proto;
};
Упаковка, заворачивание в заголовки
int
tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size);
send(socket, data, data_size);
int
ip_build_and_send_pkt(struct sk_buff *skb, const struct sock *sk,
__be32 saddr, __be32 daddr,
struct ip_options_rcu *opt);
int
dnet_mdio_write(struct mii_bus *bus, int mii_id, int regnum,
u16 value);
User space
Kernel space
int
socket(int domain, int type, int protocol);
AF_UNIX - локальный сокет, видимый только на этой машине
AF_INET - сетевые сокеты на базе IPv4
AF_PACKET - "сырые" сокеты, можно самому разбирать протоколы
int
socket(int domain, int type, int protocol);
SOCK_DGRAM - UDP, ограничен размер пакета, нет гарантий надежности
SOCK_STREAM - TCP, данные как поток байт, а не пакеты, есть гарантии доставки, соединения
SOCK_SEQPACKET - SCTP, как TCP, но данные как пакеты ограниченного размера
SOCK_RDM - RDP, как SCTP, но порядок доставки не гарантирован
int
socket(int domain, int type, int protocol);
0 - по умолчанию
IPPROTO_SCTP
IPPROTO_IP
IPPROTO_TCP
IPPROTO_RAW
IPPROTO_UDP
void try_protocol(int type, int protocol, const char *protocol_name)
{
int sock = socket(AF_INET, type, protocol);
if (sock == -1) {
printf("%s: error = %s\n", protocol_name, strerror(errno));
} else {
printf("%s: success\n", protocol_name);
close(sock);
}
}
void try_type(int type, const char *type_name)
{
printf("\nTry %s type\n", type_name);
try_protocol(type, IPPROTO_TCP, "TCP");
try_protocol(type, IPPROTO_IP, "IP");
try_protocol(type, IPPROTO_SCTP, "SCTP");
try_protocol(type, IPPROTO_RAW, "RAW");
try_protocol(type, IPPROTO_UDP, "UDP");
}
int main()
{
try_type(SOCK_DGRAM, "DGRAM");
try_type(SOCK_RAW, "RAW");
try_type(SOCK_STREAM, "STREAM");
return 0;
}
$> gcc 1_socket_protocol.c
$> ./a.out
Try DGRAM type
TCP: error = Protocol wrong type for socket
IP: success
SCTP: error = Protocol not supported
RAW: error = Protocol wrong type for socket
UDP: success
Try RAW type
TCP: success
IP: success
SCTP: success
RAW: success
UDP: success
Try STREAM type
TCP: success
IP: success
SCTP: error = Protocol not supported
RAW: error = Protocol wrong type for socket
UDP: error = Protocol wrong type for socket
Mac
$> gcc 1_socket_protocol.c
$> ./a.out
Try DGRAM type
TCP: error = Protocol not supported
IP: success
SCTP: error = Protocol not supported
RAW: error = Protocol not supported
UDP: success
Try RAW type
TCP: success
IP: error = Protocol not supported
SCTP: success
RAW: success
UDP: success
Try STREAM type
TCP: success
IP: success
SCTP: success
RAW: error = Protocol not supported
UDP: error = Protocol not supported
Linux
int
bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[108];
};
struct sockaddr_nl {
sa_family_t nl_family;
unsigned short nl_pad;
pid_t nl_pid;
__u32 nl_groups;
};
AF_UNIX
AF_INET
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
AF_INET
Адрес назначения транспортного уровня, порт, 2 байта
Адрес назначения сетевого уровня, IP адрес, 4 байта:
xxx.xxx.xxx.xxx
Порт и адрес в network byte order, big-endian
uint32_t
htonl(uint32_t hostlong);
uint16_t
htons(uint16_t hostshort);
uint32_t
ntohl(uint32_t netlong);
uint16_t
ntohs(uint16_t netshort);
int
inet_aton(const char *cp, struct in_addr *inp);
in_addr_t
inet_addr(const char *cp);
in_addr_t
inet_network(const char *cp);
char *
inet_ntoa(struct in_addr in);
struct in_addr
inet_makeaddr(in_addr_t net, in_addr_t host);
int
getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
void
freeaddrinfo(struct addrinfo *res);
const char *
gai_strerror(int errcode);
Для преобразования доменного имени в адрес для bind/connect
int
main()
{
struct addrinfo *addr, *iter;
int rc = getaddrinfo("yandex.ru", NULL, NULL, &addr);
if (rc != 0) {
printf("Error = %s\n", gai_strerror(rc));
return -1;
}
printf("Families: inet = %d, inet6 = %d\n", AF_INET, AF_INET6);
printf("Socket types: dgram = %d, stream = %d, raw = %d\n", SOCK_DGRAM,
SOCK_STREAM, SOCK_RAW);
printf("Protocols: tcp = %d, udp = %d\n\n", IPPROTO_TCP, IPPROTO_UDP);
for (iter = addr; iter != NULL; iter = iter->ai_next) {
printf("family = %d, socktype = %d, protocol = %d",
iter->ai_family, iter->ai_socktype, iter->ai_protocol);
if (iter->ai_family == AF_INET) {
char buf[128];
struct sockaddr_in *tmp =
(struct sockaddr_in *)iter->ai_addr;
inet_ntop(AF_INET, &tmp->sin_addr, buf, sizeof(buf));
printf(", ip = %s", buf);
}
printf("\n");
}
freeaddrinfo(addr);
return 0;
}
Получить начало списка адресов
Распечатать каждый из адресов
Элементы связаны через ai_next поле
В каждом элементе описание адреса и сокета, который на нем слушает
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
$> gcc 2_getaddrinfo.c
$> ./a.out
Families: inet = 2, inet6 = 10
Socket types: dgram = 2, stream = 1, raw = 3
Protocols: tcp = 6, udp = 17
family = 2, socktype = 2, protocol = 17, ip = 77.88.55.80
family = 2, socktype = 3, protocol = 0, ip = 77.88.55.80
family = 2, socktype = 1, protocol = 6, ip = 5.255.255.88
family = 2, socktype = 2, protocol = 17, ip = 5.255.255.88
family = 2, socktype = 3, protocol = 0, ip = 5.255.255.88
family = 2, socktype = 1, protocol = 6, ip = 77.88.55.77
family = 2, socktype = 2, protocol = 17, ip = 77.88.55.77
family = 2, socktype = 3, protocol = 0, ip = 77.88.55.77
family = 2, socktype = 1, protocol = 6, ip = 5.255.255.80
family = 2, socktype = 2, protocol = 17, ip = 5.255.255.80
family = 2, socktype = 3, protocol = 0, ip = 5.255.255.80
family = 10, socktype = 1, protocol = 6
family = 10, socktype = 2, protocol = 17
family = 10, socktype = 3, protocol = 0
8_net/3_client.c [1]
int
main(int argc, const char **argv)
{
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == -1) {
printf("error = %s\n", strerror(errno));
return -1;
}
struct addrinfo *addr;
struct addrinfo filter;
memset(&filter, 0, sizeof(filter));
filter.ai_family = AF_INET;
filter.ai_socktype = SOCK_STREAM;
int rc = getaddrinfo(argv[1], argv[2], &filter, &addr);
if (rc != 0) {
printf("addrinfo error = %s\n", gai_strerror(rc));
close(sock);
return -1;
}
if (addr == NULL) {
printf("not found a server\n");
freeaddrinfo(addr);
close(sock);
return -1;
}
Создал сокет для работы по TCP
Пытаюсь получить адрес сервера по имени
Вместо итерации по списку можно задать фильтр для результатов - тогда хватит одного адреса
8_net/3_client.c [2]
rc = connect(sock, addr->ai_addr, addr->ai_addrlen);
freeaddrinfo(addr);
if (rc != 0) {
printf("connect error = %s\n", strerror(errno));
close(sock);
return -1;
}
int number;
while (scanf("%d", &number) > 0) {
if (write(sock, &number, sizeof(number)) == -1) {
printf("error = %s\n", strerror(errno));
continue;
}
printf("Sent %d\n", number);
number = 0;
int rc = read(sock, &number, sizeof(number));
if (rc == 0) {
printf("Closed connection\n");
break;
}
if (rc == -1)
printf("error = %s\n", strerror(errno));
else
printf("Received %d\n", number);
}
close(sock);
return 0;
}
Далее ничего не отличается от UNIX сокетов
8_net/3_server.c [1]
void *
worker_f(void *arg)
{
printf("New client created\n");
int client_sock = (int) arg;
while(1) {
int buffer = 0;
ssize_t size = read(client_sock, &buffer, sizeof(buffer));
if (size == -1) {
printf("error = %s\n", strerror(errno));
continue;
}
if (size == 0) {
printf("Closed connection\n");
break;
}
printf("Received %d\n", buffer);
buffer++;
if (write(client_sock, &buffer, sizeof(buffer)) == -1)
printf("error = %s\n", strerror(errno));
else
printf("Sent %d\n", buffer);
}
close(client_sock);
return NULL;
}
В обслуживании клиентов ничего не меняется по сравнению с UNIX сокетами
Прочитать число, увеличить
Послать обратно
8_net/3_server.c [2]
int
main(int argc, const char **argv)
{
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == -1) {
printf("error = %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(12345);
inet_aton("127.0.0.1", &addr.sin_addr);
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
printf("bind error = %s\n", strerror(errno));
return -1;
}
if (listen(sock, 128) == -1) {
printf("listen error = %s\n", strerror(errno));
return -1;
}
Создал TCP сокет
Вместо getaddrinfo могу сам заполнить адрес
Дальше ничего не меняется от UNIX сокетов
8_net/3_server.c [3]
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, 1);
while(1) {
pthread_t worker_thread;
int client_sock = accept(sock, NULL, NULL);
if (client_sock == -1) {
printf("error = %s\n", strerror(errno));
continue;
}
int rc = pthread_create(&worker_thread, &attr, worker_f,
(void *) client_sock);
if (rc != 0) {
printf("error = %s\n", strerror(rc));
close(client_sock);
}
}
pthread_attr_destroy(&attr);
close(sock);
return 0;
}
Дальше все остается как было. Новый клиент берется через accept и кладется в свой поток
$> gcc 3_server.c -o server
$> ./server
$> gcc 3_client.c.c -o client
$> ./client 127.0.0.1 12345
New client created
1
Sent 1
Received 2
Received 1
Sent 2
^C
$>
Closed connection
int
getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int
setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
int
fcntl(int fd, int cmd, ... /* arg */ );
SO_KEEPALIVE
SO_REUSEADDR
Игнорировать допосылку данных ядром из уже закрытого сокета
Периодическая посылка пустых пакетов во время бездействия, чтобы вовремя обнаружить разрыв соединения
SO_REUSEPORT
{protocol, src_ip, src_port, dst_ip, dst_port}
Опция разрешает этим полям совпадать, даже у живых сокетов
Идентификатор каждого сокета в ядре:
O_NONBLOCK
Не блокироваться на чтение/запись, если читать/писать нечего
int enable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int));
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(int));
flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
Пул потоков
Пул потоков - это множество потоков, которым можно поручать разные задачи для фонового выполнения, и потом асинхронно получать результат. Потоки не завершаются после задачи, а берут следующую. Если задач нет, то поток спит.
Есть интерфейс для создания пула потоков, складывания туда задач, забора результатов. Надо написать его реализацию.
Цена: 15 - 25 баллов.
Срок: 2 недели.
Положить на ваш гитхаб и сказать мне его. Сдавать как угодно - лично/удаленно.
/** Thread pool API. */
int
thread_pool_new(int max_thread_count, struct thread_pool **pool);
int
thread_pool_thread_count(const struct thread_pool *pool);
int
thread_pool_delete(struct thread_pool *pool);
int
thread_pool_push_task(struct thread_pool *pool, struct thread_task *task);
/** Thread pool task API. */
int
thread_task_new(struct thread_task **task, thread_task_f function, void *arg);
bool
thread_task_is_finished(const struct thread_task *task);
bool
thread_task_is_running(const struct thread_task *task);
int
thread_task_join(struct thread_task *task, void **result);
int
thread_task_delete(struct thread_task *task);
struct thread_task {
thread_task_f function;
void *arg;
/* PUT HERE OTHER MEMBERS */
};
struct thread_pool {
pthread_t *threads;
/* PUT HERE OTHER MEMBERS */
};
В следующий раз:
Advanced IO. Неблокирующие IO операции. Блокировка файла. Мультиплексирование: select, poll, kqueue.
By Vladislav Shpilevoy
Сеть, краткая история от ARPANET. Каноническая модель OSI, реальная TCP/IP, стек протоколов. Реализация сетевого взаимодействия в ядре. Пользовательский интерфейс socket, connect, close, send, recv. TCP и UDP.
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.