Это клиент-серверный мир, детка. Практически все в Сети состоит из клиентских процессов, которые общаются с серверными процессами, и наоборот. Взять к примеру 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().
|