На основе модели многопоточного сервера чата с сокетами Linux (01)

Предисловие

Сокеты широко используются и очень важны при разработке реальных системных программ. В практических приложениях серверам часто необходимо поддерживать несколько клиентских подключений, поэтому особенно важно реализовать модель сервера с высоким уровнем параллелизма. Сервер с высоким параллелизмом превратился из простой модели сервера с циклическим циклом, предназначенной для обработки небольшого количества одновременных сетевых запросов, в модель сервера с высоким параллелизмом, которая решает проблемы C10K и C10M. В этой статье используется простая многопоточная модель, которая поможет каждому научиться самостоятельно реализовать простой параллельный сервер.

C/S-архитектура

Сервер-клиент, то есть структура Клиент-Сервер (C/S). Структура C/S обычно имеет двухслойную структуру. Сервер отвечает за управление данными, а клиент — за выполнение интерактивных задач с пользователями.

В структуре C/S прикладная программа разделена на две части: серверную и клиентскую часть. Серверная часть представляет собой информацию и функции, совместно используемые несколькими пользователями, и выполняет фоновые службы, такие как управление работой общих баз данных; клиентская часть предназначена исключительно для пользователей и отвечает за выполнение функций переднего плана и имеет мощные функции с точки зрения подсказок об ошибках. и онлайн-справку., а также может свободно переключаться между подпрограммами.

картина

Как показано на рисунке выше: Это отношение вызова функции, которое соединяет клиента и сервер на основе сокетов.Об API сокетов имеется много информации, поэтому в этой статье он не будет описываться подробно.

библиотека потоков pthread: (POSIX)

Библиотека потоков pthread — это широко используемая библиотека потоков в Linux. Вы можете найти соответствующие статьи о ее использовании и функциях. Ниже приводится краткое введение в ее использование и компиляцию.

Идентификатор потока

Потоки имеют идентификаторы, но они не уникальны для системы, а действительны только в среде процесса. Дескриптор потока имеет тип pthread_t, который не может обрабатываться как целое число, но является структурой. Ниже представлены две функции :

头文件: <pthread.h>
原型: int pthread_equal(pthread_t tid1, pthread_t tid2);
返回值: 相等返回非0, 不相等返回0.
说明: 比较两个线程ID是否相等.

头文件: <pthread.h>
原型: pthread_t pthread_self();
返回值: 返回调用线程的线程ID.

Создание темы

Создайте поток во время выполнения, вы можете назначить потоку работу, которую он должен выполнить (функция выполнения потока), поток разделяет ресурсы процесса.Функция pthread_create(), которая создает поток

头文件: <pthread.h>
原型: int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(start_rtn)(void), void *restrict arg);
返回值: 成功则返回0, 否则返回错误编号.
参数:
tidp: 指向新创建线程ID的变量, 作为函数的输出.
attr: 用于定制各种不同的线程属性, NULL为默认属性(见下).
start_rtn: 函数指针, 为线程开始执行的函数名.该函数可以返回一个void *类型的返回值,
而这个返回值也可以是其他类型,并由 pthread_join()获取
arg: 函数的唯一无类型(void)指针参数, 如要传多个参数, 可以用结构封装.

компилировать

因为pthread的库不是linux系统的库,所以在进行编译的时候要加上     -lpthread
# gcc filename -lpthread  //默认情况下gcc使用c库,要使用额外的库要这样选择使用的库

Распространенные модели веб-серверов

Насколько я понимаю, в этой статье в основном используется TCP в качестве примера для обобщения методов реализации нескольких распространенных моделей сетевых серверов и, наконец, реализован простой чат из командной строки.

одиночный цикл процесса

Принцип однострочного цикла процесса заключается в том, что основной процесс не взаимодействует с клиентом. Клиент должен сначала подключиться к серверу. Сервер принимает клиентское соединение и считывает данные от клиента, затем обрабатывает и возвращает результат обработки клиент, а затем принимает его.Запрос на подключение следующего клиента.

Преимущества Преимущество модели однопоточного цикла состоит в том, что ее легко и просто реализовать, без проблем и накладных расходов на синхронизацию и блокировку.

недостаток

  1. Модель блокировки, последовательная обработка сетевых запросов;

  2. Он не использует преимущества многоядерных процессоров, а сетевые запросы обрабатываются последовательно;

  3. Невозможно поддерживать несколько клиентских подключений одновременно;

  4. Программа работает последовательно, и сервер не может отправлять и получать данные одновременно.

    картина

Повторное использование ввода-вывода в одном потоке

epoll обычно используется в качестве механизма мультиплексирования ввода-вывода на серверах Linux с высоким уровнем параллелизма. Поток регистрирует все события чтения и записи сокета, которые необходимо обработать в epoll.Когда происходит сетевой ввод-вывод, epoll_wait возвращает значение, а поток проверяет и обрабатывает запрос на сокете.

преимущество

  1. Реализация проста, уменьшая накладные расходы на блокировку и переключение потоков.

недостаток

  1. Можно использовать только одноядерный процессор. Если время обработки слишком велико, вся служба зависнет;

  2. Когда количество клиентов превысит определенное количество, производительность значительно упадет;

  3. Он подходит только для сценариев с большим количеством операций ввода-вывода, небольшими вычислениями и коротким временем обработки дескриптора.

картина

 

Многопоточность/Несколько процессов

Основной особенностью многопоточной и многопроцессной модели является то, что каждый сетевой запрос обрабатывается процессом/потком, а внутри потока используются блокирующие системные вызовы.С точки зрения разделения функций потока, обработку может выполнять отдельный поток. принимает соединение, а остальные потоки обрабатывают определенные сетевые запросы (сбор пакетов, обработка, отправка пакетов); несколько процессов также могут прослушивать и принимать сетевые соединения по отдельности.

преимущество:

1. Реализация относительно проста, 2. Используются многоядерные ресурсы ЦП.

недостаток:

1. Поток все еще заблокирован внутри. Возьмем крайний пример: если поток спит в бизнес-логике дескриптора, он также зависнет.

картина

Многопоточное/многопроцессное мультиплексирование ввода-вывода

Многопоточная, многопроцессная модель ввода-вывода, каждый дочерний процесс прослушивает службу и использует механизм epoll для обработки сетевого запроса процесса.После того, как дочерний процесс принимает(), он создает подключенный дескриптор, а затем общаться с клиентом через подключенный дескриптор терминальной связи. Этот механизм подходит для сценариев с высоким уровнем параллелизма.

преимущество:

  1. Поддержка более высокого параллелизма.

недостаток:

  1. Асинхронное программирование неинтуитивно и подвержено ошибкам.

картина

 

Многопоточность разделяет роли ввода-вывода

Основными функциями многопоточных ролей ввода-вывода являются: поток принятия обрабатывает установление новых соединений; пул потоков ввода-вывода обрабатывает сетевой ввод-вывод; а пул потоков обработки обрабатывает бизнес-логику. Сценарии использования, такие как: приложения телемаркетинга, бережливый TThreadedSelectorServer.

преимущество:

  1. Разделите потоки по разным функциям, и каждый поток обрабатывает фиксированные функции, что более эффективно.

  2. Количество потоков можно настроить в соответствии с бизнес-характеристиками для оптимизации производительности.

недостаток:

  1. Межпотоковая связь требует введения накладных расходов на блокировку.

  2. Логика сложна и трудно реализуема.

картина

краткое содержание

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

Модель многопоточного параллельного сервера

Ниже мы в основном обсуждаем модель многопоточного параллельного сервера.

Структура кода

Структура кода параллельного сервера выглядит следующим образом:

thread_func()
{
  while(1) {
    recv(...);
    process(...);
    send(...);
  }
  close(...);
}
main(
 socket(...); 
 bind(...);
 listen(...);
 while(1) { 
  accept(...);
  pthread_create();
 }
}

Как видно из вышеизложенного, сервер разделен на две части: основной поток и подпоток.

основная тема

Основной функцией является основной поток, и его основные задачи заключаются в следующем:

  1. Socket() создает сокет прослушивания;

  2. bind() связывает номер порта и адрес;

  3. Listen() включает прослушивание;

  4. Accept() ожидает соединения клиента,

  5. Когда клиент подключается, метод Accept() создаст новый сокет new_fd;

  6. Основной поток создаст дочерний поток и передаст ему new_fd.

дочерний поток

  1. Функция подпотока — thread_func(), которая обрабатывает все задачи связи с клиентом через new_fd.

Подробные шаги для подключения клиента к серверу

Ниже мы рассмотрим пошаговую инструкцию подключения клиента к серверу.

1. Клиент подключается к серверу

  1. Сервер устанавливает прослушивающий сокет Listen_fd и инициализирует его;

  2. Клиент создает сокет fd1;

  3. Клиент client1 подключается к Listen_fd сервера через сокет fd1;

картина

 

 

2. Основной поток создает подпоток thread1.

  1. После того, как сервер получит запрос на соединение от client1, функция accpet вернет новый сокет newfd1;

  2. Позже связь между сервером и клиентом1 будет зависеть от newfd1, а прослушивающий сокет Listen_fd продолжит отслеживать соединения других клиентов;

  3. Главный поток создает подпоток thread1 с помощью pthead_create() и передает newfd1 потоку1;

  4. Связь между сервером и клиентом1 зависит от newfd1 и fd1 соответственно.

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

  6. Основной поток client1 отвечает за чтение данных с клавиатуры и отправку их на сервер, а подпоток read_thread отвечает за получение информации с сервера.

картина

 

3. клиент2 подключается к серверу

  1. Клиент client2 создает сокет fd2;

  2. Подключите Listen_fd сервера через функцию подключения;

    картина

4. Основной поток создает подпоток thread2.

  1. После того, как сервер получит запрос на соединение от client2, функция accpet вернет новый сокет newfd2;

  2. Позже связь между сервером и клиентом2 будет зависеть от newfd2, а прослушивающий сокет Listen_fd продолжит отслеживать соединения других клиентов;

  3. Главный поток создает подпоток thread2 с помощью pthead_create() и передает newfd2 в поток2;

  4. Связь между сервером и клиентом1 зависит от newfd2 и fd2 соответственно.

  5. Аналогично, клиент2 должен создать дочерний поток read_thread, чтобы получать информацию, отправленную сервером, в реальном времени и читать данные с клавиатуры;

  6. Основной поток client1 отвечает за чтение данных с клавиатуры и отправку их на сервер, а подпоток read_thread отвечает за получение информации с сервера.

картина

 

Как видно из рисунка выше, после того как каждый клиент подключается к серверу, сервер создает специальный поток, отвечающий за общение с клиентом; каждый клиент и сервер имеют фиксированную пару комбинаций fd для подключения.

Пример

Ладно, с теорией я закончил.По практике Иицзюня я тоже унаследовал учение своего предка:говорить дешево,показать мой код.Статьи,которые пишут только теорию без кодирования -просто хулиганы.

Основные функции этого примера описаны следующим образом:

  1. Разрешить нескольким клиентам одновременно подключаться к серверу;

  2. Клиент может отправлять и получать данные самостоятельно;

  3. После того, как клиент отправит данные на сервер, сервер вернет данные клиенту в целости и сохранности.

Сервис-Терминал
/*********************************************
           服务器程序  TCPServer.c  
*********************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>

#define RECVBUFSIZE 2048
void *rec_func(void *arg)
{
 int sockfd,new_fd,nbytes;
 char buffer[RECVBUFSIZE];
 int i;
 new_fd = *((int *) arg);
 free(arg); 
 
 while(1)
 {
  if((nbytes=recv(new_fd,buffer, RECVBUFSIZE,0))==-1)
  {
   fprintf(stderr,"Read Error:%s\n",strerror(errno));
   exit(1);
  }
  if(nbytes == -1)
  {//客户端出错了 返回值-1
   close(new_fd);
   break;   
  }
  if(nbytes == 0)
  {//客户端主动断开连接,返回值是0
   close(new_fd);
   break;
  }
  buffer[nbytes]='\0'; 
  printf("I have received:%s\n",buffer); 
  
  
  if(send(new_fd,buffer,strlen(buffer),0)==-1)
  {
   fprintf(stderr,"Write Error:%s\n",strerror(errno));
   exit(1);
  }
   
 }

}

int main(int argc, char *argv[])
{
 char buffer[RECVBUFSIZE];
 int sockfd,new_fd,nbytes;
 struct sockaddr_in server_addr;
 struct sockaddr_in client_addr;
 int sin_size,portnumber;
 char hello[]="Hello! Socket communication world!\n";
 pthread_t tid;
 int *pconnsocke = NULL;
 int ret,i;
 
 if(argc!=2)
 {
  fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
  exit(1);
 }
 /*端口号不对,退出*/
 if((portnumber=atoi(argv[1]))<0)
 {
  fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
  exit(1);
 }

 /*服务器端开始建立socket描述符  sockfd用于监听*/
 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)  
 {
  fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
  exit(1);
 }
 
 /*服务器端填充 sockaddr结构*/ 
 bzero(&server_addr,sizeof(struct sockaddr_in));
 server_addr.sin_family     =AF_INET;
 /*自动填充主机IP*/
 server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//自动获取网卡地址
 server_addr.sin_port       =htons(portnumber);
 
 /*捆绑sockfd描述符*/ 
 if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
 {
  fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
  exit(1);
 }
 
 /*监听sockfd描述符*/
 if(listen(sockfd, 10)==-1)
 {
  fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
  exit(1);
 }

 while(1)
 {
  /*服务器阻塞,直到客户程序建立连接*/
  sin_size=sizeof(struct sockaddr_in);
  if((new_fd = accept(sockfd,(struct sockaddr *)&client_addr,&sin_size))==-1)
  {
   fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
   exit(1);
  }
  
  pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 
 }
 //close(sockfd);
 exit(0);
}

клиент
/*********************************************
           服务器程序  TCPServer.c  
*********************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#define RECVBUFSIZE 1024

void *func(void *arg)
{
 int sockfd,new_fd,nbytes;
 char buffer[RECVBUFSIZE];
 
 new_fd = *((int *) arg);
 free(arg);
 
 while(1)
 {
  if((nbytes=recv(new_fd,buffer, RECVBUFSIZE,0))==-1)
  {
   fprintf(stderr,"Read Error:%s\n",strerror(errno));
   exit(1);
  }
  buffer[nbytes]='\0';
  printf("I have received:%s\n",buffer); 
 }

}

int main(int argc, char *argv[])
{
 int sockfd;
 char buffer[RECVBUFSIZE];
 struct sockaddr_in server_addr;
 struct hostent *host;
 int portnumber,nbytes; 
 pthread_t tid;
 int *pconnsocke = NULL;
 int ret;
 
 //检测参数个数
 if(argc!=3)
 {
  fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
  exit(1);
 }
 //argv2 存放的是端口号 ,读取该端口,转换成整型变量
 if((portnumber=atoi(argv[2]))<0)
 {
  fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
  exit(1);
 }
 //创建一个 套接子
 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
 {
  fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
  exit(1);
 }

 //填充结构体,ip和port必须是服务器的
 bzero(&server_addr,sizeof(server_addr));
 server_addr.sin_family=AF_INET;
 server_addr.sin_port=htons(portnumber);
 server_addr.sin_addr.s_addr = inet_addr(argv[1]);//argv【1】 是server ip地址

 /*¿Í»§³ÌÐò·¢ÆðÁ¬œÓÇëÇó*/ 
 if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
 {
  fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
  exit(1);
 }
 
 //创建线程
 pconnsocke = (int *) malloc(sizeof(int));
 *pconnsocke = sockfd;
 
 ret = pthread_create(&tid, NULL, func, (void *) pconnsocke);
 if (ret < 0) 
 {
  perror("pthread_create err");
  return -1;
 } 
 while(1)
 {
 #if 1
  printf("input msg:");
  scanf("%s",buffer);
  if(send(sockfd,buffer,strlen(buffer),0)==-1)
  {
   fprintf(stderr,"Write Error:%s\n",strerror(errno));
   exit(1);
  }
  #endif
 }
 close(sockfd);
 exit(0);
}

Для компиляции и компиляции потоков необходимо использовать библиотеку pthread.Команда компиляции выглядит следующим образом:

  1. gcc sc -os -lpthread

  2. gcc cli.c -oc -lpthread сначала проверьте на этом компьютере

  3. Откройте терминал./s 8888

  4. Откройте другой терминал./cl 127.0.0.1 8888 и введите строку «qqqqqqq».

  5. Откройте другой терминал./cl 127.0.0.1 8888 и введите строку «yikoulinux».

    картина

Некоторые читатели могут заметить, что сервер использует следующий код при создании дочернего потока:

 pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 

Почему нам нужно выделить часть памяти специально для хранения этого нового сокета? 

Это очень тонкая ошибка, которую допускают многие новички. В следующей главе я объясню это конкретно вам.

Supongo que te gusta

Origin blog.csdn.net/weixin_41114301/article/details/133383942
Recomendado
Clasificación