Предыдущая | Оглавление | Следующая |
Это клиент-серверный мир, детка. Практически все в Сети состоит из клиентских процессов, которые общаются с серверными процессами, и наоборот. Взять к примеру telnet. Когда ты соединяешься с удаленным компьютером на порт 23 с помощью клиента telnet, программа на этом компьютере (telnetd, сервер) пробуждается к жизни и начинает диалог с клиентом.
Клиент-серверное взаимодействие
Обмен информацией между клиентом и сервером обобщен на Рисунке 2.
Заметь, что пара клиент-сервер может использовать SOCK_STREAM, SOCK_DGRAM, или вообще что угодно, пока они используют один и тот же протокол. Примеры клиент-серверных пар: telnet/telnetd, ftp/ftpd, или bootp/bootpd. Когда ты используешь ftp, ее обслуживает удаленная программа ftpd.
Часто на машине расположен только один сервер, и он обрабатывает множественных клиентов с помощью fork(). Базовая процедура такова: сервер ждет соединения, принимает его через accept(), создает процесс с помощью fork() и обрабатывает его. Всем этим занимается наш пример сервера в следующем разделе.
Этот сервер просто отсылает строку "Hello, World!\n" по потоковому соединению. Чтобы его протестировать, запусти сервер в одном окне и telnet в другом:
$ telnet remotehostname 3490 |
где remotehostname - имя твоего компьютера.
/* ** server.c -- пример потокового сервера */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> #define MYPORT 3490 // порт #define BACKLOG 10 // количество входящих соединений void sigchld_handler(int s) { while(waitpid(-1, NULL, WNOHANG) > 0); } int main(void) { int sockfd, new_fd; // слушаем на sock_fd, соединение в new_fd struct sockaddr_in my_addr; // локальный адрес struct sockaddr_in their_addr; // удаленный адрес socklen_t sin_size; struct sigaction sa; int yes=1; if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); } my_addr.sin_family = AF_INET; // системный порядок байт my_addr.sin_port = htons(MYPORT); // сетевой порядок байт my_addr.sin_addr.s_addr = INADDR_ANY; // автоматически указать локальный IP memset(&(my_addr.sin_zero), '\0', 8); // обнулить остаток if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } if (listen(sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } sa.sa_handler = sigchld_handler; // закончить процессы sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(1); } while(1) { // основной цикл accept() sin_size = sizeof(struct sockaddr_in); if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) { perror("accept"); continue; } printf("сервер: соединение с %s\n", inet_ntoa(their_addr.sin_addr)); if (!fork()) { // это дочерний процесс close(sockfd); // которому не нужно слушать if (send(new_fd, "Hello, world!\n", 14, 0) == -1) perror("send"); close(new_fd); exit(0); } close(new_fd); // основному процессу не нужно соединение } return 0; } |
Я разместил код в одной большой функции main() для ясности. Его можно разбить на меньшие функции, если тебе так проще.
(Вызов sigaction() может быть новым для тебя - это нормально. Этот код удаляет завершившиеся процессы. Если их долго не удалять, администратор точно разозлится.)
Для взаимодействия с этим сервером можно использовать клиента из следующего раздела.
Клиент даже проще, чем сервер. Все, что он делает, это соединяется с сервером, указанном в командной строке, на порт 3490. Он получает строку, посланную сервером.
/* ** client.c -- пример потокового клиента */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #define PORT 3490 // порт соединения #define MAXDATASIZE 100 // максимальный объем пакета int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; // адрес сервера if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // получить адрес сервера herror("gethostbyname"); exit(1); } if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // системный порядок their_addr.sin_port = htons(PORT); // сетевой порядок their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // обнуляем остаток структуры if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Принял: %s",buf); close(sockfd); return 0; } |
Заметь, что если не запустить сервер до клиента, connect() вернет "В соединении отказано". Очень полезно.
Тут практически не о чем говорить, так что я просто предоставлю пару примеров: talker.c и listener.c.
listener ждет входящего пакета на порт 4950. talker посылает пакет на этот порт, на указанную машину.
Исходник listener.c:
/* ** listener.c -- пример датаграммного "сервера" */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MYPORT 4950 // порт соединения #define MAXBUFLEN 100 int main(void) { int sockfd; struct sockaddr_in my_addr; // локальный адрес struct sockaddr_in their_addr; // удаленный адрес socklen_t addr_len; int numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; // системный порядок my_addr.sin_port = htons(MYPORT); // сетевой порядок my_addr.sin_addr.s_addr = INADDR_ANY; // автоматически ввести локальный IP memset(&(my_addr.sin_zero), '\0', 8); // обнулить остаток if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN-1 , 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("получил пакет от %s\n",inet_ntoa(their_addr.sin_addr)); printf("длина пакета %d байт\n",numbytes); buf[numbytes] = '\0'; printf("пакет содержит: \"%s\"\n",buf); close(sockfd); return 0; } |
Обрати внимание, что в вызове socket() мы наконец-то используем SOCK_DGRAM. Кроме этого, не нужно вызывать listen() или accept(). Это одно из преимуществ использования датаграммных сокетов!
Код talker.c:
/* ** talker.c -- пример датаграммного "клиента" */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #define SERVERPORT 4950 // порт соединения int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // удаленный адрес struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"usage: talker hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // получить информацию о хосте perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // системный порядок their_addr.sin_port = htons(SERVERPORT); // сетевой адрес their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // обнулить остаток if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("отослал %d байт на %s\n", numbytes, inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; } |
Вот и все! Запусти listener на одной машине, talker на другой и смотри как они общаются! Бесконечные часы развлечений для всей семьи!
Еще одна небоьшая деталь: соединенный датаграммные сокеты. Допустим, talker вызывает connect() и указывает адрес listener'а. Теперь talker может обмениваться данными только с адресом, указанным в connect(). Поэтому вместо sendto() и recvfrom(); нужно использовать send() и recv().
Предыдущая | Оглавление | Следующая |