Сервер Linux с высоким уровнем параллелизма. Часть 2.

Каталог статей

Протокол уровня канала передачи данных — протокол кадра Ethernet — инкапсуляция MAC-адреса

изображение.png

Протокол уровня сетевого интерфейса-протокол ARP

изображение.png

инкапсуляция

изображение.png

  • Транспортный уровень добавляет данные TCP/UDP в заголовок, затем объединяет данные приложения для инкапсуляции и так далее для других уровней.

Разделять

изображение.png

Процесс сетевого общения

  • Если QQ на одном компьютере хочет отправить сообщение QQ на другом компьютере, уровень приложения содержит данные сообщения и заголовок протокола QQ. Эти два параметра инкапсулируются и затем передаются вниз.
  • Если для передачи используется UDP, требуются 16-битный номер порта источника, 16-битный номер порта назначения, 16-битная длина UDP, 16-битная контрольная сумма UDP и данные, поэтому их необходимо добавлять на основе верхний прикладной уровень.
  • Содержимое, инкапсулированное в верхний уровень, используется в качестве данных следующего уровня.Сетевой уровень использует протокол IP.Необходимо добавить такие данные, как номер версии, длина заголовка, IP-адрес (источник и пункт назначения), образуя IP-заголовок + IP-данные. Формат отчета.
  • Затем передается на уровень канала передачи данных через протокол кадра Ethernet, плюс заголовок MAC, проверку CRC и т. д.

Процесс сетевого общения.png

  • Проверьте, соответствует ли MAC-адрес в сообщении MAC-адресу назначения. Если они совпадают, получите сообщение и так далее снизу вверх.
  • Поиск по MAC-адресу Как найти MAC-адрес? Протокол ARP используется
  • Протокол ARP находит MAC-адрес на основе IP-адреса.инкапсуляция запроса arp.pngизображение.png
  • ARP-пакет занимает 28 байт.
  • Также добавьте протокол Ethernet Frame.
  • Вопрос: Нужно ли мне менять IP-адрес на широковещательный адрес при отправке arp-рассылки?
  • О: «Когда хост отправляет информацию, он передает запрос ARP, содержащий целевой IP-адрес, всем хостам в локальной сети и получает ответное сообщение для определения физического адреса цели; после получения обратного сообщения он сохраняет IP-адрес и физический адрес. адрес. Войдите в локальный кэш ARP и сохраните его в течение определенного периода времени. Следующий запрос будет напрямую запрашивать кэш ARP для экономии ресурсов». IP-адрес, упомянутый выше, — это IP-адрес, который будет искать протокол ARP. Он имеет ничего общего с отправленным широковещательным адресом. Не запутайтесь.

Разъемная связь

  • Межсетевое взаимодействие также реализовано, но оно должно быть на разных хостах.
  • Сокет представляет собой комбинацию IP-адреса и порта, а также включает в себя API стека протоколов.

Введение

изображение.png

  • Сокет - это сквозная связь. Нет необходимости учитывать сетевой уровень, уровень канала передачи данных и т. д. Раньше при передаче между каждым уровнем необходимо было добавлять заголовок данных уровня. Теперь сокет напрямую включает в себя весь
  • Меня не волнует, как декапсулировать нижний слой сокета.

изображение.png

Порядок байтов

  • Аккумулятор современного процессора может загружать (как минимум) 4 байта за раз (учитывая здесь 32-битные машины), то есть целое число. Тогда порядок расположения этих 4 байтов в памяти будет влиять на значение целого числа, загружаемого в аккумулятор.Это проблема порядка байтов. В различных компьютерных архитектурах механизмы хранения байтов, слов и т.п. различны, что приводит к очень важной проблеме в области компьютерной коммуникации, а именно единицам информации (битам, байтам, словам, двойникам), которыми обмениваются общающиеся стороны.слова и т.п.) в каком порядке следует передавать. Если согласованные правила не будут достигнуты, стороны связи не смогут выполнить правильное кодирование/декодирование, что приведет к сбою связи.
  • Порядок байтов, как следует из названия, — это порядок, в котором в памяти хранятся данные размером более одного байта (о порядке данных в один байт говорить, конечно, не приходится).
  • Порядок байтов делится на Big-Endian и Little-Endian. Порядок байтов с прямым порядком байтов означает, что старший байт целого числа (23 31 бит) хранится по младшему адресу памяти, а младший байт (0 7 бит) хранится по старшему адресу памяти; порядок байтов с прямым порядком байтов относится к целое число. Старший байт хранится по старшему адресу памяти, а младший байт сохраняется по младшему адресу памяти.

с прямым порядком байтов

изображение.png

/*  
字节序:字节在内存中存储的顺序。
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/

// 通过代码检测当前主机的字节序
#include <stdio.h>

int main() {
    
    

    union {
    
    
    short value;    // 2字节
    char bytes[sizeof(short)];  // char[2]
} test;

    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
    
    
        printf("大端字节序\n");
    } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
    
    
        printf("小端字节序\n");
    } else {
    
    
        printf("未知\n");
    }

    return 0;
}

изображение.png

  • Порядок байтов с прямым порядком байтов: старшие биты данных хранятся в старших битах памяти.
  • Прямой порядок байтов: старшие биты данных хранятся в младших битах памяти.

Преобразование порядка байтов

  • Когда отформатированные данные передаются напрямую между двумя хостами с использованием разного порядка байтов, принимающая сторона должна интерпретировать их неправильно. Способ решения проблемы следующий: передающая сторона всегда преобразует отправляемые данные в данные с обратным порядком байтов перед их отправкой, а принимающая сторона знает, что данные, отправленные другой стороной, всегда имеют обратный порядок байтов. , поэтому принимающая сторона может. Принятый порядок байтов сам по себе определяет, следует ли преобразовывать полученные данные (машина с прямым порядком байтов преобразует, машина с прямым порядком байтов не преобразует)
  • Сетевой порядок байтов — это формат представления данных, указанный в TCP/IP. Он не имеет ничего общего с конкретными типами процессоров, операционными системами и т. д., что обеспечивает правильную интерпретацию данных при передаче между различными хостами. Сетевой порядок байтов Используйте big- сортировка по порядку байтов
  • BSD Socket предоставляет инкапсулированный интерфейс преобразования для удобства программиста. Включая функции преобразования порядка байтов хоста в порядок байтов в сети: htons, htonl; функции преобразования порядка байтов в сети в порядок байтов хоста: ntohs, ntohl

изображение.png

/*

    网络通信时,需要将主机字节序转换成网络字节序(大端字节序),
	(主机字节序可能是小端,,也可能是大端)
    另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。

    // 转换端口
    uint16_t htons(uint16_t hostshort);		// 主机字节序 - 网络字节序
    uint16_t ntohs(uint16_t netshort);		// 主机字节序 - 网络字节序

    // 转IP
    uint32_t htonl(uint32_t hostlong);		// 主机字节序 - 网络字节序
    uint32_t ntohl(uint32_t netlong);		// 主机字节序 - 网络字节序

*/

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    
    

    // htons 转换端口
    unsigned short a = 0x0102;
    printf("a : %x\n", a);
    unsigned short b = htons(a);
    printf("b : %x\n", b);

    printf("=======================\n");

    // htonl  转换IP
    char buf[4] = {
    
    192, 168, 1, 100};
    int num = *(int *)buf;
    int sum = htonl(num);
    unsigned char *p = (char *)&sum;

    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));

    printf("=======================\n");

    // ntohl
    unsigned char buf1[4] = {
    
    1, 1, 168, 192};
    int num1 = *(int *)buf1;
    int sum1 = ntohl(num1);
    unsigned char *p1 = (unsigned char *)&sum1;
    printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
    
     // ntohs


    return 0;
}

Адрес сокета

  • // Адрес сокета на самом деле представляет собой структуру, инкапсулирующую такую ​​информацию, как номер порта и IP. Этот адрес сокета необходимо использовать в последующих API, связанных с сокетами.
  • // Клиент->Сервер (IP, Порт)

Универсальный адрес сокета

  • В интерфейсе программирования сети сокетов адрес сокета представлен структурой sockaddr, которая определяется следующим образом:
#include <bits/socket.h> struct sockaddr {
      
      
	sa_family_t sa_family; 
	char sa_data[14];//只用了6个字节来表示IPv4的地址数据,2+4,2位端口号,4位为IP,剩下8个空在那里
};

typedef unsigned short int sa_family_t;
  • Член sa_family — это переменная типа семейства адресов (sa_family_t). Типы семейства адресов обычно соответствуют типам семейства протоколов. Общие семейства протоколов (также называемые доменом) и соответствующие семейства адресов следующие:
    | Семейство протоколов | Семейство адресов | Описание |
    | — | — | — |
    | PF_UNIX | AF_UNIX | Семейство протоколов локального домена UNIX |
    | PF_INET | AF_INET | TCP/ Набор протоколов IPv4 |
    | PF_INET6 | AF_INET6 | Набор протоколов TCP/IPv6|

  • Макросы PF_* и AF_* определены в заголовочном файле bits/socket.h, причем последний имеет точно такое же значение, что и первый, поэтому их обычно смешивают.

  • Член sa_data используется для хранения значения адреса сокета. Однако значения адреса разных семейств протоколов имеют разное значение и длину, как показано ниже:
    | Семейство протоколов | Значение и длина значения адреса |
    | — | — |
    | PF_UNIX | Путь к файлу, длина может достигать 108 байт |
    | PF_INET | 16-битный номер порта и 32-битный адрес IPv4, всего 6 байт |
    | PF_INET6 | 16-битный номер порта, 32-битный идентификатор потока, 128-битный адрес IPv6, 32-битный идентификатор диапазона, всего 26 байт |

  • Как видно из приведенной выше таблицы, 14-байтовые sa_data просто не могут вместить значения адресов большинства семейств протоколов. Поэтому Linux определяет следующую новую структуру адреса универсального сокета. Эта структура не только обеспечивает достаточно места для хранения значений адреса, но также выравнивается по памяти.

#include <bits/socket.h> struct sockaddr_storage
{
    
    
	sa_family_t sa_family; 
	unsigned long int   ss_align;
	char ss_padding[ 128 - sizeof(__ss_align) ];
};


typedef unsigned short int sa_family_t;

Адрес частного сокета

  • Многие функции сетевого программирования родились раньше протокола IPv4.В то время все они использовали структуру structsockaddr.Для прямой совместимости теперь sockaddr выродился в роль (void*), передавая адрес функции.Что касается того, это функция sockaddr_in или sockaddr_in6, определяемая семейством адресов, а затем функция принудительно преобразует тип в требуемый тип адреса внутри страны.

изображение.png

  • Семейство протоколов локального домена UNIX использует следующую структуру адреса выделенного сокета:
#include <sys/un.h> struct sockaddr_un
{
    
    
sa_family_t sin_family; char sun_path[108];
};

  • Семейство протоколов TCP/IP имеет две выделенные структуры адресов сокетов: sockaddr_in и sockaddr_in6, которые используются для IPv4 и IPv6 соответственно:

изображение.png

  • Все специальные переменные типа адреса сокета (и sockaddr_storage) необходимо преобразовать в общий тип адреса сокета sockaddr при фактическом использовании (просто принудительное преобразование), поскольку тип параметра адреса, используемый всеми интерфейсами программирования сокетов, — sockaddr.

Преобразование IP-адреса (строковое IP-целое число, хост, преобразование порядка сетевых байтов)

  • IP-адреса выражаются в десятичном формате с точками.
  • Обычно люди привыкли использовать читаемые строки для представления IP-адресов, например десятичные строки с точками для представления адресов IPv4 и шестнадцатеричные строки для представления адресов IPv6. Но в программировании нам необходимо преобразовать их в целые (двоичные числа), прежде чем их можно будет использовать. Напротив, при записи логов нам необходимо преобразовать IP-адрес, представленный целым числом, в читаемую строку. Следующие три функции можно использовать для преобразования адресов IPv4, представленных в виде десятичных строк с точками, и адресов IPv4, представленных в виде целых чисел сетевого порядка байтов:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp); 
char *inet_ntoa(struct in_addr in);
  • Следующая пара обновленных функций также может выполнять те же функции, что и предыдущие три функции, и они работают как с адресами IPv4, так и с адресами IPv6:

изображение.png
изображение.png

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数 
//将点分十进制的IP地址转换为网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6 
src:需要转换的点分十进制的IP字符串 
dst:转换后的结果保存在这个里面,传出参数

//  将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 
af:地址族: AF_INET AF_INET6
src: 要转换的IP的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
/*
    #include <arpa/inet.h>
    // p:点分十进制的IP字符串,n:表示network,网络字节序的整数
    int inet_pton(int af, const char *src, void *dst);
        af:地址族: AF_INET  AF_INET6
        src:需要转换的点分十进制的IP字符串
        dst:转换后的结果保存在这个里面

    // 将网络字节序的整数,转换成点分十进制的IP地址字符串
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
        af:地址族: AF_INET  AF_INET6
        src: 要转换的ip的整数的地址
        dst: 转换成IP地址字符串保存的地方
        size:第三个参数的大小(数组的大小)
        返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

*/

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    
    

    // 创建一个ip字符串,点分十进制的IP地址字符串
    char buf[] = "192.168.1.4";
    unsigned int num = 0;

    // 将点分十进制的IP字符串转换成网络字节序的整数
    inet_pton(AF_INET, buf, &num);
    unsigned char * p = (unsigned char *)&num;
    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));


    // 将网络字节序的IP整数转换成点分十进制的IP字符串
    char ip[16] = "";
    const char * str =  inet_ntop(AF_INET, &num, ip, 16);
    printf("str : %s\n", str);
    printf("ip : %s\n", str);
    printf("%d\n", ip == str);

    return 0;
}

TCP-процесс связи

изображение.png

// Процесс связи TCP
// Сторона сервера (роль, которая пассивно принимает соединения)

  1. Создайте сокет для прослушивания (socket())
  • Привязать IP и номер порта (bind())
  • Прослушивание: прослушивание клиентских соединений (listen()).
  • Сокет: этот сокет на самом деле является дескриптором файла.
  1. Привяжите этот дескриптор прослушиваемого файла к локальному IP-адресу и порту (IP-адрес и порт — это адресная информация сервера).
  • Клиент использует этот IP и порт при подключении к серверу (accept())
  1. Настраиваем мониторинг и мониторинг fd начинает работать Мониторинг заключается в отслеживании наличия данных в буфере чтения клиента.
  1. Блокировка и ожидание.Когда клиент инициирует соединение, разблокируйте и примите соединение клиента, вы получите сокет
    (fd) для связи с клиентом.
  2. коммуникация
  • Получить данные
  • Отправить данные (отправить данные в буфер чтения другого хоста)
  1. Связь прервана, отключитесь

// клиент

  1. Создайте сокет (fd) для связи
  2. Для подключения к серверу необходимо указать IP и порт подключенного сервера.
  3. Соединение установлено успешно, и клиент может напрямую общаться с сервером.
  • Получить данные
  • отправить данные
  1. Связь прервана, отключитесь

изображение.png

  • Когда клиент контролирует сервер, он возвращает новый дескриптор файла для связи. Вы не можете использовать отслеживаемый файловый дескриптор для связи. Если вы используете отслеживаемый дескриптор файла, это будет беспорядочно.
  • Вопрос: Я не понимаю, почему серверной стороне нужно привязывать файловый дескриптор серверного сокета к локальному IP и номеру порта, а клиенту это не нужно? Используется ли локальный компьютер в качестве сервера?Как указывается IP-адрес клиента?
  • О: Если клиент не привязан, система автоматически выделит сокет, но сервер должен быть привязан, поскольку send() требует адрес назначения в качестве параметра. Как и при телефонном звонке, вам не нужно знать свой номер, но вы должны знать номер собеседника.

функция сокета

изображение.pngизображение.png

Клиент-серверная связь

Сервис-Терминал

// TCP 通信的服务器端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    
    

    // 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
    
    
        perror("socket");
        exit(-1);
    }

    // 2.绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0
    saddr.sin_port = htons(9999);
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 8);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
    
    if(cfd == -1) {
    
    
        perror("accept");
        exit(-1);
    }

    // 输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);

    // 5.通信
    char recvBuf[1024] = {
    
    0};
    while(1) {
    
    
        
        // 获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1) {
    
    
            perror("read");
            exit(-1);
        } else if(num > 0) {
    
    
            printf("recv client data : %s\n", recvBuf);
        } else if(num == 0) {
    
    
            // 表示客户端断开连接
            printf("clinet closed...");
            break;
        }

        char * data = "hello,i am server";
        // 给客户端发送数据
        write(cfd, data, strlen(data));
    }
   
    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
}

клиент

// TCP通信的客户端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    
    

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
    
    
        perror("connect");
        exit(-1);
    }

    
    // 3. 通信
    char recvBuf[1024] = {
    
    0};
    while(1) {
    
    

        char * data = "hello,i am client";
        // 给客户端发送数据
        write(fd, data , strlen(data));

        sleep(1);
        
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
    
    
            perror("read");
            exit(-1);
        } else if(len > 0) {
    
    
            printf("recv server data : %s\n", recvBuf);
        } else if(len == 0) {
    
    
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

    }

    // 关闭连接
    close(fd);

    return 0;
}

результат операции

изображение.png

  • Настройте отправку данных каждую секунду.

изображение.png

Трехстороннее рукопожатие TCP

  • TCP — это одноадресный протокол, ориентированный на соединение. Трехстороннее рукопожатие — это поведение протокола при установлении соединения, а не поведение программиста. То же самое относится и к четырехстороннему рукопожатию.
  • Трехстороннее рукопожатие происходит при клиентском соединении.
  • TCP — это одноадресный протокол, ориентированный на соединение. Перед отправкой данных обе стороны связи должны установить соединение друг с другом. Так называемое «соединение» на самом деле представляет собой часть информации друг о друге, хранящуюся в памяти клиента и сервера, например IP-адрес, номер порта и т. д.
  • TCP можно рассматривать как поток байтов, который обрабатывает потерю, дублирование и ошибки пакетов на уровне IP или ниже. В процессе установления соединения обеим сторонам необходимо обменяться некоторыми параметрами соединения. Эти параметры можно поместить в заголовок TCP.
  • TCP обеспечивает надежную, ориентированную на соединение службу транспортного уровня с байтовым потоком, использующую трехстороннее рукопожатие для установления соединения. Используйте четыре волны, чтобы закрыть соединение

изображение.pngизображение.png

  • Трехстороннее рукопожатие может подтвердить прием и передачу между клиентом и сервером.

изображение.png

  • Как убедиться в полноте отправленных данных?
  • При отправке они отправляются в определенном порядке, так как же обеспечить согласованность порядка получения?
  • Полноту и последовательность можно просмотреть по серийному номеру и номеру подтверждения.

изображение.png

Первое рукопожатие:
1. Клиент устанавливает флаг SYN на 1.
2. Генерирует случайный 32-битный порядковый номер seq=]. Данные могут передаваться после этого серийного номера (размера данных).
3. Первое рукопожатие не может
передавать второе подтверждение данных :
1. Сервер получает соединение клиента: ACK=1
2. Сервер отправляет обратно порядковый номер подтверждения: ack = порядковый номер клиента + длина данных + SYN/FIN (рассчитывается в одном байте)
3. сервер инициирует запрос на соединение с клиентом: SYN=1
4. Сервер сгенерирует случайный порядковый номер: seq=K
Третье рукопожатие:
1. Клиент отвечает на запрос на соединение сервера: ACK=1
2. Ответ клиента получены данные на стороне сервера: ack = серийный номер на стороне сервера + длина данных + SYN/FIN (рассчитывается как один байт

Скользящее окно TCP

  • Скользящее окно — это технология управления потоками. На ранних этапах сетевой связи стороны напрямую отправляли данные, не принимая во внимание перегрузку сети. Поскольку все не знают о ситуации перегрузки сети и отправляют данные одновременно, промежуточные узлы блокируются и пакеты отбрасываются, и никто не может отправлять данные, поэтому для решения этой проблемы разработан механизм скользящего окна. Протокол скользящего окна — это метод, используемый для повышения пропускной способности, позволяя отправителю передавать дополнительные пакеты до получения каких-либо ответов. Получатель сообщает отправителю, сколько пакетов он может отправить за определенное время (это называется размером окна).
  • TCP использует скользящее окно для управления передачей.Размер скользящего окна означает, какой объем буфера доступен получателю для приема данных. Отправитель может использовать размер скользящего окна, чтобы определить, сколько байтов данных следует отправить. Когда скользящее окно равно 0, отправитель, как правило, не может больше отправлять датаграммы. Скользящее окно — это структура носителя в TCP, которая реализует подтверждение ACK, управление потоком и контроль перегрузки.

изображение.png

  • Буфер отправителя, под буфером понимается память. Белая сетка — это свободное пространство; серая сетка — данные, которые были отправлены, но не получены; фиолетовая сетка — данные, которые еще не были отправлены.
  • В буфере приёмника белая сетка — свободное место, фиолетовая сетка — полученные данные.

изображение.png

  • Понимание первого утверждения и так далее для других утверждений: клиент трехстороннего рукопожатия отправляет флаг SYN, 0 (0) означает, что случайный порядковый номер равен 0, и отправляются 0 данных, win4096 означает, что скользящее окно равно 4096, что означает, что буфер может принять 4096 фрагментов данных, mss1460 означает, что максимальное количество данных в сегменте сообщения равно 1460, win6144 означает, что максимальное скользящее окно принимающей стороны составляет 6144, самый большой фрагмент данных — mss1024, а затем был возвращен ACK 8001. Почему это ACK8001? Поскольку приведенный вами серийный номер — 8000, флаг — 1, прибавьте 1, чтобы получить 8001, и так далее до конца.

изображение.png

  • Под окном понимается размер буфера
  • Размер скользящего окна меняется по мере отправки и получения данных.
  • Обе стороны связи имеют буферы для отправки и получения данных.
  • сервер:
    • Буфер отправки (окно буфера отправки)
    • Буфер приема (окно буфера приема)
  • Клиент:
    • Буфер отправки (окно буфера отправки)
    • Буфер приема (окно буфера приема)

TCP посылает четыре сигнала

  • Когда взаимодействующие стороны отключаются, вызов close() в программе будет использовать протокол TCP для четырехкратного отправки сигнала.
  • И клиент, и сервер могут активно инициировать отключение. Тот, кто первым вызовет close(), инициирует отключение.
  • Потому что когда TCP подключается

изображение.png
изображение.png

TCP-коммуникация, многопроцессная реализация параллельного сервера

  • Чтобы реализовать TCP-сервер связи для обработки параллельных задач, используйте многопотоки или многопроцессы для решения проблемы.
  • Идея:
    • 1 родительский процесс, несколько дочерних процессов
    • Родительский процесс отвечает за ожидание и принятие соединений от клиентов.
    • Подпроцесс: завершить связь, принять клиентское соединение и создать подпроцесс для связи.
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    
    

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
    
    
        perror("connect");
        exit(-1);
    }
    
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
    
    
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
    
    
            perror("read");
            exit(-1);
        } else if(len > 0) {
    
    
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
    
    
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {
    
    
    while(1) {
    
    
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
    
    
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
    
    
            // 还有子进程活着
            break;
        } else if(ret > 0){
    
    
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {
    
    

    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    
    
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {
    
    

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
    
    
            if(errno == EINTR) {
    
    
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
    
    
            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
    
    
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
    
    
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
    
    
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }

    }
    close(lfd);
    return 0;
}
  • Дочерний процесс не может быть переработан в родительском процессе, поэтому вам необходимо зарегистрировать захват сигнала и использовать сигнал sigchld для захвата.

TCP-коммуникация-многопоточность для реализации одновременного сервера

  • Несколько потоков используют одно и то же виртуальное адресное пространство.
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo {
    
    
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;//客户端信息
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];

void * working(void * arg) {
    
    
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
    
    
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
    
    
            perror("read");
            exit(-1);
        }else if(len > 0) {
    
    
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
    
    
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    
    
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
    
    
        bzero(&sockinfos[i], sizeof(sockinfos[i]));//初始化成0
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {
    
    

        struct sockaddr_in cliaddr;//保存客户端接收到的信息
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
    
    
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
    
    
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
    
    
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);

        // 创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
}

Переход состояния TCP

  • Трехстороннее рукопожатие сначала инициируется клиентом: он вызывает функцию Connect() для активного подключения к серверу, а нижний уровень запускает первое рукопожатие.

изображение.pngизображение.png
изображение.png

полузакрытый

  • После четырехкратного помахивания клиентом вызывает close() и возвращается подтверждение. Если сервер не вызывает close(), он не будет помахивать в третий раз. Если сервер не вызывает close() в это время, то в это время Это состояние является полузакрытым состоянием, то есть закрыта только одна сторона.Один конец не может отправлять данные и может только получать данные.Поэтому, если есть такая необходимость, можно использовать полузакрытое состояние .
  • С точки зрения программы вы можете использовать API для управления состоянием полузакрытого соединения.
#include <sys/socket.h>
int shutdown(int sockfd, int how); 
sockfd: 需要关闭的socket的描述符
how:	允许为shutdown操作选择以下几种方式:
SHUT_RD(0):  关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1):  关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。

  • Используйте close, чтобы завершить соединение, но это только уменьшает счетчик ссылок дескриптора и не закрывает соединение напрямую. Соединение закрывается только тогда, когда счетчик ссылок дескриптора равен 0. Shutdown закрывает дескриптор напрямую, независимо от счетчика ссылок дескриптора. Вы также можете выбрать разрыв соединения в одном направлении: только чтение или только запись.
  • Уведомление:
  1. Если сокет используется несколькими процессами, каждый раз при вызове close счетчик будет уменьшаться на 1, пока счетчик не достигнет 0, то есть все процессы вызвали close, и сокет будет освобожден.
  2. В многопроцессном режиме, если один процесс вызывает завершение работы (sfd, SHUT_RDWR), другие процессы не смогут взаимодействовать. Но если закрытие процесса (sfd) не повлияет на другие процессы

Повторное использование портов

изображение.png

параметр:

  • sockfd: дескриптор файла, с которым нужно работать.
  • уровень: уровень - SOL_SOCKET (уровень мультиплексирования портов)
  • optname: имя опции
    • SO_REUSEADDR
    • SO_REUSEPORT
  • optval: значение мультиплексирования портов (в форме)
    • 1: Можно использовать повторно
    • 0: невозможно использовать повторно.
  • optlen: размер параметра optval
  • Повторное использование порта, время настройки — до того, как сервер привяжет порт
  • Сначала вызовите функцию setockopt(), чтобы настроить повторное использование портов.
  • Затем используйте метод связывания() для привязки

Общие параметры команды
netstat
для просмотра информации, связанной с сетью:
-a все сокеты
-p отображает имя программы, использующей сокет
- напрямую использует адрес P, не проходя через сервер доменных имен

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    
    

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    
    
        perror("connect");
        return -1;
    }

    while(1) {
    
    
        char sendBuf[1024] = {
    
    0};
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
    
    
            perror("read");
            return -1;
        }else if(len > 0) {
    
    
            printf("read buf = %s\n", sendBuf);
        } else {
    
    
            printf("服务器已经断开连接...\n");
            break;
        }
    }

    close(fd);

    return 0;
}

#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
    
    
        perror("socket");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);
    
    //int optval = 1;
    //setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    // 绑定
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    
    
        perror("bind");
        return -1;
    }

    // 监听
    ret = listen(lfd, 8);
    if(ret == -1) {
    
    
        perror("listen");
        return -1;
    }

    // 接收客户端连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
    
    
        perror("accpet");
        return -1;
    }

    // 获取客户端信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);

    // 输出客户端的信息
    printf("client's ip is %s, and port is %d\n", cliIp, cliPort );

    // 接收客户端发来的数据
    char recvBuf[1024] = {
    
    0};
    while(1) {
    
    
        int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
        if(len == -1) {
    
    
            perror("recv");
            return -1;
        } else if(len == 0) {
    
    
            printf("客户端已经断开连接...\n");
            break;
        } else if(len > 0) {
    
    
            printf("read buf = %s\n", recvBuf);
        }

        // 小写转大写
        for(int i = 0; i < len; ++i) {
    
    
            recvBuf[i] = toupper(recvBuf[i]);
        }

        printf("after buf = %s\n", recvBuf);

        // 大写字符串发给客户端
        ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
        if(ret == -1) {
    
    
            perror("send");
            return -1;
        }
    }
    
    close(cfd);
    close(lfd);

    return 0;
}

изображение.png

  • Почему на стороне сервера два сокета? Один используется для мониторинга, а другой — для связи, поэтому их статус также различен.

изображение.png

  • Когда состояние находится в режиме ожидания, время ожидания сообщения будет вдвое больше.
  • Если порт не будет освобожден в течение 1 минуты, привязка не может быть продолжена.

изображение.png

  • После добавления повторного использования портов об ошибках не сообщается, и их можно привязать.

изображение.png

Мультиплексирование ввода-вывода (мультиплексирование ввода-вывода)

  • У обеих сторон связи есть SOCKET, который соответствует буферу в ядре.IO — это операция буфера. Например, запись осуществляется из буфера записи. При записи данные передаются в буфер чтения другой стороны. Чтение происходит из буфера чтения.
  • Мультиплексирование ввода-вывода позволяет программе одновременно отслеживать несколько файловых дескрипторов, что может повысить производительность программы.Системные вызовы для реализации мультиплексирования ввода-вывода в Linux в основном включают select, poll и epoll.
  • Если много клиентов отправляют мне сообщения, и я не знаю, какой клиент их отправил, нужно ли мне просматривать их файловые дескрипторы? Да, это был предыдущий подход, обход дескрипторов файлов один за другим, но теперь, используя мультиплексирование ввода-вывода, вы можете одновременно отслеживать
  • Мультиплексирование ввода-вывода предназначено для проверки наличия данных в буфере чтения и записи файлового дескриптора.

Модель ввода-вывода

блокировка ожидания

  • Функции чтения, функции get, Accept() и Recv() заблокированы.Чтение всегда ожидает данных, что неэффективно.

изображение.png

  • При блокировке не используется временной интервал
  • Используйте многопоточность и многопроцессность для решения проблем параллелизма.

БИО-модель

изображение.png

Неблокирующий, занятый опрос

изображение.png

Модель НИО

изображение.png

Мультиплексирование ввода-вывода

изображение.png

  • Делегировать завершение ядра

изображение.png

  • Раньше было несколько путей, но теперь есть только один путь. Первоначально его нужно было пройти один за другим, чтобы увидеть, есть ли в дескрипторе файла данные. Теперь он передается дескриптору файла, и он может пройти по нему с помощью сам.

выбирать

  • Основная идея:
    • Во-первых, вам необходимо создать список дескрипторов файлов и добавить в него дескрипторы файлов, которые будут отслеживаться.
    • Вызов системной функции для мониторинга файловых дескрипторов в списке. Функция не завершает работу до тех пор, пока один или несколько из этих дескрипторов не выполнят операции ввода-вывода: а. Эта функция блокирует б. Функция блокирует файловый дескриптор. Операция обнаружения
      завершена
      . по ядру
    • По возвращении он сообщает процессу, сколько (каких) дескрипторов требуют операций ввода-вывода.

изображение.png

  • Следующие 1024 бита, каждый бит представляет собой дескриптор файла.
  • Буфер чтения проверяется на наличие в нем данных, а буфер записи проверяется на наличие свободного места . Если есть свободное место, в него можно записать данные. Если есть свободные данные, соответствующая позиция флага устанавливается в 1.
  • Точно так же то же самое верно и для обнаружения буферов чтения. Установите соответствующую позицию флага на 1, а затем определите, когда он встретит 1. Когда он равен 0, он не обнаружит его. Пока он вызывается однажды он будет знать, какие файловые дескрипторы содержат данныеизображение.pngизображение.png

Содержание работы

изображение.png

  • Что касается объяснения, почему select нужен +1, потому что select проходит с начала, от 0 до 101, если длина всего 101, то он может проходить только от 0 до 100, поэтому +1 нужен
  • Чтение — это указатель, передаваемый в параметры и из них. Он отслеживает, какой бит изменился. Например, если он отслеживает 3, его позиция равна 1. Пользовательский режим может перемещаться по этой коллекции. Если он переходит к 3, если есть данные, будет описано из файла 3. символ для чтения данных, а остальное читать не нужно, если данных нет. Здесь используется мониторинг fd_set readsбуфера чтения. При обнаружении коллекция будет скопирована из состояние пользователя в состояние ядра, а затем коллекция будет пройдена, чтобы увидеть, какой из них необходимо обнаружить. Необходимо определить, равен ли бит флага 1. Если в это время есть данные в 3 и 4, Бит флага остается равным 1. Если в это время нет данных в 100 и 101, бит флага изменяется с 1 на 0, указывая, что данные не поступили. Затем скопируйте из состояния ядра в состояние пользователя. Теперь мы знаем что 3 имеет данные, а затем снова проходит по ним, а затем в 3 для чтения данных используются read и Recv. Чтобы определить, равен ли бит флага 1, вы можете FD_ISSET(3,&reads)использовать
  • В следующем цикле, если клиент, соответствующий номеру 10, завершится и отключится, он больше не будет обнаруживаться ядром и будет FD_CLR(100,&reads)очищен.

Код

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    fd_set rdset, tmp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {
    
    

        tmp = rdset;

        // 调用select系统函数,让内核帮检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1) {
    
    
            perror("select");
            exit(-1);
        } else if(ret == 0) {
    
    
            continue;
        } else if(ret > 0) {
    
    
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(FD_ISSET(lfd, &tmp)) {
    
    
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }

            for(int i = lfd + 1; i <= maxfd; i++) {
    
    
                if(FD_ISSET(i, &tmp)) {
    
    
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {
    
    0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
    
    
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
    
    
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
    
    
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    
    

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    
    
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
    
    
        char sendBuf[1024] = {
    
    0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
    
    
            perror("read");
            return -1;
        }else if(len > 0) {
    
    
            printf("read buf = %s\n", sendBuf);
        } else {
    
    
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        usleep(1000);
    }

    close(fd);

    return 0;
}

изображение.png

  • Обеспечьте многоклиентское соединение с помощью select, это технология мультиплексирования ввода-вывода.

изображение.png
изображение.png

голосование

  • Улучшите выбор и инкапсулируйте fds в массив структур, который представляет собой набор файловых дескрипторов, которые необходимо обнаружить.

изображение.png

  • Сосредоточьтесь на обнаружении событий чтения
  • Когда обнаруживаются события чтения и записи,myfd.events = POLLIN | POLLOUT;

изображение.png

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
    
    
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;//要监听的文件描述符
    int nfds = 0;

    while(1) {
    
    

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
    
    
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
    
    
            continue;
        } else if(ret > 0) {
    
    
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {
    
    
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                for(int i = 1; i < 1024; i++) {
    
    
                    if(fds[i].fd == -1) {
    
    
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }

            for(int i = 1; i <= nfds; i++) {
    
    
                if(fds[i].revents & POLLIN) {
    
    
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {
    
    0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
    
    
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
    
    
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
    
    
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}

изображение.png

эполл

  • Сначала создайте экземпляр epoll в области ядра.
  • Структурой области ядра можно управлять через файловый дескриптор.

изображение.png

  • Управляйте ядром напрямую, без копирования или переключения из пользовательского режима в режим ядра.
  • rbr — это красно-черная древовидная структура данных, которая заменяет исходную структуру линейного массива, ускоряет обход и значительно повышает эффективность.
  • rdlist — двусвязный список

изображение.pngизображение.png

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);//参数随便给一个值

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
    //第一个是epfd,第二个是要操作的类型,第三个是要添加的文件描述符信息,第四个是event

    struct epoll_event epevs[1024];//把发生改变的文件描述符信息装到这里,即内核检测完的数据,直接遍历就行

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);//-1表示阻塞
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
    
    

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);//返回的通信文件描述符,这不能和监听文件描述符混用

                epev.events = EPOLLIN; //EPOLLIN | EPOLLOUT
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);//添加到实例当中
            } else {
    
    //我们的逻辑是只有读检测到了事件,才能顺应接下来的逻辑,如果是写到来,则不适用
                //EPOLLIN | EPOLLOUT,所以要加上这个判断,要根据返回来的events进行具体操作
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {
    
    0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
    
    
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);//关闭连接删除用NULL
                    close(curfd);
                } else if(len > 0) {
    
    //说明读到数据了,然后把数据写出来
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

изображение.png

режим работы эполла

  • Горизонтальный запуск является рабочим режимом по умолчанию. Режим работы по умолчанию всегда будет уведомлять вас, пока в буфере есть данные.
  • Запуск по краям поддерживает только неблокировку и уведомляет только один раз.

изображение.png

Триггер уровня LT
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
    
    

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[5] = {
    
    0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
    
    
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
    
    
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

изображение.png

  • Из эффекта выполнения видно, что пока в буфере есть данные, этот ret всегда будет обновляться.
Триггер по краю ET
  • Используется вместе с неблокирующими API.
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
    
    

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag | O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                epev.events = EPOLLIN | EPOLLET;    // 设置边沿触发
                epev.data.fd = cfd;//有数据只会通知一次
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }  

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while( (len = read(curfd, buf, sizeof(buf))) > 0) {
    
    
                    // 打印数据
                    // printf("recv data : %s\n", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
    
    
                    printf("client closed....");
                }else if(len == -1) {
    
    
                    if(errno == EAGAIN) {
    
    
                        printf("data over.....");
                    }else {
    
    
                        perror("read");
                        exit(-1);
                    }
                    
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

изображение.png

  • Поскольку ret, вызванный фронтом, будет обновляться только один раз, после завершения подцикла вы вернетесь к циклу while и будете заблокированы в int ret = epoll_wait(epfd, epevs, 1024, -1);нем, поэтому он не будет отображаться позже.
  • После улучшения

изображение.png

Ошибка соединения отклонена

изображение.png
изображение.png

  • Это связано с 2MSL, подождите 1 минуту.
  • Поскольку клиент здесь только что был заблокирован, сервер останавливается первым. Тот, кто остановится первым, будет находиться в состоянии TIME_WAIT. Вам придется подождать 1 минуту, а затем снова подключиться.

изображение.png

UDP

  • UDP обменивается данными посредством дейтаграмм и не требует обеспечения безопасности данных.
  • UDP-сокет не похож на TCP-сокет, который обеспечивает мониторинг и связь. Это непосредственно сокет для связи.

изображение.png

UDP-связь

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
    
    

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }   

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    // 2.绑定
    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
    
    
        char recvbuf[128];
        char ipbuf[16];

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);

        // 接收数据
        int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);

        printf("client IP : %s, Port : %d\n", 
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
            ntohs(cliaddr.sin_port));

        printf("client say : %s\n", recvbuf);

        // 发送数据
        sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    }

    close(fd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
    
    

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }   

    // 服务器的地址信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);

    int num = 0;
    // 3.通信
    while(1) {
    
    

        // 发送数据
        char sendBuf[128];
        sprintf(sendBuf, "hello , i am client %d \n", num++);
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));

        // 接收数据
        int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
        printf("server say : %s\n", sendBuf);

        sleep(1);
    }

    close(fd);
    return 0;
}

изображение.pngизображение.png

  • UDP не требует многопоточности/многопроцессности, поэтому с сервером могут взаимодействовать несколько клиентов.

транслировать

  • Используется в локальной сети
  • Отправьте сообщение один раз, и все компьютеры в локальной сети смогут получить сообщение.

изображение.png

  • Широковещательный адрес должен установить все биты флага адреса хоста в 1, что равно 255.
  • Теперь клиенту также необходимо привязать порт, используемый сервером, а IP-адрес используется в качестве широковещательного адреса.
  • Сделайте еще две вещи: сервер устанавливает атрибут широковещания, а клиент привязывает номер порта широковещательных данных сервера.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
    
    

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }   

    // 2.设置广播属性
    int op = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));
    
    // 3.创建一个广播的地址
    struct sockaddr_in cliaddr;
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(9999);
    inet_pton(AF_INET, "192.168.193.255", &cliaddr.sin_addr.s_addr);

    // 3.通信
    int num = 0;
    while(1) {
    
    
       
        char sendBuf[128];
        sprintf(sendBuf, "hello, client....%d\n", num++);
        // 发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        printf("广播的数据:%s\n", sendBuf);
        sleep(1);
    }

    close(fd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
    
    

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }   

    struct in_addr in;

    // 2.客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
    
    
        
        char buf[128];
        // 接收数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("server say : %s\n", buf);

    }

    close(fd);
    return 0;
}

изображение.png
изображение.png

  • Если клиент открывается поздно, данные могут быть потеряны и это небезопасно, как показано на рисунке. Если клиент открыт поздно, прием начнется в 28.

изображение.png

  • Компьютер нельзя привязать повторно, но его можно снова привязать на новом компьютере, как показано на рисунке, к двум разным хостам.

изображение.png

Многоадресная рассылка (многоадресная рассылка)

  • Идентифицирует группу IP-интерфейсов
  • К трансляции нужно добавить еще несколько шагов
  • Настройте многоадресную рассылку на сервере и присоединитесь к группе многоадресной рассылки на клиенте.

изображение.png
изображение.png

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
    
    

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }   

    // 2.设置多播的属性,设置外出接口
    struct in_addr imr_multiaddr;
    // 初始化多播地址
    inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
    setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));
    
    // 3.初始化客户端的地址信息
    struct sockaddr_in cliaddr;
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(9999);
    inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);

    // 3.通信
    int num = 0;
    while(1) {
    
    
       
        char sendBuf[128];
        sprintf(sendBuf, "hello, client....%d\n", num++);
        // 发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        printf("组播的数据:%s\n", sendBuf);
        sleep(1);
    }

    close(fd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
    
    

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }   

    struct in_addr in;
    // 2.客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    struct ip_mreq op;
    inet_pton(AF_INET, "239.0.0.10", &op.imr_multiaddr.s_addr);
    op.imr_interface.s_addr = INADDR_ANY;

    // 加入到多播组
    setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof(op));

    // 3.通信
    while(1) {
    
    
        
        char buf[128];
        // 接收数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("server say : %s\n", buf);

    }

    close(fd);
    return 0;
}

изображение.pngизображение.png

  • Оба хоста могут получать

связь по локальному сокету

  • Функция — это связь локальных процессов (в хосте).Вышеупомянутые называются сетевыми сокетами, то есть связь процессов между разными хостами.
  • Процесс реализации аналогичен реализации сетевого сокета с использованием TCP.

изображение.pngизображение.png

  • Используйте AF_LOCAL для локального общения
  • Локально используйте sockaddr_un
  • Принцип работы местных розеток очень похож на принцип работы знаменитых труб.

изображение.png

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {
    
    

    unlink("server.sock");

    // 1.创建监听的套接字
    int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);//本地套接字用LOCAL
    if(lfd == -1) {
    
    
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "server.sock");//因为数组名是指针常量,是不能被修改的,所以用strcpy
    int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 100);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 4.等待客户端连接
    struct sockaddr_un cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
    
    
        perror("accept");
        exit(-1);
    }

    printf("client socket filename: %s\n", cliaddr.sun_path);

    // 5.通信
    while(1) {
    
    

        char buf[128];
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
    
    
            perror("recv");
            exit(-1);
        } else if(len == 0) {
    
    
            printf("client closed....\n");
            break;
        } else if(len > 0) {
    
    
            printf("client say : %s\n", buf);
            send(cfd, buf, len, 0);
        }

    }

    close(cfd);
    close(lfd);

    return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {
    
    

    unlink("client.sock");

    // 1.创建套接字
    int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(cfd == -1) {
    
    
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "client.sock");
    int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 3.连接服务器
    struct sockaddr_un seraddr;
    seraddr.sun_family = AF_LOCAL;
    strcpy(seraddr.sun_path, "server.sock");
    ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret == -1) {
    
    
        perror("connect");
        exit(-1);
    }

    // 4.通信
    int num = 0;
    while(1) {
    
    

        // 发送数据
        char buf[128];
        sprintf(buf, "hello, i am client %d\n", num++);
        send(cfd, buf, strlen(buf) + 1, 0);
        printf("client say : %s\n", buf);

        // 接收数据
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
    
    
            perror("recv");
            exit(-1);
        } else if(len == 0) {
    
    
            printf("server closed....\n");
            break;
        } else if(len > 0) {
    
    
            printf("server say : %s\n", buf);
        }

        sleep(1);

    }

    close(cfd);
    return 0;
}

изображение.png

  • Будут созданы соответствующие псевдофайлы.

изображение.png

  • Каждый раз при запуске сначала удаляйте фиктивный файл, а затем перепривязывайте его.

Веб сервер

Блокирующий/неблокирующий, синхронный/асинхронный

  • В основном нацелен на сетевой ввод-вывод.
  • два этапа

Блокирующий, неблокирующий, синхронный, асинхронный.png

  • Sockfd здесь может быть блокирующим или неблокирующим.
  • EINTER, EAGAIN и т.п. не являются ошибками, поэтому их нужно судить.
  • buf читается нами самими, поэтому он синхронизируется
  • Например, если вы покупаете билет и идете в аэропорт, чтобы распечатать посадочный талон, это синхронно; асинхронно означает, что когда билет выдается и аэропорт передает его вам, это асинхронно; асинхронно более эффективно и принимает данные других людей время
  • При работе с вводом-выводом и блокирующий, и неблокирующий ввод-вывод являются синхронными. Только при использовании специального API используется асинхронный ввод-вывод.

Модель ввода-вывода (расширение мультиплексирования ввода-вывода)

  • Является ли API мультиплексирования ввода-вывода синхронным или асинхронным? Использование epoll позволяет обнаружить только поступление данных, а все конкретные операции чтения (чтение, получение) являются синхронными операциями ввода-вывода. Помните одно предложение: при работе с вводом-выводом блокирующий и неблокирующий ввод-вывод являются синхронным вводом-выводом. Только когда используется специальный API, это асинхронный ввод-вывод, поэтому API мультиплексирования ввода-вывода является синхронным.

изображение.png

Блокировка

  • Вызывающая функция вызывает функцию и ждет ее возврата, в течение этого периода он ничего не делает и постоянно проверяет, вернулась ли функция.

Прежде чем переходить к следующему шагу, необходимо дождаться возврата этой функции.Это атрибут файлового дескриптора.
изображение.png

Неблокирующий (NIO)

  • Неблокирующее ожидание, время от времени проверяющее готовность события ввода-вывода. Не будьте готовы делать другие вещи. Системные вызовы неблокирующего ввода-вывода всегда возвращаются немедленно, независимо от того, произошло ли событие. Если событие не произошло, возвращается -1. В это время две ситуации можно отличить по errno. Для принятия, Recv и send, событие не произошло. errno обычно имеет значение EAGAIN.

изображение.png

Мультиплексирование ввода-вывода

  • Linux использует функцию select/poll/epoll для реализации модели мультиплексирования ввода-вывода.Эти функции также блокируют процесс, но отличие от блокировки ввода-вывода состоит в том, что эти функции могут блокировать несколько операций ввода-вывода одновременно. Более того, функции ввода-вывода нескольких операций чтения и записи могут быть обнаружены одновременно. Функция операции ввода-вывода фактически не вызывается до тех пор, пока не появятся данные для чтения или записи.
  • Целью повторного использования ввода-вывода является одновременное обнаружение нескольких клиентских событий (в одном процессе).

изображение.png

Управляемый сигналом

  • Linux использует сокеты для ввода-вывода, управляемого сигналами, и устанавливает функцию обработки сигналов. Процесс продолжает выполняться без блокировки. Когда событие ввода-вывода будет готово, продолжайте

Процесс получает сигнал SIGIO, а затем обрабатывает событие ввода-вывода.
изображение.png

  • Ядро является асинхронным на первом этапе и синхронным на втором этапе; отличие от неблокирующего ввода-вывода заключается в том, что оно предоставляет механизм уведомления о сообщениях, который не требует от пользовательского процесса постоянного опроса и проверки, что уменьшает количество системных API. звонки и повышение эффективности

Асинхронный

  • В Linux вы можете вызвать функцию aio_read, чтобы сообщить ядру указатель буфера дескриптора и размер буфера, смещение файла и метод уведомления, а затем немедленно вернуться.Когда ядро ​​копирует данные в буфер, оно уведомляет приложение.

изображение.png
изображение.png

Введение в веб-сервер (Веб-сервер)

  • Веб-сервер — это серверное программное обеспечение (программа) или оборудование (компьютер), на котором работает серверное программное обеспечение. Его основная функция — связь с клиентом (обычно браузером) через протокол HTTP для получения, хранения и обработки HTTP-запросов от клиента, создания HTTP-ответов на его запросы и возврата клиенту запрошенной информации. , веб-страницы и т. д.) или вернуть сообщение об ошибке.

изображение.png

  • Обычно пользователи используют веб-браузер для связи с соответствующим сервером. Введите «имя домена» или «IP-адрес:номер порта» в браузере, и браузер сначала преобразует ваше доменное имя в соответствующий IP-адрес или напрямую отправит HTTP-запрос на соответствующий веб-сервер на основе вашего IP-адреса. Этот процесс сначала устанавливает соединение с целевым веб-сервером посредством трехэтапного подтверждения протокола TCP, а затем использует протокол HTTP для генерации сообщения HTTP-запроса для целевого веб-сервера, которое отправляется на целевой веб-сервер через TCP. , IP и другие протоколы.

HTTP

  • Протокол передачи гипертекста (HTTP) — это простой протокол запроса-ответа, который обычно работает поверх TCP. Он определяет, какие сообщения клиент может отправлять на сервер и какой ответ он получает. Заголовки сообщений запроса и ответа задаются в форме ASCII, содержимое сообщения имеет формат MIME. HTTP является основой передачи данных во Всемирной паутине.

принцип

  • Протокол HTTP определяет, как веб-клиент запрашивает веб-страницу у веб-сервера и как сервер доставляет веб-страницу клиенту. Протокол HTTP использует модель запрос/ответ. Клиент отправляет серверу сообщение запроса. Сообщение запроса содержит метод запроса, URL-адрес, версию протокола, заголовок запроса и данные запроса. Сервер отвечает строкой состояния, которая включает версию протокола, код успеха или ошибки, информацию о сервере, заголовки ответа и данные ответа.
  • Ниже приведены шаги для HTTP-запроса/ответа:
  1. Соединение клиента с веб-сервером
    HTTP-клиент, обычно браузер, устанавливает соединение TCP-сокета с HTTP-портом веб-сервера (по умолчанию — 80). Например, http://www.baidu.com. (URL-адрес)
  2. Чтобы отправить HTTP-запрос
    через TCP-сокет, клиент отправляет на веб-сервер текстовое сообщение запроса. Сообщение запроса состоит из четырех частей: строки запроса, заголовка запроса, пустой строки и данных запроса.
  3. Сервер принимает запрос и возвращает ответ HTTP.
    Веб-сервер анализирует запрос и находит запрошенный ресурс. Сервер записывает копию ресурса в сокет TCP, который читается клиентом. Ответ состоит из 4 частей: строка состояния, заголовок ответа, пустая строка и данные ответа.
  4. Освободите соединение TCP-соединение.
    Если режим соединения закрыт, сервер активно закрывает TCP-соединение, а клиент пассивно закрывает соединение и освобождает TCP-соединение. Если режим соединения поддерживает активность, соединение будет поддерживаться в течение периода время, и запросы могут продолжать поступать в течение этого времени;
  5. Браузер клиента анализирует содержимое HTML
    Клиентский браузер сначала анализирует строку состояния, чтобы увидеть код состояния, указывающий, был ли запрос успешным. Затем каждый заголовок ответа анализируется, и заголовок ответа сообщает следующий HTML-документ, который представляет собой количество байтов, и набор символов документа. Браузер клиента считывает данные ответа в формате HTML, форматирует их в соответствии с синтаксисом HTML и отображает в окне браузера.
    Например: введите URL-адрес в адресную строку браузера и нажмите Enter. Вы выполните следующий процесс:
  6. Браузер запрашивает DNS-сервер для разрешения IP-адреса, соответствующего имени домена в URL-адресе;
  7. После анализа IP-адреса установите TCP-соединение с сервером на основе IP-адреса и порта 80 по умолчанию;
  8. Браузер выдает HTTP-запрос на чтение файла (файл, соответствующий части после имени домена в URL-адресе), и сообщение запроса отправляется на сервер как данные третьего сообщения трехстороннего рукопожатия TCP;
  9. Сервер отвечает на запрос браузера и отправляет браузеру соответствующий HTML-текст;
  10. Освободите TCP-соединение;
  11. Браузер преобразует текст HTML и отображает содержимое.

изображение.png

  • Протокол HTTP — это протокол прикладного уровня, основанный на протоколе TCP/IP и основанный на модели запрос-ответ. Протокол HTTP предусматривает, что от клиента выдается запрос, и, наконец, сервер отвечает на запрос и возвращает результат. Другими словами, сначала должна быть установлена ​​связь с клиентом, и сервер не будет отправлять ответ, пока не получит запрос.

Сообщение HTTP-запроса

изображение.png

  • Вы можете увидеть сообщение запроса через браузер

изображение.png

HTTP-ответное сообщение

изображение.png

HTTP-запрос

  • Протокол HTTP/1.1 определяет в общей сложности восемь методов (также называемых «действиями») для управления указанными ресурсами различными способами:
  1. GET: Делает запрос «показать» к указанному ресурсу. Метод GET следует использовать только для чтения данных и не следует использовать в операциях, вызывающих «побочные эффекты», например в веб-приложениях. Одна из причин заключается в том, что GET может быть
    доступен
  2. HEAD: Как и метод GET, он отправляет запрос указанного ресурса на сервер. Просто текстовую часть ресурса сервер не вернет. Преимущество состоит в том, что с помощью этого метода можно получить «информацию о ресурсе» (метаинформацию или метаданные) без необходимости передавать весь контент.
  3. POST: отправьте данные на указанный ресурс и запросите сервер для обработки (например, отправки формы или загрузки файла). Данные включены в статью запроса. Этот запрос может создать новый ресурс или изменить существующий ресурс, или и то, и другое.
  4. PUT: загрузите последний контент в указанное расположение ресурса.
  5. УДАЛИТЬ: запросить у сервера удаление ресурса, определенного Request-URI.
  6. TRACE: Отображение запроса, полученного сервером, в основном используемого для тестирования или диагностики.
  7. ОПЦИИ: этот метод позволяет серверу возвращать все методы HTTP-запросов, поддерживаемые ресурсом. Используйте «*», чтобы заменить имя ресурса, и отправьте запрос OPTIONS на веб-сервер, чтобы проверить, правильно ли работает функция сервера.
  8. ПОДКЛЮЧЕНИЕ: зарезервировано в протоколе HTTP/1.1 для прокси-серверов, которые могут изменять подключения к конвейерам. Обычно используется для подключений к серверам с шифрованием SSL (через незашифрованные HTTP-прокси).
  • Часто используемый GET/POST

Базовая основа серверного программирования

  • Хотя существует множество типов серверных программ, их базовая структура одинакова, разница заключается в логической обработке.

изображение.png

модуль Функция
Блок обработки ввода-вывода Обработка клиентских подключений, чтение и запись сетевых данных
логическая единица Бизнес-процесс или поток
сетевой накопитель База данных, файл или кэш
очередь запросов Методы связи между подразделениями
  • Процессор ввода-вывода — это модуль сервера, который управляет клиентскими соединениями. Обычно он выполняет следующие задачи: ожидает и принимает новые клиентские соединения, получает данные клиента и возвращает клиенту данные ответа сервера. Однако отправка и прием данных не обязательно могут выполняться в блоке обработки ввода-вывода, но также могут выполняться в логическом блоке.Конкретное местоположение зависит от режима обработки событий.
  • Логической единицей обычно является процесс или поток. Он анализирует и обрабатывает данные клиента, а затем передает результаты в блок обработки ввода-вывода или непосредственно клиенту (какой метод используется, зависит от режима обработки событий). Серверы обычно имеют несколько логических единиц, позволяющих одновременно обрабатывать несколько клиентских задач.
  • Сетевыми хранилищами могут быть базы данных, кэши и файлы, но это не обязательно.
  • Очередь запросов — это абстракция того, как взаимодействуют устройства. Когда блок обработки ввода-вывода получает запрос клиента, ему необходимо каким-либо образом уведомить логическое устройство о необходимости обработки запроса. Аналогичным образом, когда несколько логических устройств одновременно обращаются к устройству хранения, также требуется некоторый механизм для координации и обработки состояний гонки. Очереди запросов обычно реализуются как часть пула. (Пул: пул процессов и пул потоков)

Модели обработки событий двух университетов

  • Серверным программам обычно необходимо обрабатывать три типа событий: события ввода-вывода, сигналы и синхронизированные события. Существует два эффективных режима обработки событий: Reactor и Proactor.Модель синхронного ввода-вывода обычно используется для реализации режима Reactor, а модель асинхронного ввода-вывода обычно используется для реализации режима Proactor.

Схема реактора

Основной поток (блок обработки ввода-вывода) должен только отслеживать, происходит ли событие в файловом дескрипторе. Если это так, он немедленно уведомляет рабочий поток (логический модуль) о событии и помещает события сокета, доступные для чтения и записи, в очередь запросов.Передайте ее рабочему потоку. Помимо этого, основной поток не выполняет никакой другой основной работы. Чтение и запись данных, принятие новых подключений и обработка запросов клиентов — все это выполняется в рабочих потоках.
Рабочий процесс режима Reactor, реализованный с использованием синхронного ввода-вывода (на примере epoll_wait):

  1. Главный поток регистрирует событие готовности к чтению на сокете в таблице событий ядра epoll .
  2. Основной поток вызывает epoll_wait, чтобы дождаться чтения данных в сокете.
  3. Когда в сокете есть данные для чтения, epoll_wait уведомляет основной поток. Основной поток помещает событие, доступное для чтения через сокет, в очередь запросов.
  4. Рабочий поток, спящий в очереди запросов, пробуждается, считывает данные из сокета, обрабатывает клиентский запрос, а затем
    регистрирует событие готовности к записи в сокете в таблице событий ядра epoll.
  5. Когда основной поток вызывает epoll_wait, он ожидает, пока сокет станет доступен для записи.
  6. Когда сокет доступен для записи, epoll_wait уведомляет основной поток. Основной поток помещает событие, доступное для записи сокета, в очередь запросов.
  7. Рабочий поток, спящий в очереди запросов, пробуждается и записывает результат обработки клиентского запроса сервером в сокет.

изображение.png

Режим проактора

В режиме Proactor все операции ввода-вывода передаются основному потоку и ядру для обработки (чтение и запись), а рабочий поток отвечает только за бизнес-логику. Рабочий процесс режима Proactor, реализованный с использованием модели асинхронного ввода-вывода (в качестве примера взяты aio_read и aio_write):

  1. Основной поток вызывает функцию aio_read, чтобы зарегистрировать событие завершения чтения в сокете с ядром, и сообщает пользователю ядра расположение буфера чтения и способ уведомить приложение о завершении операции чтения (здесь примите сигнал как пример).
  2. Основной поток продолжает обрабатывать другую логику.
  3. Когда данные из сокета считываются в пользовательский буфер, ядро ​​отправляет приложению сигнал, уведомляющий приложение о доступности данных.
  4. Предопределенная функция обработки сигналов приложения выбирает рабочий поток для обработки клиентского запроса. После того, как рабочий поток обрабатывает запрос клиента, он вызывает функцию aio_write, чтобы зарегистрировать событие завершения записи в сокете с ядром, и сообщает пользователю ядра расположение буфера записи и способ уведомить приложение о завершении операции записи. .
  5. Основной поток продолжает обрабатывать другую логику.
  6. Когда данные из пользовательского буфера записываются в сокет, ядро ​​отправляет сигнал приложению, чтобы уведомить приложение об отправке данных.
  7. Функция обработки сигналов, предопределенная приложением, выбирает рабочий поток для последующей обработки, например принятия решения о закрытии сокета.

изображение.png

  • Разница в том, что в режиме R основной поток отвечает только за мониторинг, а в режиме P все остается на усмотрение основного потока.

Имитировать режим Proactor

Используйте метод синхронного ввода-вывода для имитации режима Proactor. Принцип таков: основной поток выполняет операции чтения и записи данных.После завершения чтения и записи основной поток уведомляет рабочий поток об этом «событии завершения». Таким образом, с точки зрения рабочих потоков, они напрямую получают результаты чтения и записи данных, и следующее, что нужно сделать, — это логически обработать результаты чтения и записи.
Рабочий процесс режима Proactor, смоделированный с использованием модели синхронного ввода-вывода (на примере epoll_wait), выглядит следующим образом:

  1. Главный поток регистрирует событие готовности к чтению в сокете в таблице событий ядра epoll.
  2. Основной поток вызывает epoll_wait, чтобы дождаться чтения данных в сокете.
  3. Когда в сокете есть данные для чтения, epoll_wait уведомляет основной поток. Основной поток считывает данные из сокета в цикле до тех пор, пока не останется данных для чтения, а затем инкапсулирует прочитанные данные в объект запроса и вставляет его в очередь запросов.
  4. Рабочий поток, спящий в очереди запросов, пробуждается, получает объект запроса и обрабатывает запрос клиента, а затем регистрирует событие готовности к записи в сокете в таблице событий ядра epoll.
  5. Основной поток вызывает epoll_wait, чтобы дождаться, пока сокет станет доступен для записи.
  6. Когда сокет доступен для записи, epoll_wait уведомляет основной поток. Основной поток записывает результат обработки сервером клиентского запроса в сокет.

изображение.png

Пул потоков

  • Нехорошо создавать тред, когда приходит клиент.
  • Пул потоков — это группа подпотоков, предварительно созданных сервером. Число потоков в пуле потоков должно быть примерно таким же, как количество процессоров. Все дочерние потоки в пуле потоков выполняют один и тот же код. Когда поступает новая задача, основной поток выберет подпоток в пуле потоков, чтобы каким-либо образом ее обслужить. По сравнению с динамическим созданием подпотоков, стоимость выбора существующего подпотока, очевидно, намного меньше. Что касается того, какой подпоток основной поток выберет для обслуживания новой задачи, существует множество способов:
  • Основной поток использует некоторый алгоритм для активного выбора дочерних потоков. Самыми простыми и наиболее часто используемыми алгоритмами являются случайный алгоритм и алгоритм RoundRobin, но более совершенные и умные алгоритмы распределяют задачи более равномерно между различными рабочими потоками, тем самым снижая общую нагрузку на сервер.
  • Основной поток и все дочерние потоки синхронизируются через общую рабочую очередь, а дочерние потоки находятся в рабочей очереди. Когда поступает новая задача, основной поток добавляет ее в рабочую очередь. Это разбудит дочерние потоки, ожидающие выполнения задачи, но только один дочерний поток получит «право на управление» новой задачей. Он может взять задачу из рабочей очереди и выполнить ее, в то время как другие дочерние потоки продолжат работу. спать в очереди на работу.
  • Общая модель пула потоков:

изображение.png

Наиболее прямым ограничивающим фактором количества потоков в пуле потоков является количество N процессоров/ядер центрального процессора (ЦП)
: если ваш ЦП четырехъядерный, для задач с интенсивным использованием ЦП (например, редактирование видео). Для задач, потребляющих вычислительные ресурсы ЦП), лучше всего установить количество потоков в пуле потоков равным 4 (или +1, чтобы предотвратить блокировку потоков, вызванную другими факторами); для задач с интенсивным вводом-выводом оно обычно больше, чем количество процессоров количество ядер, поскольку потоки конкурируют не за вычислительные ресурсы процессора, а за ввод-вывод, а обработка ввода-вывода обычно медленнее.потоки с большим количеством ядер будут конкурировать за большее количество задач для процессора, предотвращая бездействие процессора во время обработки потоков. IO. Приведут к пустой трате ресурсов.

  • Пространство обменивается на время, а ресурсы серверного оборудования тратятся впустую в обмен на эффективность работы.
  • Пул — это набор ресурсов, которые полностью создаются и инициализируются при запуске сервера.Это называется статическим ресурсом. Когда сервер переходит в стадию официальной эксплуатации и начинает обрабатывать запросы клиентов, если ему нужны соответствующие ресурсы, он может получить их непосредственно из пула без динамического выделения.
  • После того как сервер обработал клиентское соединение, он может поместить связанные ресурсы обратно в пул без выполнения системного вызова для освобождения ресурсов.

Конечный автомат

  • Эффективный метод программирования внутри логической единицы: конечный автомат. Некоторые заголовки протоколов прикладного уровня содержат поле типа пакета.Каждый тип может быть сопоставлен с состоянием выполнения логической единицы, и сервер может на его основе написать соответствующую логику обработки. Ниже представлен независимый от состояния конечный автомат:
STATE_MACHINE( Package _pack )
{
    
    
	PackageType _type = _pack.GetType(); switch( _type )
	{
    
    
		case type_A:
    	process_package_A( _pack ); break;
    	case type_B:
    	process_package_B( _pack ); break;
	}
}

  • Это простой конечный автомат, за исключением того, что каждое состояние конечного автомата независимо друг от друга, то есть перенос между состояниями отсутствует. Для перехода между состояниями требуется внутренний драйвер конечного автомата, как показано в следующем коде:
STATE_MACHINE()
{
    
    
    State cur_State = type_A;
    while( cur_State != type_C )
    {
    
    
        Package _pack = getNewPackage(); 
        switch( cur_State )
        {
    
    
            case type_A:
            	process_package_state_A( _pack );
                cur_State = type_B;
            	break;
            case type_B:
            	process_package_state_B( _pack ); 
                cur_State = type_C;
            break;
    	}
	}
}

  • Конечный автомат содержит три состояния: type_A, type_B и type_C, где type_A — начальное состояние конечного автомата, а type_C — конечное состояние конечного автомата. Текущее состояние конечного автомата записывается в переменную cur_State. Во время цикла конечный автомат сначала получает новый пакет данных с помощью метода getNewPackage, а затем определяет, как обрабатывать пакет данных, на основе значения переменной cur_State. После обработки пакета данных конечный автомат реализует переход состояний, передавая значение целевого состояния в переменную cur_State. Затем, когда конечный автомат перейдет в следующий цикл, он выполнит логику, соответствующую новому состоянию.

СОБЫТИЕ ЭПОЛЛОНЕШОТ

  • Даже если можно использовать режим ET, событие в сокете все равно может запускаться несколько раз. Это может вызвать проблемы в параллельных программах. Например, поток начинает обработку данных после чтения данных в определенном сокете.В процессе обработки данных в сокете читаются новые данные (снова срабатывает EPOLLIN).В это время пробуждается другой поток.Прочитайте это новое данные. Итак, возникает ситуация, когда два потока одновременно управляют сокетом. Соединение сокета обрабатывается только одним потоком в любой момент времени, что может быть достигнуто с помощью события EPOLLONESHOT команды epoll.
  • Для файлового дескриптора, зарегистрированного с помощью события EPOLLONESHOT, операционная система запускает не более одного события, доступного для чтения, записи или исключения, зарегистрированного в нем, и запускает его только один раз, если только мы не используем функцию epoll_ctl для сброса события EPOLLONESHOT, зарегистрированного в файловом дескрипторе. Таким образом, когда один поток обрабатывает определенный сокет, другие потоки не могут иметь возможности управлять этим сокетом. Но если подумать об этом наоборот: как только сокет, зарегистрированный с событием EPOLLONESHOT, обрабатывается потоком, поток должен немедленно сбросить событие EPOLLONESHOT в сокете, чтобы гарантировать, что событие EPOLLIN может быть вызвано в следующий раз, когда сокет будет доступен для чтения. Это дает другим рабочим потокам возможность продолжить обработку этого сокета.

выполнить

  • Пул потоков используется вместе с блокировкой мьютекса.
  • Итак, сначала создайте взаимоисключающий файл заголовка блокировки (код прост и объединяет файл .h и файл операции).
#ifndef LOCKER_H
#define LOCKER_H

#include <exception>..异常
#include <pthread.h>
#include <semaphore.h>

// 线程同步机制封装类

// 互斥锁类
class locker {
    
    
public:
    locker() {
    
    //构造函数
        if(pthread_mutex_init(&m_mutex, NULL) != 0) {
    
     //返回值不等于0就是出错了
            throw std::exception();//抛出异常
        }
    }

    ~locker() {
    
     //析构函数
        pthread_mutex_destroy(&m_mutex);
    }

    bool lock() {
    
    
        return pthread_mutex_lock(&m_mutex) == 0;
    }

    bool unlock() {
    
    
        return pthread_mutex_unlock(&m_mutex) == 0;
    }

    pthread_mutex_t *get()//获取互斥量
    {
    
    
        return &m_mutex;
    }

private:
    pthread_mutex_t m_mutex;
};


// 条件变量类,判断队列当中是否有数据
class cond {
    
    
public:
    cond(){
    
    
        if (pthread_cond_init(&m_cond, NULL) != 0) {
    
    
            throw std::exception();
        }
    }
    ~cond() {
    
    
        pthread_cond_destroy(&m_cond);
    }

    bool wait(pthread_mutex_t *m_mutex) {
    
    
        int ret = 0;
        ret = pthread_cond_wait(&m_cond, m_mutex);
        return ret == 0;
    }
    bool timewait(pthread_mutex_t *m_mutex, struct timespec t) {
    
    
        int ret = 0;
        ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
        return ret == 0;
    }
    bool signal() {
    
    
        return pthread_cond_signal(&m_cond) == 0;
    }
    bool broadcast() {
    
    
        return pthread_cond_broadcast(&m_cond) == 0;
    }

private:
    pthread_cond_t m_cond;
};


// 信号量类
class sem {
    
    
public:
    sem() {
    
    
        if( sem_init( &m_sem, 0, 0 ) != 0 ) {
    
    
            throw std::exception();
        }
    }
    sem(int num) {
    
    
        if( sem_init( &m_sem, 0, num ) != 0 ) {
    
    
            throw std::exception();
        }
    }
    ~sem() {
    
    
        sem_destroy( &m_sem );
    }
    // 等待信号量
    bool wait() {
    
    
        return sem_wait( &m_sem ) == 0;
    }
    // 增加信号量
    bool post() {
    
    
        return sem_post( &m_sem ) == 0;
    }
private:
    sem_t m_sem;
};

#endif
  • Затем создайте класс пула потоков
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include "locker.h"

// 线程池类,将它定义为模板类是为了代码复用,模板参数T是任务类
template<typename T>
class threadpool {
    
    
public:
    /*thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/
    threadpool(int thread_number = 8, int max_requests = 10000);//构造
    ~threadpool();//析构
    bool append(T* request);//添加任务

private:
    /*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
    static void* worker(void* arg);
    void run();

private:
    // 线程的数量
    int m_thread_number;  
    
    // 描述线程池的数组,大小为m_thread_number    
    pthread_t * m_threads;//动态创建数组

    // 请求队列中最多允许的、等待处理的请求的数量  
    int m_max_requests; 
    
    // 请求队列
    std::list< T* > m_workqueue;  

    // 保护请求队列的互斥锁
    locker m_queuelocker;   

    // 是否有任务需要处理
    sem m_queuestat;//状态

    // 是否结束线程          
    bool m_stop;                    
};

template< typename T >//实现构造函数
threadpool< T >::threadpool(int thread_number, int max_requests) : //冒号后可以对成员进行初始化
        m_thread_number(thread_number), m_max_requests(max_requests), 
        m_stop(false), m_threads(NULL) {
    
    

    if((thread_number <= 0) || (max_requests <= 0) ) {
    
     //小于0就是错误值,抛出异常
        throw std::exception();
    }

    m_threads = new pthread_t[m_thread_number]; //创建数组,new动态创建
    if(!m_threads) {
    
    
        throw std::exception();
    }

    // 创建 thread_number 个线程,并将他们设置为脱离线程。自己释放资源
    for ( int i = 0; i < thread_number; ++i ) {
    
    
        printf( "create the %dth thread\n", i);
        if(pthread_create(m_threads + i, NULL, worker, this ) != 0) {
    
     //worker必须是静态函数,C中是全局函数
            //第一个参数为地址
            //不等于0代表出错
            delete [] m_threads;
            throw std::exception();
        }
        
        if( pthread_detach( m_threads[i] ) ) {
    
     //出错了线程分离
            delete [] m_threads;
            throw std::exception();
        }
    }
}

template< typename T >
threadpool< T >::~threadpool() {
    
     //析构函数
    delete [] m_threads;
    m_stop = true;
}

template< typename T > //添加事件
bool threadpool< T >::append( T* request )
{
    
    
    // 操作工作队列时一定要加锁,因为它被所有线程共享。
    m_queuelocker.lock();
    if ( m_workqueue.size() > m_max_requests ) {
    
     //不能超过最大的请求
        m_queuelocker.unlock();
        return false;
    }
    m_workqueue.push_back(request);//追加
    m_queuelocker.unlock();//解锁
    m_queuestat.post();//信号量增加
    return true;
}

template< typename T >
void* threadpool< T >::worker( void* arg ) //实现worker,但是可以使用this将参数传递过来
{
    
    
    threadpool* pool = ( threadpool* )arg;
    pool->run();//线程创建出来就执行
    return pool;
}

template< typename T >
void threadpool< T >::run() {
    
     //实现run

    while (!m_stop) {
    
     //一直循环
        m_queuestat.wait();
        m_queuelocker.lock();
        if ( m_workqueue.empty() ) {
    
    
            m_queuelocker.unlock();
            continue;
        }
        T* request = m_workqueue.front();//取出第一个任务
        m_workqueue.pop_front();//取了就删掉,删掉第一个
        m_queuelocker.unlock();
        if ( !request ) {
    
    
            continue;
        }
        request->process();
    }

}

#endif

  • Запись основных файлов, связанных с сокетами.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include "locker.h"
#include "threadpool.h"
#include "http_conn.h"

#define MAX_FD 65536   // 最大的文件描述符个数
#define MAX_EVENT_NUMBER 10000  // 监听的最大的事件数量

// 添加文件描述符
extern void addfd( int epollfd, int fd, bool one_shot );//加入exteren这个就是为了在别的文件中也能用
extern void removefd( int epollfd, int fd );

void addsig(int sig, void( handler )(int)){
    
     //添加信号捕捉
    struct sigaction sa; //注册信号
    memset( &sa, '\0', sizeof( sa ) );
    sa.sa_handler = handler;
    sigfillset( &sa.sa_mask );//设置临时阻塞信号集
    assert( sigaction( sig, &sa, NULL ) != -1 );
}

int main( int argc, char* argv[] ) {
    
    
    
    if( argc <= 1 ) {
    
    
        printf( "usage: %s port_number\n", basename(argv[0]));
        return 1;
    }

    int port = atoi( argv[1] ); //转换成整数
    addsig( SIGPIPE, SIG_IGN );//信号处理,捕捉到了信号就忽略他

    threadpool< http_conn >* pool = NULL; //任务是http连接的任务
    try {
    
    
        pool = new threadpool<http_conn>;//创建
    } catch( ... ) {
    
    
        return 1;
    }

    http_conn* users = new http_conn[ MAX_FD ];//最大用户数

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );//监听套接字

    int ret = 0;
    struct sockaddr_in address;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_family = AF_INET;
    address.sin_port = htons( port );//网络字节序

    // 端口复用,在绑定之前进行设置
    int reuse = 1;//为1就是复用
    //SOL_SOCKET是级别,SO_REUSEADDR代表端口复用
    setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );//绑定
    ret = listen( listenfd, 5 );//监听

    // 创建epoll对象,和事件数组,添加,多路复用
    epoll_event events[ MAX_EVENT_NUMBER ];//检测到了把事件写入数组
    int epollfd = epoll_create( 5 );
    // 添加到epoll对象中
    addfd( epollfd, listenfd, false );//添加文件描述符
    http_conn::m_epollfd = epollfd;//静态成员

    while(true) {
    
     //主线程不断循环检测事件发生
        
        int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 ); //检测到了几个事件
        
        if ( ( number < 0 ) && ( errno != EINTR ) ) {
    
    
            printf( "epoll failure\n" );
            break;
        }

        for ( int i = 0; i < number; i++ ) {
    
     //循环遍历事件数组
            
            int sockfd = events[i].data.fd;//获取监听的文件描述符
            
            if( sockfd == listenfd ) {
    
    //有客户端连接进来
                
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
                //连接客户端
                
                if ( connfd < 0 ) {
    
    
                    printf( "errno is: %d\n", errno );
                    continue;
                } 

                if( http_conn::m_user_count >= MAX_FD ) {
    
     //最大用户数不能超
                    close(connfd);
                    continue;
                }
                users[connfd].init( connfd, client_address);//直接把sockfd作为索引去操作

            } else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) {
    
    
            	//对方异常断开
                users[sockfd].close_conn();//非监听fd的判断,即为通信fd

            } else if(events[i].events & EPOLLIN) {
    
     //读的事件发生,模拟P模式一次性要把数据都读出来

                if(users[sockfd].read()) {
    
     //一次性把所有数据读完
                    pool->append(users + sockfd);//交给工作线程处理,地址直接相加
                } else {
    
    
                    users[sockfd].close_conn();//读失败了,关闭连接
                }

            }  else if( events[i].events & EPOLLOUT ) {
    
     //检测写事件

                if( !users[sockfd].write() ) {
    
     
                    users[sockfd].close_conn(); 
                }

            }
        }
    }
    //程序结束
    close( epollfd );
    close( listenfd );
    delete [] users;
    delete pool;
    return 0;
}
  • Определить файл заголовка http
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include "locker.h"
#include <sys/uio.h>

class http_conn
{
    
    
public:
    static const int FILENAME_LEN = 200;        // 文件名的最大长度
    static const int READ_BUFFER_SIZE = 2048;   // 读缓冲区的大小
    static const int WRITE_BUFFER_SIZE = 1024;  // 写缓冲区的大小
    
    // HTTP请求方法,这里只支持GET
    enum METHOD {
    
    GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT};
    
    /*
        解析客户端请求时,主状态机的状态
        CHECK_STATE_REQUESTLINE:当前正在分析请求行
        CHECK_STATE_HEADER:当前正在分析头部字段
        CHECK_STATE_CONTENT:当前正在解析请求体
    */
    enum CHECK_STATE {
    
     CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };
    
    /*
        服务器处理HTTP请求的可能结果,报文解析的结果
        NO_REQUEST          :   请求不完整,需要继续读取客户数据
        GET_REQUEST         :   表示获得了一个完成的客户请求
        BAD_REQUEST         :   表示客户请求语法错误
        NO_RESOURCE         :   表示服务器没有资源
        FORBIDDEN_REQUEST   :   表示客户对资源没有足够的访问权限
        FILE_REQUEST        :   文件请求,获取文件成功
        INTERNAL_ERROR      :   表示服务器内部错误
        CLOSED_CONNECTION   :   表示客户端已经关闭连接了
    */
    enum HTTP_CODE {
    
     NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };
    
    // 从状态机的三种可能状态,即行的读取状态,分别表示
    // 1.读取到一个完整的行 2.行出错 3.行数据尚且不完整
    enum LINE_STATUS {
    
     LINE_OK = 0, LINE_BAD, LINE_OPEN };
public:
    http_conn(){
    
    }//构造与析构
    ~http_conn(){
    
    }
public:
    void init(int sockfd, const sockaddr_in& addr); // 初始化新接受的连接
    void close_conn();  // 关闭连接
    void process(); // 处理客户端请求,响应也在这里处理
    bool read();// 非阻塞读
    bool write();// 非阻塞写
private:
    void init();    // 初始化连接
    HTTP_CODE process_read();    // 解析HTTP请求
    bool process_write( HTTP_CODE ret );    // 填充HTTP应答

    // 下面这一组函数被process_read调用以分析HTTP请求
    HTTP_CODE parse_request_line( char* text );
    HTTP_CODE parse_headers( char* text );
    HTTP_CODE parse_content( char* text );
    HTTP_CODE do_request();
    char* get_line() {
    
     return m_read_buf + m_start_line; }
    LINE_STATUS parse_line();

    // 这一组函数被process_write调用以填充HTTP应答。
    void unmap();
    bool add_response( const char* format, ... );
    bool add_content( const char* content );
    bool add_content_type();
    bool add_status_line( int status, const char* title );
    bool add_headers( int content_length );
    bool add_content_length( int content_length );
    bool add_linger();
    bool add_blank_line();

public:
    static int m_epollfd;       // 所有socket上的事件都被注册到同一个epoll内核事件中,所以设置成静态的
    static int m_user_count;    // 统计用户的数量

private:
    int m_sockfd;           // 该HTTP连接的socket和对方的socket地址
    sockaddr_in m_address; //通信socket地址
    
    char m_read_buf[ READ_BUFFER_SIZE ];    // 读缓冲区
    int m_read_idx;                         // 标识读缓冲区中已经读入的客户端数据的最后一个字节的下一个位置
    int m_checked_idx;                      // 当前正在分析的字符在读缓冲区中的位置
    int m_start_line;                       // 当前正在解析的行的起始位置

    CHECK_STATE m_check_state;              // 主状态机当前所处的状态
    METHOD m_method;                        // 请求方法

    char m_real_file[ FILENAME_LEN ];       // 客户请求的目标文件的完整路径,其内容等于 doc_root + m_url, doc_root是网站根目录
    char* m_url;                            // 客户请求的目标文件的文件名
    char* m_version;                        // HTTP协议版本号,我们仅支持HTTP1.1
    char* m_host;                           // 主机名
    int m_content_length;                   // HTTP请求的消息总长度
    bool m_linger;                          // HTTP请求是否要求保持连接

    char m_write_buf[ WRITE_BUFFER_SIZE ];  // 写缓冲区
    int m_write_idx;                        // 写缓冲区中待发送的字节数
    char* m_file_address;                   // 客户请求的目标文件被mmap到内存中的起始位置
    struct stat m_file_stat;                // 目标文件的状态。通过它我们可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息
    struct iovec m_iv[2];                   // 我们将采用writev来执行写操作,所以定义下面两个成员,其中m_iv_count表示被写内存块的数量。
    int m_iv_count;
};

#endif

  • Исходный файл
#include "http_conn.h"

// 定义HTTP响应的一些状态信息
const char* ok_200_title = "OK";
const char* error_400_title = "Bad Request";
const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
const char* error_403_title = "Forbidden";
const char* error_403_form = "You do not have permission to get file from this server.\n";
const char* error_404_title = "Not Found";
const char* error_404_form = "The requested file was not found on this server.\n";
const char* error_500_title = "Internal Error";
const char* error_500_form = "There was an unusual problem serving the requested file.\n";

// 网站的根目录
const char* doc_root = "/home/kagome/webserver/resources";

int setnonblocking( int fd ) {
    
     //设置文件描述符非阻塞
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

// 向epoll中添加需要监听的文件描述符
void addfd( int epollfd, int fd, bool one_shot ) {
    
    
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLRDHUP;//水平触发/边沿触发
    if(one_shot) 
    {
    
    
        // 防止同一个通信被不同的线程处理
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    // 设置文件描述符非阻塞
    setnonblocking(fd);  //ET模式要全给,非阻塞
}

// 从epoll中移除监听的文件描述符
void removefd( int epollfd, int fd ) {
    
    
    epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );
    close(fd);
}

// 修改文件描述符,重置socket上的EPOLLONESHOT事件,以确保下一次可读时,EPOLLIN事件能被触发
void modfd(int epollfd, int fd, int ev) {
    
    
    epoll_event event;
    event.data.fd = fd;
    event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}

// 所有的客户数
int http_conn::m_user_count = 0;
// 所有socket上的事件都被注册到同一个epoll内核事件中,所以设置成静态的
int http_conn::m_epollfd = -1;

// 关闭连接
void http_conn::close_conn() {
    
    
    if(m_sockfd != -1) {
    
    
        removefd(m_epollfd, m_sockfd);//删除
        m_sockfd = -1;
        m_user_count--; // 关闭一个连接,将客户总数量-1
    }
}

// 初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in& addr){
    
    
    m_sockfd = sockfd;
    m_address = addr;
    
    // 端口复用
    int reuse = 1;
    setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );
    addfd( m_epollfd, sockfd, true);//添加到epoll,主线程只负责监听,其余交给http_conn
    m_user_count++;
    init();
}

void http_conn::init()
{
    
    
    m_check_state = CHECK_STATE_REQUESTLINE;    // 初始状态为检查请求行
    m_linger = false;       // 默认不保持链接  Connection : keep-alive保持连接

    m_method = GET;         // 默认请求方式为GET
    m_url = 0;              
    m_version = 0;
    m_content_length = 0;
    m_host = 0;
    m_start_line = 0;
    m_checked_idx = 0;
    m_read_idx = 0;
    m_write_idx = 0;
    bzero(m_read_buf, READ_BUFFER_SIZE);
    bzero(m_write_buf, READ_BUFFER_SIZE);
    bzero(m_real_file, FILENAME_LEN);
}

// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read() {
    
    
    if( m_read_idx >= READ_BUFFER_SIZE ) {
    
    
        return false;
    }
    int bytes_read = 0;
    while(true) {
    
    
        // 从m_read_buf + m_read_idx索引出开始保存数据,大小是READ_BUFFER_SIZE - m_read_idx
        bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, 
        READ_BUFFER_SIZE - m_read_idx, 0 );
        if (bytes_read == -1) {
    
    
            if( errno == EAGAIN || errno == EWOULDBLOCK ) {
    
    
                // 没有数据
                break;
            }
            return false;   
        } else if (bytes_read == 0) {
    
       // 对方关闭连接
            return false;
        }
        m_read_idx += bytes_read;
    }
    return true;
}

// 解析一行,判断依据\r\n
http_conn::LINE_STATUS http_conn::parse_line() {
    
    
    char temp;
    for ( ; m_checked_idx < m_read_idx; ++m_checked_idx ) {
    
    
        temp = m_read_buf[ m_checked_idx ];
        if ( temp == '\r' ) {
    
    
            if ( ( m_checked_idx + 1 ) == m_read_idx ) {
    
    
                return LINE_OPEN;
            } else if ( m_read_buf[ m_checked_idx + 1 ] == '\n' ) {
    
    
                m_read_buf[ m_checked_idx++ ] = '\0';
                m_read_buf[ m_checked_idx++ ] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        } else if( temp == '\n' )  {
    
    
            if( ( m_checked_idx > 1) && ( m_read_buf[ m_checked_idx - 1 ] == '\r' ) ) {
    
    
                m_read_buf[ m_checked_idx-1 ] = '\0';
                m_read_buf[ m_checked_idx++ ] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    return LINE_OPEN;
}

// 解析HTTP请求行,获得请求方法,目标URL,以及HTTP版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char* text) {
    
    
    // GET /index.html HTTP/1.1
    m_url = strpbrk(text, " \t"); // 判断第二个参数中的字符哪个在text中最先出现
    if (! m_url) {
    
     
        return BAD_REQUEST;
    }
    // GET\0/index.html HTTP/1.1
    *m_url++ = '\0';    // 置位空字符,字符串结束符
    char* method = text;
    if ( strcasecmp(method, "GET") == 0 ) {
    
     // 忽略大小写比较
        m_method = GET;
    } else {
    
    
        return BAD_REQUEST;
    }
    // /index.html HTTP/1.1
    // 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
    m_version = strpbrk( m_url, " \t" );
    if (!m_version) {
    
    
        return BAD_REQUEST;
    }
    *m_version++ = '\0';
    if (strcasecmp( m_version, "HTTP/1.1") != 0 ) {
    
    
        return BAD_REQUEST;
    }
    /**
     * http://192.168.110.129:10000/index.html
    */
    if (strncasecmp(m_url, "http://", 7) == 0 ) {
    
       
        m_url += 7;
        // 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
        m_url = strchr( m_url, '/' );
    }
    if ( !m_url || m_url[0] != '/' ) {
    
    
        return BAD_REQUEST;
    }
    m_check_state = CHECK_STATE_HEADER; // 检查状态变成检查头
    return NO_REQUEST;
}

// 解析HTTP请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char* text) {
    
       
    // 遇到空行,表示头部字段解析完毕
    if( text[0] == '\0' ) {
    
    
        // 如果HTTP请求有消息体,则还需要读取m_content_length字节的消息体,
        // 状态机转移到CHECK_STATE_CONTENT状态
        if ( m_content_length != 0 ) {
    
    
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        // 否则说明我们已经得到了一个完整的HTTP请求
        return GET_REQUEST;
    } else if ( strncasecmp( text, "Connection:", 11 ) == 0 ) {
    
    
        // 处理Connection 头部字段  Connection: keep-alive
        text += 11;
        text += strspn( text, " \t" );
        if ( strcasecmp( text, "keep-alive" ) == 0 ) {
    
    
            m_linger = true;
        }
    } else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 ) {
    
    
        // 处理Content-Length头部字段
        text += 15;
        text += strspn( text, " \t" );
        m_content_length = atol(text);
    } else if ( strncasecmp( text, "Host:", 5 ) == 0 ) {
    
    
        // 处理Host头部字段
        text += 5;
        text += strspn( text, " \t" );
        m_host = text;
    } else {
    
    
        printf( "oop! unknow header %s\n", text );
    }
    return NO_REQUEST;
}

// 我们没有真正解析HTTP请求的消息体,只是判断它是否被完整的读入了
http_conn::HTTP_CODE http_conn::parse_content( char* text ) {
    
    
    if ( m_read_idx >= ( m_content_length + m_checked_idx ) )
    {
    
    
        text[ m_content_length ] = '\0';
        return GET_REQUEST;
    }
    return NO_REQUEST;
}

// 主状态机,解析请求
http_conn::HTTP_CODE http_conn::process_read() {
    
    
    LINE_STATUS line_status = LINE_OK;
    HTTP_CODE ret = NO_REQUEST;
    char* text = 0;
    while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK))
                || ((line_status = parse_line()) == LINE_OK)) {
    
    
        // 获取一行数据
        text = get_line();
        m_start_line = m_checked_idx;
        printf( "got 1 http line: %s\n", text );

        switch ( m_check_state ) {
    
    
            case CHECK_STATE_REQUESTLINE: {
    
    
                ret = parse_request_line( text );
                if ( ret == BAD_REQUEST ) {
    
    
                    return BAD_REQUEST;
                }
                break;
            }
            case CHECK_STATE_HEADER: {
    
    
                ret = parse_headers( text );
                if ( ret == BAD_REQUEST ) {
    
    
                    return BAD_REQUEST;
                } else if ( ret == GET_REQUEST ) {
    
    
                    return do_request();
                }
                break;
            }
            case CHECK_STATE_CONTENT: {
    
    
                ret = parse_content( text );
                if ( ret == GET_REQUEST ) {
    
    
                    return do_request();
                }
                line_status = LINE_OPEN;
                break;
            }
            default: {
    
    
                return INTERNAL_ERROR;
            }
        }
    }
    return NO_REQUEST;
}

// 当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性,
// 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其
// 映射到内存地址m_file_address处,并告诉调用者获取文件成功
http_conn::HTTP_CODE http_conn::do_request()
{
    
    
    // "/home/nowcoder/webserver/resources"
    strcpy( m_real_file, doc_root );
    int len = strlen( doc_root );
    strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );
    // 获取m_real_file文件的相关的状态信息,-1失败,0成功
    if ( stat( m_real_file, &m_file_stat ) < 0 ) {
    
    
        return NO_RESOURCE;
    }

    // 判断访问权限
    if ( ! ( m_file_stat.st_mode & S_IROTH ) ) {
    
    
        return FORBIDDEN_REQUEST;
    }

    // 判断是否是目录
    if ( S_ISDIR( m_file_stat.st_mode ) ) {
    
    
        return BAD_REQUEST;
    }

    // 以只读方式打开文件
    int fd = open( m_real_file, O_RDONLY );
    // 创建内存映射
    m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
    close( fd );
    return FILE_REQUEST;
}

// 对内存映射区执行munmap操作
void http_conn::unmap() {
    
    
    if( m_file_address )
    {
    
    
        munmap( m_file_address, m_file_stat.st_size );
        m_file_address = 0;
    }
}

// 写HTTP响应
bool http_conn::write()
{
    
    
    int temp = 0;
    int bytes_have_send = 0;    // 已经发送的字节
    int bytes_to_send = m_write_idx;// 将要发送的字节 (m_write_idx)写缓冲区中待发送的字节数
    
    if ( bytes_to_send == 0 ) {
    
    
        // 将要发送的字节为0,这一次响应结束。
        modfd( m_epollfd, m_sockfd, EPOLLIN ); 
        init();
        return true;
    }

    while(1) {
    
    
        // 分散写
        temp = writev(m_sockfd, m_iv, m_iv_count);
        if ( temp <= -1 ) {
    
    
            // 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
            // 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。
            if( errno == EAGAIN ) {
    
    
                modfd( m_epollfd, m_sockfd, EPOLLOUT );
                return true;
            }
            unmap();
            return false;
        }
        bytes_to_send -= temp;
        bytes_have_send += temp;
        if ( bytes_to_send <= bytes_have_send ) {
    
    
            // 发送HTTP响应成功,根据HTTP请求中的Connection字段决定是否立即关闭连接
            unmap();
            if(m_linger) {
    
    
                init();
                modfd( m_epollfd, m_sockfd, EPOLLIN );
                return true;
            } else {
    
    
                modfd( m_epollfd, m_sockfd, EPOLLIN );
                return false;
            } 
        }
    }
}

// 往写缓冲中写入待发送的数据
bool http_conn::add_response( const char* format, ... ) {
    
    
    if( m_write_idx >= WRITE_BUFFER_SIZE ) {
    
    
        return false;
    }
    va_list arg_list;
    va_start( arg_list, format );
    int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );
    if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) {
    
    
        return false;
    }
    m_write_idx += len;
    va_end( arg_list );
    return true;
}

bool http_conn::add_status_line( int status, const char* title ) {
    
    
    return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
}

bool http_conn::add_headers(int content_len) {
    
    
    add_content_length(content_len);
    add_content_type();
    add_linger();
    add_blank_line();
}

bool http_conn::add_content_length(int content_len) {
    
    
    return add_response( "Content-Length: %d\r\n", content_len );
}

bool http_conn::add_linger()
{
    
    
    return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
}

bool http_conn::add_blank_line()
{
    
    
    return add_response( "%s", "\r\n" );
}

bool http_conn::add_content( const char* content )
{
    
    
    return add_response( "%s", content );
}

bool http_conn::add_content_type() {
    
    
    return add_response("Content-Type:%s\r\n", "text/html");
}

// 根据服务器处理HTTP请求的结果,决定返回给客户端的内容
bool http_conn::process_write(HTTP_CODE ret) {
    
    
    switch (ret)
    {
    
    
        case INTERNAL_ERROR:
            add_status_line( 500, error_500_title );
            add_headers( strlen( error_500_form ) );
            if ( ! add_content( error_500_form ) ) {
    
    
                return false;
            }
            break;
        case BAD_REQUEST:
            add_status_line( 400, error_400_title );
            add_headers( strlen( error_400_form ) );
            if ( ! add_content( error_400_form ) ) {
    
    
                return false;
            }
            break;
        case NO_RESOURCE:
            add_status_line( 404, error_404_title );
            add_headers( strlen( error_404_form ) );
            if ( ! add_content( error_404_form ) ) {
    
    
                return false;
            }
            break;
        case FORBIDDEN_REQUEST:
            add_status_line( 403, error_403_title );
            add_headers(strlen( error_403_form));
            if ( ! add_content( error_403_form ) ) {
    
    
                return false;
            }
            break;
        case FILE_REQUEST:
            add_status_line(200, ok_200_title );
            add_headers(m_file_stat.st_size);
            m_iv[ 0 ].iov_base = m_write_buf;
            m_iv[ 0 ].iov_len = m_write_idx;
            m_iv[ 1 ].iov_base = m_file_address;
            m_iv[ 1 ].iov_len = m_file_stat.st_size;
            m_iv_count = 2;
            return true;
        default:
            return false;
    }

    m_iv[ 0 ].iov_base = m_write_buf;
    m_iv[ 0 ].iov_len = m_write_idx;
    m_iv_count = 1;
    return true;
}

// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process() {
    
    
    // 解析HTTP请求
    HTTP_CODE read_ret = process_read();
    if ( read_ret == NO_REQUEST ) {
    
    
        modfd( m_epollfd, m_sockfd, EPOLLIN );
        return;
    }
    
    // 生成响应
    bool write_ret = process_write( read_ret );
    if ( !write_ret ) {
    
    
        close_conn();
    }
    modfd( m_epollfd, m_sockfd, EPOLLOUT);
}
  • выполнить

изображение.png

испытание под давлением

  • Webbench — известный и превосходный инструмент для стресс-тестирования веб-производительности в Linux. Он разработан корпорацией Lionbridge .

Тест проводится на одном и том же оборудовании, производительности разных служб и состоянии работы одной и той же службы на разном оборудовании. Отображает два аспекта работы сервера: количество запросов ответа в секунду и объем данных, передаваемых в секунду.

  • Основной принцип: Webbench сначала выделяет несколько дочерних процессов, и каждый дочерний процесс циклически выполняет тестирование веб-доступа. Дочерний процесс сообщает родительскому процессу результаты доступа через канал, а родительский процесс выдает окончательные статистические результаты.
  • 10 000 клиентов, успешно работающих за 5 секунд

изображение.png
изображение.png

рекомендация

отblog.csdn.net/weixin_44673253/article/details/131991102