Сокеты в C

Содержание
Введение
Необходимые библиотеки
Создание сокета
TCP Server
TCP Server v2
TCP Client
socket man
sys/socket.h
inet_aton
sockaddr_in
Другие статьи о С

Необходимые библиотеки

Примерный набор библиотек, которые, скорее всего, придётся подключить выглядит так

#include <stdlib.h> #include <string.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h>

Хочу обратить Ваше внимание на то, что библиотеки sys/types, sys/socket не подключатся если Вы работаете в Windows с компилятором MinGW.

Попробуйте компилировать с помощью подсистемы Linux для Windows .

Создание сокета

Сокет в Си создаётся следующим образом:

int socket(int domain, int type, int protocol);

domain: Аргументом является семейство протоколов. Если Вы планируете использовать IPv4 укажите домен AF_INET.

Если нужен IPv6 то AF_INET6. Полный список смотрите в разделе MAN DESCRIPTION


type: Обычно выбирают SOCK_STREAM это надёжная упорядоченная передача байтов в режиме полный дуплекс.

Полный список типов: MAN DESCRIPTION types


protocol: Обычно выбирают 0. Если Вам нужен не 0, то Вы уже, видимо, знаете, что делаете лучше меня.

Типичный вариант создания сокета выглядит так:

int socket(AF_INET, SOCK_STREAM, 0);

TCP Сервер

#include <stdlib.h> #include <string.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main() { char server_message[256] = "You have reached the server!"; // create the server socket int server_socket; server_socket = socket(AF_INET, SOCK_STREAM, 0); // define the server address struct sockaddr_in server_address; server_address.sin_family = AF_INET; server_address.sin_port = htons(9002); server_address.sin_addr.s_addr = INADDR_ANY; // bind the socket to our specified IP and port bind(server_socket, (struct sockaddr*) &server_address, sizeof(server_address)); // second agrument is a backlog - how many connections can be waiting for this socket simultaneously listen(server_socket, 5); int client_socket; client_socket = accept(server_socket, NULL, NULL); // send the message send(client_socket, server_message, sizeof(server_message), 0); // close the socket close(server_socket); return 0; }

TCP Сервер v 2

Более продвинутая версия

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define SERVER_PORT 9002 void processing_message(unsigned char *s, int n) { printf("processing message...\n"); unsigned char *p; for (p=s; p < s + n; p++) { if (islower(*p)) { *p += 13; if(*p > 'z') *p -= 26; } } } void processing_message_service(int in, int out) { printf("processing message service...\n"); printf("fd is: %f\n", in); unsigned char buf[1024]; // Pre-allocated buffer int count; // ssize_t recv(int sockfd, void *buf, size_t len, int flags); // ssize_t read(int fd, void *buf, size_t count); //count = recv(in, &buf, 1024,0); //printf("%p\n",count); //if (count <0) {perror("HH_ERROR: recv() failed");} //while ((count = recv(in, buf, 1024,0)) > 0) while ((count = read(in, buf, 1024)) > 0) { printf("%p\n",count); printf("reading...\n"); processing_message(buf, count); //send(out, buf, count, 0); write(out, buf, count); } } int main() { printf("HH: TCP SERVER is starting\n"); // we will use this rc variable to test if the // function calls were successfull int rc; char server_message[256] = "HH: You have reached the server!\n"; // create the server socket int server_socket; server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { perror("HH_ERROR: error in calling socket()"); exit(1); }; // define the server address // htons means host to network short and is related to network byte order // htonl host to network long struct sockaddr_in server_address; struct sockaddr_in client_address; server_address.sin_family = AF_INET; // IPv4 server_address.sin_port = htons(SERVER_PORT); // port is 16 bit so htons is enough. //htons returns host_uint16 converted to network byte order server_address.sin_addr.s_addr = htonl(INADDR_ANY); // returns host_uint32 converted to network byte order // bind the socket to our specified IP and port // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // sockfd argument is a file descriptor obtained from a previous call to socket() // addr agrument is a pointer to a struckture specifying the address to which this socket is to be bound rc = bind(server_socket, (struct sockaddr*) &server_address, sizeof(server_address)); // bind should return 0; if (rc < 0) { perror("HH_ERROR: bind() call failed"); exit(1); } // second agrument is a backlog - how many connections can be waiting for this socket simultaneously rc = listen(server_socket, 5); if (rc < 0) { perror("HH_ERROR: listen() call failed"); exit(1); } printf("listening ...\n"); int keep_socket = 1; while (keep_socket) { int client_socket_fd; int client_address_len; // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // the application that called listen() then accepts the connection using accept() // accept() creates a new socket and this new socket is connected to the peer socket that performed the connect() // a file descriptor for the connected socket is returned as the function result of the accept() call // client_socket = accept(server_socket, NULL, NULL); client_address_len = sizeof(client_address); client_socket_fd = accept(server_socket, (struct sockaddr *)&client_address, &client_address_len); printf("Someone connected with address %d\n", client_address.sin_addr.s_addr); // т.е. мы взяли слушающий сокет, что-то из него достали и создали новый сокет куда положили то что надо // теперь client_socket это сокет (connection descriptor) который соединен с клиентом который запускал connect() if (client_socket_fd < 0) { perror("HH_ERROR: accept() call failed"); continue; } // send the message //send(client_socket_fd, server_message, sizeof(server_message), 0); processing_message_service(client_socket_fd, client_socket_fd); close(client_socket_fd); }; // close the socket return 0; }

Изображение баннера

TCP Клиент

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main() { // create a socket int network_socket; network_socket = socket(AF_INET, SOCK_STREAM, 0); // specify an address for the socket struct sockaddr_in server_address; server_address.sin_family = AF_INET; server_address.sin_port = htons(9002); server_address.sin_addr.s_addr = INADDR_ANY; int connection_status = connect(network_socket, (struct sockaddr *) &server_address, sizeof(server_address)); // check for error with the connection if (connection_status == -1){ printf("There was an error making a connection to the remote socket \n\n"); } // receive data from the server char server_response[256]; recv(network_socket, &server_response, sizeof(server_response), 0); // print out the server's response printf("The server sent the data: %s\n", server_response); // and then close the socket close(network_socket); return 0; }

Socket Man

Чтобы прочитать справочную информацию о socket введите

man socket

Если man не установлен прочитайте инструкцию по установке здесь

SOCKET(2) Linux Programmer's Manual SOCKET(2) NAME socket - create an endpoint for communication SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);

DESCRIPTION socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process. The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families are defined in <sys/socket.h>. The currently understood formats include: Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) AF_IPX IPX - Novell protocols AF_NETLINK Kernel user interface device netlink(7) AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7) AF_AX25 Amateur radio AX.25 protocol AF_ATMPVC Access to raw ATM PVCs AF_APPLETALK AppleTalk ddp(7) AF_PACKET Low level packet interface packet(7) AF_ALG Interface to kernel crypto API The socket has the indicated type, which specifies the communication semantics.

Currently defined types are: SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported. SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length). SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call. SOCK_RAW Provides raw network protocol access. SOCK_RDM Provides a reliable datagram layer that does not guarantee ordering. SOCK_PACKET Obsolete and should not be used in new programs; see packet(7). Some socket types may not be implemented by all protocol families. Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it may include the bitwise OR of any of the following values, to modify the behavior of socket(): SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve the same result. SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful. The protocol specifies a particular protocol to be used with the socket. Normally only a single protocol exists to support a particular socket type within a given protocol family, in which case protocol can be specified as 0. However, it is possible that many protocols may exist, in which case a particular protocol must be specified in this manner. The protocol number to use is specific to the “communication domain” in which communication is to take place; see protocols(5). See getprotoent(3) on how to map protocol name strings to protocol numbers. Sockets of type SOCK_STREAM are full-duplex byte streams. They do not preserve record boundaries. A stream socket must be in a connected state before any data may be sent or received on it. A connection to another socket is created with a connect(2) call. Once connected, data may be transferred using read(2) and write(2) calls or some variant of the send(2) and recv(2) calls. When a session has been completed a close(2) may be performed. Out-of-band data may also be transmitted as described in send(2) and received as described in recv(2). The communications protocols which implement a SOCK_STREAM ensure that data is not lost or duplicated. If a piece of data for which the peer protocol has buffer space cannot be successfully transmitted within a reasonable length of time, then the connection is considered to be dead. When SO_KEEPALIVE is enabled on the socket the protocol checks in a protocol-specific manner if the other end is still alive. A SIGPIPE signal is raised if a process sends or receives on a broken stream; this causes naive processes, which do not handle the signal, to exit. SOCK_SEQPACKET sockets employ the same system calls as SOCK_STREAM sockets. The only difference is that read(2) calls will return only the amount of data requested, and any data remaining in the arriving packet will be discarded. Also all message boundaries in incoming datagrams are preserved. SOCK_DGRAM and SOCK_RAW sockets allow sending of datagrams to correspondents named in sendto(2) calls. Datagrams are generally received with recvfrom(2), which returns the next datagram along with the address of its sender. SOCK_PACKET is an obsolete socket type to receive raw packets directly from the device driver. Use packet(7) instead. An fcntl(2) F_SETOWN operation can be used to specify a process or process group to receive a SIGURG signal when the out-of-band data arrives or SIGPIPE signal when a SOCK_STREAM connection breaks unexpectedly. This operation may also be used to set the process or process group that receives the I/O and asynchronous notification of I/O events via SIGIO. Using F_SETOWN is equivalent to an ioctl(2) call with the FIOSETOWN or SIOCSPGRP argument. When the network signals an error condition to the protocol module (e.g., using an ICMP message for IP) the pending error flag is set for the socket. The next operation on this socket will return the error code of the pending error. For some protocols it is possible to enable a per-socket error queue to retrieve detailed information about the error; see IP_RECVERR in ip(7). The operation of sockets is controlled by socket level options. These options are defined in <sys/socket.h>. The functions setsockopt(2) and getsockopt(2) are used to set and get options, respectively. RETURN VALUE On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately. ERRORS EACCES Permission to create a socket of the specified type and/or protocol is denied. EAFNOSUPPORT The implementation does not support the specified address family. EINVAL Unknown protocol, or protocol family not available. EINVAL Invalid flags in type. EMFILE The per-process limit on the number of open file descriptors has been reached. ENFILE The system-wide limit on the total number of open files has been reached. ENOBUFS or ENOMEM Insufficient memory is available. The socket cannot be created until sufficient resources are freed. EPROTONOSUPPORT The protocol type or the specified protocol is not supported within this domain. Other errors may be generated by the underlying protocol modules. CONFORMING TO POSIX.1-2001, POSIX.1-2008, 4.4BSD. The SOCK_NONBLOCK and SOCK_CLOEXEC flags are Linux-specific. socket() appeared in 4.2BSD. It is generally portable to/from non-BSD systems supporting clones of the BSD socket layer (including System V variants). NOTES POSIX.1 does not require the inclusion of <sys/types.h>, and this header file is not required on Linux. However, some historical (BSD) implementations required this header file, and portable applications are probably wise to include it. The manifest constants used under 4.x BSD for protocol families are PF_UNIX, PF_INET, and so on, while AF_UNIX, AF_INET, and so on are used for address families. However, already the BSD man page promises: "The protocol family generally is the same as the address family", and subsequent standards use AF_* everywhere. The AF_ALG protocol type was added in Linux 2.6.38. More information on this interface is provided with the kernel HTML documentation at https://www.kernel.org/doc/htmldocs/crypto-API/User.html. EXAMPLE An example of the use of socket() is shown in getaddrinfo(3). SEE ALSO accept(2), bind(2), close(2), connect(2), fcntl(2), getpeername(2), getsockname(2), getsockopt(2), ioctl(2), listen(2), read(2), recv(2), select(2), send(2), shutdown(2), socketpair(2), write(2), getprotoent(3), ip(7), socket(7), tcp(7), udp(7), unix(7) “An Introductory 4.3BSD Interprocess Communication Tutorial” and “BSD Interprocess Communication Tutorial”, reprinted in UNIX Programmer's Supplementary Documents Volume 1. COLOPHON This page is part of release 4.16 of the Linux man-pages project. A description of the project, information about reporting bugs, and the latest version of this page, can be found at https://www.kernel.org/doc/man-pages/. Linux 2017-09-15 SOCKET(2)

sys/socket.h

The Single UNIX ® Specification, Version 2 Copyright © 1997 The Open Group NAME sys/socket.h - Internet Protocol family SYNOPSIS #include <sys/socket.h> DESCRIPTION <sys/socket.h> makes available a type, socklen_t, which is an unsigned opaque integral type of length of at least 32 bits. To forestall portability problems, it is recommended that applications should not use values larger than 232 - 1. The <sys/socket.h> header defines the unsigned integral type sa_family_t. The <sys/socket.h> header defines the sockaddr structure that includes at least the following members: sa_family_t sa_family address family char sa_data[] socket address (variable-length data) The <sys/socket.h> header defines the msghdr structure that includes at least the following members: void *msg_name optional address socklen_t msg_namelen size of address struct iovec *msg_iov scatter/gather array int msg_iovlen members in msg_iov void *msg_control ancillary data, see below socklen_t msg_controllen ancillary data buffer len int msg_flags flags on received message The <sys/socket.h> header defines the cmsghdr structure that includes at least the following members: socklen_t cmsg_len data byte count, including the cmsghdr int cmsg_level originating protocol int cmsg_type protocol-specific type Ancillary data consists of a sequence of pairs, each consisting of a cmsghdr structure followed by a data array. The data array contains the ancillary data message, and the cmsghdr structure contains descriptive information that allows an application to correctly parse the data. The values for cmsg_level will be legal values for the level argument to the getsockopt() and setsockopt() functions. The system documentation should specify the cmsg_type definitions for the supported protocols. Ancillary data is also possible at the socket level. The <sys/socket.h> header defines the following macro for use as the cmsg_type value when cmsg_level is SOL_SOCKET: SCM_RIGHTS Indicates that the data array contains the access rights to be sent or received. The <sys/socket.h> header defines the following macros to gain access to the data arrays in the ancillary data associated with a message header: CMSG_DATA(cmsg) If the argument is a pointer to a cmsghdr structure, this macro returns an unsigned character pointer to the data array associated with the cmsghdr structure. CMSG_NXTHDR(mhdr,cmsg) If the first argument is a pointer to a msghdr structure and the second argument is a pointer to a cmsghdr structure in the ancillary data, pointed to by the msg_control field of that msghdr structure, this macro returns a pointer to the next cmsghdr structure, or a null pointer if this structure is the last cmsghdr in the ancillary data. CMSG_FIRSTHDR(mhdr) If the argument is a pointer to a msghdr structure, this macro returns a pointer to the first cmsghdr structure in the ancillary data associated with this msghdr structure, or a null pointer if there is no ancillary data associated with the msghdr structure. The <sys/socket.h> header defines the linger structure that includes at least the following members: int l_onoff indicates whether linger option is enabled int l_linger linger time, in seconds The <sys/socket.h> header defines the following macros, with distinct integral values: SOCK_DGRAM Datagram socket SOCK_STREAM Byte-stream socket SOCK_SEQPACKET Sequenced-packet socket The <sys/socket.h> header defines the following macro for use as the level argument of setsockopt() and getsockopt(). SOL_SOCKET Options to be accessed at socket level, not protocol level. The <sys/socket.h> header defines the following macros, with distinct integral values, for use as the option_name argument in getsockopt() or setsockopt() calls: SO_ACCEPTCONN Socket is accepting connections. SO_BROADCAST Transmission of broadcast messages is supported. SO_DEBUG Debugging information is being recorded. SO_DONTROUTE bypass normal routing SO_ERROR Socket error status. SO_KEEPALIVE Connections are kept alive with periodic messages. SO_LINGER Socket lingers on close. SO_OOBINLINE Out-of-band data is transmitted in line. SO_RCVBUF Receive buffer size. SO_RCVLOWAT receive "low water mark" SO_RCVTIMEO receive timeout SO_REUSEADDR Reuse of local addresses is supported. SO_SNDBUF Send buffer size. SO_SNDLOWAT send "low water mark" SO_SNDTIMEO send timeout SO_TYPE Socket type. The <sys/socket.h> header defines the following macros, with distinct integral values, for use as the valid values for the msg_flags field in the msghdr structure, or the flags parameter in recvfrom(), recvmsg(), sendto() or sendmsg() calls: MSG_CTRUNC Control data truncated. MSG_DONTROUTE Send without using routing tables. MSG_EOR Terminates a record (if supported by the protocol). MSG_OOB Out-of-band data. MSG_PEEK Leave received data in queue. MSG_TRUNC Normal data truncated. MSG_WAITALL Wait for complete message. The <sys/socket.h> header defines the following macros, with distinct integral values: AF_UNIX UNIX domain sockets AF_UNSPEC Unspecified AF_INET Internet domain sockets The <sys/socket.h> header defines the following macros, with distinct integral values: SHUT_RD Disables further receive operations. SHUT_WR Disables further send operations. SHUT_RDWR Disables further send and receive operations. The following are declared as functions, and may also be defined as macros: int accept(int socket, struct sockaddr *address, socklen_t *address_len); int bind(int socket, const struct sockaddr *address, socklen_t address_len); int connect(int socket, const struct sockaddr *address, socklen_t address_len); int getpeername(int socket, struct sockaddr *address, socklen_t *address_len); int getsockname(int socket, struct sockaddr *address, socklen_t *address_len); int getsockopt(int socket, int level, int option_name, void *option_value, socklen_t *option_len); int listen(int socket, int backlog); ssize_t recv(int socket, void *buffer, size_t length, int flags); ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len); ssize_t recvmsg(int socket, struct msghdr *message, int flags); ssize_t send(int socket, const void *message, size_t length, int flags); ssize_t sendmsg(int socket, const struct msghdr *message, int flags); ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len); int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len); int shutdown(int socket, int how); int socket(int domain, int type, int protocol); int socketpair(int domain, int type, int protocol, int socket_vector[2]); SEE ALSO accept(), bind(), connect(), getpeername(), getsockname(), getsockopt(), listen(), recv(), recvfrom(), recvmsg(), send(), sendmsg(), sendto(), setsockopt(), shutdown(), socket(), socketpair(). UNIX ® is a registered Trademark of The Open Group. Copyright © 1997 The Open Group [ Main Index | XSH | XCU | XBD | XCURSES | XNS ]

inet_aton

Функция inet_aton конвертирует строку в сетевой адрес. Возвращает int. 1 если конвертация прошла успешно. 0 если конвертация не получилась.

В качестве параметров использует указатель const char и структуру in_addr *addr

Эта функция считается устаревшей, на смену ей пришли inet_pton() и inet_ntop()

int inet_aton(const char *cp, struct in_addr *addr);

inet_aton(address, &remote_address.sin_addr.s_addr);

inet_aton() convert Internet dot address to network address Function SYNOPSIS #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *addr); DESCRIPTION The inet_aton() function converts the specified string, in the Internet standard dot notation, to a network address, and stores the address in the structure provided. The converted address is in network byte order (bytes ordered from left to right). Values specified using dot notation take one of the following forms: a.b.c.d When four parts are specified, each is interpreted as a byte of data and assigned, from left to right, to the four bytes of an internet address. a.b.c When a three-part address is specified, the last part is interpreted as a 16-bit quantity and placed in the rightmost two bytes of the network address. This makes the three-part address format convenient for specifying Class B network addresses as 128.net.host. a.b When a two-part address is supplied, the last part is interpreted as a 24-bit quantity and placed in the rightmost three bytes of the network address. This makes the two-part address format convenient for specifying Class A network addresses as net.host. a When only one part is given, the value is stored directly in the network address without any byte rearrangement. All numbers supplied as parts in dot notation may be decimal, octal, or hexadecimal, as specified in the ISO C standard (that is, a leading 0x or 0X implies hexadecimal; otherwise a leading 0 implies octal; otherwise, the number is interpreted as decimal). PARAMETERS cp Points to a string in Internet standard dot notation. addr Buffer where the converted address is to be stored. RETURN VALUES The inet_aton() function returns 1 if the address is successfully converted, or 0 if the conversion failed. CONFORMANCE 4.4BSD MULTITHREAD SAFETY LEVEL MT-Safe. PORTING ISSUES None. AVAILABILITY PTC MKS Toolkit for Professional Developers PTC MKS Toolkit for Professional Developers 64-Bit Edition PTC MKS Toolkit for Enterprise Developers PTC MKS Toolkit for Enterprise Developers 64-Bit Edition

sockaddr_in

struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };

Ошибки

fatal error: sys/socket.h: No such file or directory

Эта ошибка возникает в случае когда Вы установили MinGW gcc, например, по моей инструкции . а в коде используете sys/socket.h

Я бы предложил установить подсистему Linux для Winodows и установить gcc там

Как вариант можно попробовать cygwin вместо MinGW. Этот вариант стоит выбрать, если программа, которую Вы пишите должна будет работать в UNIX-подобной ОС.

Либо использовать winsock2.h вместо sys/socket.

pclose

tcp_client.c: In function ‘main’: tcp_client.c:34:2: warning: implicit declaration of function ‘close’; did you mean ‘pclose’? [-Wimplicit-function-declaration] close(network_socket);

Похожие статьи
Программирование на Си
Основы Си
Учебник по Си
Boolean в Си
К и Р
Что такое argc, char * argv[]
Функция scanf()
Указатели
Структуры в Си
Запросы к REST API на Си
Оператор «стрелка» указатель на член структуры

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: