Предисловие
Сокеты широко используются и очень важны при разработке реальных системных программ. В практических приложениях серверам часто необходимо поддерживать несколько клиентских подключений, поэтому особенно важно реализовать модель сервера с высоким уровнем параллелизма. Сервер с высоким параллелизмом превратился из простой модели сервера с циклическим циклом, предназначенной для обработки небольшого количества одновременных сетевых запросов, в модель сервера с высоким параллелизмом, которая решает проблемы 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 в качестве примера для обобщения методов реализации нескольких распространенных моделей сетевых серверов и, наконец, реализован простой чат из командной строки.
одиночный цикл процесса
Принцип однострочного цикла процесса заключается в том, что основной процесс не взаимодействует с клиентом. Клиент должен сначала подключиться к серверу. Сервер принимает клиентское соединение и считывает данные от клиента, затем обрабатывает и возвращает результат обработки клиент, а затем принимает его.Запрос на подключение следующего клиента.
Преимущества Преимущество модели однопоточного цикла состоит в том, что ее легко и просто реализовать, без проблем и накладных расходов на синхронизацию и блокировку.
недостаток
-
Модель блокировки, последовательная обработка сетевых запросов;
-
Он не использует преимущества многоядерных процессоров, а сетевые запросы обрабатываются последовательно;
-
Невозможно поддерживать несколько клиентских подключений одновременно;
-
Программа работает последовательно, и сервер не может отправлять и получать данные одновременно.
Повторное использование ввода-вывода в одном потоке
epoll обычно используется в качестве механизма мультиплексирования ввода-вывода на серверах Linux с высоким уровнем параллелизма. Поток регистрирует все события чтения и записи сокета, которые необходимо обработать в epoll.Когда происходит сетевой ввод-вывод, epoll_wait возвращает значение, а поток проверяет и обрабатывает запрос на сокете.
преимущество
-
Реализация проста, уменьшая накладные расходы на блокировку и переключение потоков.
недостаток
-
Можно использовать только одноядерный процессор. Если время обработки слишком велико, вся служба зависнет;
-
Когда количество клиентов превысит определенное количество, производительность значительно упадет;
-
Он подходит только для сценариев с большим количеством операций ввода-вывода, небольшими вычислениями и коротким временем обработки дескриптора.
Многопоточность/Несколько процессов
Основной особенностью многопоточной и многопроцессной модели является то, что каждый сетевой запрос обрабатывается процессом/потком, а внутри потока используются блокирующие системные вызовы.С точки зрения разделения функций потока, обработку может выполнять отдельный поток. принимает соединение, а остальные потоки обрабатывают определенные сетевые запросы (сбор пакетов, обработка, отправка пакетов); несколько процессов также могут прослушивать и принимать сетевые соединения по отдельности.
преимущество:
1. Реализация относительно проста, 2. Используются многоядерные ресурсы ЦП.
недостаток:
1. Поток все еще заблокирован внутри. Возьмем крайний пример: если поток спит в бизнес-логике дескриптора, он также зависнет.
Многопоточное/многопроцессное мультиплексирование ввода-вывода
Многопоточная, многопроцессная модель ввода-вывода, каждый дочерний процесс прослушивает службу и использует механизм epoll для обработки сетевого запроса процесса.После того, как дочерний процесс принимает(), он создает подключенный дескриптор, а затем общаться с клиентом через подключенный дескриптор терминальной связи. Этот механизм подходит для сценариев с высоким уровнем параллелизма.
преимущество:
-
Поддержка более высокого параллелизма.
недостаток:
-
Асинхронное программирование неинтуитивно и подвержено ошибкам.
Многопоточность разделяет роли ввода-вывода
Основными функциями многопоточных ролей ввода-вывода являются: поток принятия обрабатывает установление новых соединений; пул потоков ввода-вывода обрабатывает сетевой ввод-вывод; а пул потоков обработки обрабатывает бизнес-логику. Сценарии использования, такие как: приложения телемаркетинга, бережливый TThreadedSelectorServer.
преимущество:
-
Разделите потоки по разным функциям, и каждый поток обрабатывает фиксированные функции, что более эффективно.
-
Количество потоков можно настроить в соответствии с бизнес-характеристиками для оптимизации производительности.
недостаток:
-
Межпотоковая связь требует введения накладных расходов на блокировку.
-
Логика сложна и трудно реализуема.
краткое содержание
Выше представлены общие модели сетевых серверов, а также AIO, сопрограммы и даже другие варианты, которые здесь не будут обсуждаться. Важно понять проблемы, с которыми сталкиваются в каждом сценарии, и характеристики каждой модели, а также разработать решение, соответствующее сценарию приложения, которое является хорошим решением.
Модель многопоточного параллельного сервера
Ниже мы в основном обсуждаем модель многопоточного параллельного сервера.
Структура кода
Структура кода параллельного сервера выглядит следующим образом:
thread_func()
{
while(1) {
recv(...);
process(...);
send(...);
}
close(...);
}
main(
socket(...);
bind(...);
listen(...);
while(1) {
accept(...);
pthread_create();
}
}
Как видно из вышеизложенного, сервер разделен на две части: основной поток и подпоток.
основная тема
Основной функцией является основной поток, и его основные задачи заключаются в следующем:
-
Socket() создает сокет прослушивания;
-
bind() связывает номер порта и адрес;
-
Listen() включает прослушивание;
-
Accept() ожидает соединения клиента,
-
Когда клиент подключается, метод Accept() создаст новый сокет new_fd;
-
Основной поток создаст дочерний поток и передаст ему new_fd.
дочерний поток
-
Функция подпотока — thread_func(), которая обрабатывает все задачи связи с клиентом через new_fd.
Подробные шаги для подключения клиента к серверу
Ниже мы рассмотрим пошаговую инструкцию подключения клиента к серверу.
1. Клиент подключается к серверу
-
Сервер устанавливает прослушивающий сокет Listen_fd и инициализирует его;
-
Клиент создает сокет fd1;
-
Клиент client1 подключается к Listen_fd сервера через сокет fd1;
2. Основной поток создает подпоток thread1.
-
После того, как сервер получит запрос на соединение от client1, функция accpet вернет новый сокет newfd1;
-
Позже связь между сервером и клиентом1 будет зависеть от newfd1, а прослушивающий сокет Listen_fd продолжит отслеживать соединения других клиентов;
-
Главный поток создает подпоток thread1 с помощью pthead_create() и передает newfd1 потоку1;
-
Связь между сервером и клиентом1 зависит от newfd1 и fd1 соответственно.
-
Чтобы получать информацию, отправленную сервером, в режиме реального времени, клиент1 также должен иметь возможность читать данные с клавиатуры.Эти две операции являются блокирующими.Когда данных нет, процесс переходит в спящий режим, поэтому дочерний поток read_thread должен быть созданный;
-
Основной поток client1 отвечает за чтение данных с клавиатуры и отправку их на сервер, а подпоток read_thread отвечает за получение информации с сервера.
3. клиент2 подключается к серверу
-
Клиент client2 создает сокет fd2;
-
Подключите Listen_fd сервера через функцию подключения;
4. Основной поток создает подпоток thread2.
-
После того, как сервер получит запрос на соединение от client2, функция accpet вернет новый сокет newfd2;
-
Позже связь между сервером и клиентом2 будет зависеть от newfd2, а прослушивающий сокет Listen_fd продолжит отслеживать соединения других клиентов;
-
Главный поток создает подпоток thread2 с помощью pthead_create() и передает newfd2 в поток2;
-
Связь между сервером и клиентом1 зависит от newfd2 и fd2 соответственно.
-
Аналогично, клиент2 должен создать дочерний поток read_thread, чтобы получать информацию, отправленную сервером, в реальном времени и читать данные с клавиатуры;
-
Основной поток client1 отвечает за чтение данных с клавиатуры и отправку их на сервер, а подпоток read_thread отвечает за получение информации с сервера.
Как видно из рисунка выше, после того как каждый клиент подключается к серверу, сервер создает специальный поток, отвечающий за общение с клиентом; каждый клиент и сервер имеют фиксированную пару комбинаций fd для подключения.
Пример
Ладно, с теорией я закончил.По практике Иицзюня я тоже унаследовал учение своего предка:говорить дешево,показать мой код.Статьи,которые пишут только теорию без кодирования -просто хулиганы.
Основные функции этого примера описаны следующим образом:
-
Разрешить нескольким клиентам одновременно подключаться к серверу;
-
Клиент может отправлять и получать данные самостоятельно;
-
После того, как клиент отправит данные на сервер, сервер вернет данные клиенту в целости и сохранности.
Сервис-Терминал
/*********************************************
服务器程序 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.Команда компиляции выглядит следующим образом:
-
gcc sc -os -lpthread
-
gcc cli.c -oc -lpthread сначала проверьте на этом компьютере
-
Откройте терминал./s 8888
-
Откройте другой терминал./cl 127.0.0.1 8888 и введите строку «qqqqqqq».
-
Откройте другой терминал./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;
}
Почему нам нужно выделить часть памяти специально для хранения этого нового сокета?
Это очень тонкая ошибка, которую допускают многие новички. В следующей главе я объясню это конкретно вам.