Sockets
В этой лекции мы попишем много кода: клиент и сервер. Поговорим немного про TCP. И Unix domain sockets.
Продолжаем играться с функцией getaddrinfo:
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s NODE SERVICE\n", argv[0]);
return 1;
}
// perform address resolution
struct addrinfo* res = NULL;
int gai_err;
struct addrinfo hints = {
.ai_family = PF_UNSPEC,
.ai_socktype = SOCK_STREAM,
.ai_flags = 0, // try AI_ALL to include IPv6 on non-v6-enabled systems
};
if ((gai_err = getaddrinfo(argv[1], argv[2], &hints, &res))) {
fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
return 2;
}
// iterate over the resulting addresses
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
struct protoent* proto = getprotobynumber(ai->ai_protocol);
if (proto) {
printf("ai_flags=%d, ai_family=%d, ai_socktype=%d, ai_protocol=%s\n",
ai->ai_flags,
ai->ai_family,
ai->ai_socktype,
proto->p_name);
}
char host[1024], port[10];
if ((gai_err = getnameinfo(ai->ai_addr,
ai->ai_addrlen,
host,
sizeof(host),
port,
sizeof(port),
NI_NUMERICHOST | NI_NUMERICSERV)))
// флаги, чтобы не преобразовывать обратно в символьную форму
{
fprintf(stderr, "getnameinfo error: %s\n", gai_strerror(gai_err));
return 3;
}
printf("\taddress: %s, port: %s\n", host, port);
}
freeaddrinfo(res);
}
Функция getaddrinfo - очень умная, может на вход принимать, как ip цифрами, так и доменные имена, как порт, так и название сервиса.
Пример использования:
./gai 127.0.0.1 31337
./gai ya.ru http
Главное, что мы получили - это преобразовали символьные имена в бинарные.
Переходим к написанию своего клиента
Код нашего клиента:
// client.c
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int create_connection(char* node, char* service) {
struct addrinfo* res = NULL;
int gai_err;
struct addrinfo hint = {
.ai_family = AF_UNSPEC, // можно и AF_INET, и AF_INET6
.ai_socktype = SOCK_STREAM, // но мы хотим поток (соединение)
};
if ((gai_err = getaddrinfo(node, service, &hint, &res))) {
fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
return -1;
}
int sock = -1;
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
sock = socket(ai->ai_family, ai->ai_socktype, 0);
if (sock < 0) {
perror("socket");
continue;
}
if (connect(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
perror("connect");
close(sock);
sock = -1;
continue;
}
break;
}
freeaddrinfo(res);
return sock;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s NODE SERVICE\n", argv[0]);
return 1;
}
int sock = create_connection(argv[1], argv[2]);
if (sock < 0) {
return 1;
}
char buf[1024] = {0};
if (read(sock, &buf, sizeof(buf) - 1) > 0) {
printf("received message: %s\n", buf);
}
close(sock);
}
С помощью network можем создать сервер из консоли.
strace -e network nc -l 31337
strace - чтобы отслеживать какие вызовы происходят внутри нашей программы.
теперь использование client:
./client 127.0.0.1 31337
Теперь поробуем написать соотвествующий сервер
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int create_listener(char* service) {
struct addrinfo* res = NULL;
int gai_err;
struct addrinfo hint = {
.ai_family = AF_INET6,
.ai_socktype = SOCK_STREAM,
.ai_flags = AI_PASSIVE, // get addresses suitable for a server to bind to
};
if ((gai_err = getaddrinfo(NULL, service, &hint, &res))) {
fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
return -1;
}
int sock = -1;
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
// create socket of the suitable family (AF_INET or AF_INET6)
sock = socket(ai->ai_family, ai->ai_socktype, 0);
if (sock < 0) {
perror("socket");
continue;
}
// make port immediately reusable after we release it
int one = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))) {
perror("setsockopt");
goto err;
}
// try to bind and listen
if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
perror("bind");
goto err;
}
if (listen(sock, SOMAXCONN) < 0) {
perror("listen");
goto err;
}
break;
err:
close(sock);
sock = -1;
}
freeaddrinfo(res);
return sock;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s SERVICE\n", argv[0]);
return 1;
}
int sock = create_listener(argv[1]);
if (sock < 0) {
return 1;
}
struct sockaddr_in6 address;
socklen_t addrlen = sizeof(address);
int connection = accept(sock, (struct sockaddr*)&address, &addrlen);
char buf[512] = {0};
inet_ntop(address.sin6_family, &address.sin6_addr, buf, sizeof(buf));
printf("accepted connection from %s\n", buf);
char* msg = "hello world\n";
write(connection, msg, strlen(msg));
close(sock);
}
Серверу нужно создать специальный слушающий сокет.
- Для начала серверу надо найти адресс, на котором он будет слушать входящие соединения.
Можем посмотреть сетевые интерфейсы на машине с помощью ifconfig:
- у нас будут локальный интерфейс lo
- вероятно будет сетевая карта (eth0)
ОС для нас заботится, мы можем создавать только сокеты для ipv6. Запросы с обычного ip4 будут автоматически передоваться.
- Потом биндимся на нужный сокет (syscall bind)
- И переключаем сокет в слущающий режим (syscall listen)
Настройка сокета готова. Мы готовы принимать соединения.
Создать соединение мы можем с помощью система вызова accept.
Функция возвращает fd. Можем писать при помощи обычного write.
Usage:
./server PORT
Если убить сервер, у которого есть незакрытые сокеты, то какое-то время на эти порты нельзя привязываться. Чтобы избежать этого стандартного поведения, надо переконфигурировать сокет используя setsockport. Очень замороченная функция. См. больше в man.
fork’ающий сервер
Чаще всего мы хотим параллельно принимать несколько различных соединений. Самая примитивная и не самая эффективная идея - это использовать fork:
//fork_server.c
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int create_listener(char* service) {
struct addrinfo* res = NULL;
int gai_err;
struct addrinfo hint = {
.ai_family = AF_INET6,
.ai_socktype = SOCK_STREAM,
.ai_flags = AI_PASSIVE,
};
if ((gai_err = getaddrinfo(NULL, service, &hint, &res))) {
fprintf(stderr, "gai error: %s\n", gai_strerror(gai_err));
return -1;
}
int sock = -1;
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
sock = socket(ai->ai_family, ai->ai_socktype, 0);
if (sock < 0) {
perror("socket");
continue;
}
int one = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))) {
perror("setsockopt");
goto err;
}
if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
perror("bind");
goto err;
}
if (listen(sock, 1) < 0) {
perror("listen");
goto err;
}
break;
err:
close(sock);
sock = -1;
}
freeaddrinfo(res);
return sock;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s SERVICE\n", argv[0]);
return 1;
}
int sock = create_listener(argv[1]);
if (sock < 0) {
return 1;
}
while (1) {
int connection = accept(sock, NULL, NULL);
if (fork() == 0) {
// worker
close(sock); // we won't be accepting anything here
// здесь можно проводить какую-нибудь работу
char* msg = "hello world";
write(connection, msg, strlen(msg));
return 0;
} else {
// собираем зомбиков
close(connection);
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
}
close(sock);
}
Пора поговорить про TCP и как он не теряет данные
Совместный просмотр absolute cinema:
Анимашка крайне проста, клиент и сервер перекидываются пакетами и подтверждениями, соотвественно.
И теперь немножко про UDP
Можем работать при помощи вызовов:
// Датаграммные сокеты
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
И теперь также немножкооо про Unix domain sockets
Совершенно другое семейство адресс, чтобы программы на одном компьютере могли взаимодействовать между собой. Их можно отоброжать в файловую систему.
Пример сервера:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
int create_listener(const char* path) {
struct sockaddr_un address = {.sun_family = AF_UNIX};
if (strlen(path) >= sizeof(address.sun_path)) {
fprintf(stderr, "pathname too long\n");
exit(1);
}
strcpy(address.sun_path, path);
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return sock;
}
// try to bind and listen
if (bind(sock, (struct sockaddr*)&address, sizeof(address)) < 0) {
perror("bind");
goto err;
}
if (listen(sock, SOMAXCONN) < 0) {
perror("listen");
goto err;
}
return sock;
err:
close(sock);
sock = -1;
return sock;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s PATH\n", argv[0]);
return 1;
}
int sock = create_listener(argv[1]);
if (sock < 0) {
return 1;
}
struct sockaddr_un address;
socklen_t addrlen = sizeof(address);
int connection = accept(sock, (struct sockaddr*)&address, &addrlen);
printf("addrlen is %d\n", addrlen);
printf("client address is %s\n", address.sun_path + 1);
char* msg = "hello world\n";
write(connection, msg, strlen(msg));
close(connection);
close(sock);
unlink(argv[1]);
}
Еще есть вызов socketpair - аналог pipe.