TCP要实现一对多需要用到TCP并发服务器,而服务器分为两种
1,单循环服务器:服务端同一时刻只能处理一个客户端任务
2,并发服务器:服务端同一时刻可以处理多个客户端的任务,例如UDP
TCP服务端并发模型
1,多进程:资源开销大,安全性高
2,多线程:资源开销小,相同资源环境下并发量大
3,线程池:为了解决多进程或者多线程,再服务器运行过程中频繁你创建和销毁带来的时间消耗问题,基于生产者和消费者编程模型,以及任务队列等,实现的一套多线程框架
4,IO多路复用
多进程
流程:

cs
#include<stdio.h>
#include "head.h"
#define N "192.168.214.210"
int creat_TCP()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("socket error");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(50000);
addr.sin_addr.s_addr = inet_addr(N);
int bin = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if(bin < 0)
{
perror("bind error");
return -1;
}
return sockfd;
}
//多个客户端接入时,进行三次握手所使用的套接字文件描述符相同
void wait_handler(int signo)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD, wait_handler);
int sockfd = creat_TCP();
listen(sockfd, 2);
struct sockaddr_in addr;
socklen_t clien = sizeof(addr);
while(1)
{
int connfd = accept(sockfd, (struct sockaddr *)&addr, &clien);
if(connfd < 0)
{
perror("accept error");
return -1;
}
pid_t pid = fork();
if(0 == pid)
{
char buff0[100] = "\0";
while(1)
{
int rec = recv(connfd, buff0, sizeof(buff0), 0);
if(0 == rec)
{
printf("[%s : %d] : offline\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
break;
}
if(rec < 0)
{
perror("error");
break;
}
printf("[%s : %d] : %s\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), buff0);
strcat(buff0, "---->ok");
send(connfd, buff0, strlen(buff0), 0);
memset(buff0, 0, sizeof(buff0));
}
close(connfd);
}
}
close(sockfd);
return 0;
}
多线程
流程:

注意:在创建之后不能使用pthread_join(),因为该函数会阻塞,无法导致服务端再次accept,如果要结束可以将线程设置成分离属性
cs
#include<stdio.h>
#include"head.h"
#define N "192.168.110.231"
struct address
{
int connfds;
struct sockaddr_in cliaddrs;
socklen_t clilens;
};
//定义结构体,将端口号,地址,套接字绑定,便于线程传递参数
int creat_socket()//建立套接字并绑定
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("socket error");
return -1;
}
//允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
struct sockaddr_in addr;//确认地址信息
addr.sin_family = AF_INET;
addr.sin_port = htons(50000);
addr.sin_addr.s_addr = inet_addr(N);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
return sockfd;
}
void *task(void *arg)
{
struct address addres= *((struct address *)arg);
//强制类型转换传入的套接字指针
//不能放在while里,主进程在进行多个线程任务时,arg指向的内容改变,导致connfd改变
char buff[100] = "\0";
while(1)
{
int rec = recv(addres.connfds, buff, sizeof(buff), 0);//接收客户端信息
printf("[%s : %d] : %s\n", inet_ntoa(addres.cliaddrs.sin_addr), ntohs(addres.cliaddrs.sin_port), buff);//输出信息与地址,并进行字节序的转变
if(0 == rec)
{
printf("offline\n");
break;
}
if(rec < 0)
{
perror("connfd error\n");
break;
}
strcat(buff, "----->ok");
send(addres.connfds, buff, strlen(buff), 0);
memset(buff, 0, sizeof(buff));//清空buff
}
close(addres.connfds);
}
int main()
{
int sockfd = creat_socket();
listen(sockfd, 3);
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);//保存客户端地址
struct address addres;
while(1)
{
int connfd = accept(sockfd, (struct sockaddr *) &cliaddr, &clilen);
if(connfd < 0)
{
perror("accept error");
continue;
}
addres.connfds = connfd;
addres.cliaddrs = cliaddr;
pthread_t thread;//保存线程地址
pthread_create(&thread, NULL, task, &addres);
pthread_detach(thread);
}
close(sockfd);
}

进线程区别
多进程:进程资源开销大;安全性高
多线程:相同资源环境下资源开销小,并发量大
线程池
基于生产者和消费者编程模型,以及任务队列等,实现的一套多线程框架。
目的:为了解决多线程或者多进程模型,在服务器运行过程,频繁创建和销毁线程(进程)带来的时间消耗问题。
流程:提前创建一堆线程,阻塞等待接收任务。主线程(生产者)负责接任务保存在任务队列中,空闲的次线程(消费者 )负责从队列中执行任务
IO多路复用(IO多路转接)
对多个文件描述符的读写可以复用一个进程。本质为在不创建新的进程和线程的前提下,使用一个进程实现对多个文件读写的同时监测
IO多路复用的缺点:在处理耗时任务时不占优势,例如对音视频进行处理时候,因为只有一个进程,所以其他任务来临时, 没有空闲的进程去处理
|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| 方式 | 特点 |
| select | 1. 使用位图(数组)实现对文件描述符集合的保存,最多允许同时监测1024个文件描述符; 2. 需要应用层和内核层的反复数据(文件描述符集合表)拷贝,比较费时; 3. 返回的集合表需要遍历寻找到达的事件; 4. 只能工作在水平触发模式(低速模式),不能工作在边沿触发模式(高速模式) |
| poll | 1. 使用链表实现对文件描述符集合的保存,没有了监测的文件描述符上限限制; 其余和select相同,即poll只是改良了select的第一个特点 |
| epoll | 1. 使用红黑树(二叉树)实现文件描述符集合的存储,没有文件描述符上限限制,提高查找效率; 2. 将文件描述符集合创建在内核层,避免了应用层和内核层的反复数据拷贝; 3. 返回的是到达事件,不需要遍历,只需要处理事件即可; 4. 可工作在水平触发模式(低速模式),也可工作在边沿触发模式(高速模式) |
水平触发模式:只要 IO 资源 "有数据待处理",就持续通知程序
边沿触发模式:只有 IO 资源 "状态从'无数据'变'有数据'" 时,才通知一次
原理:对于文件描述符(0,fifofd)的监测任务交给Linux内核,三种方式会将关注的描述符通知给内核,内核会在后台监测文件描述符的事件,当某个文件描述符的事件到达时,内核会通过三个函数返回给应用层,应用层会知道哪个事件以及哪个文件描述符的事件


横坐标:监测的文件描述符个数
纵坐标:花费的时间
select
步骤
1,创建文件描述符集合
2,添加关注的文件描述符到集合
3,使用select传递集合表给内核,内阁开始监测事件
4,当内核监测到事件时,应用层select将解除阻塞,并获得相关的事件结果
5,根据select返回的结果做不同的任务处理

小写名称为函数,大写名称为系统定义的带参宏即带参宏,可直接使用
fd:文件描述符
fd_set:文件描述符集合表
FD_CLR: 在set集合表内将fd文件描述符删除(clear)
FD_ISSET:判断fd是否在set内(is set?)
FD_SET: 在set内添加fd(set) 在则返回1,否则则0
FD_ZERO:将set清空(zero)
功能:传递文件描述符集合表给内核,并等待事件结果
参数:
nfds:关注的最大文件描述符+1
readfds:读事件的文件描述符集合,如果没有则传NULL,下两个同理
writefds:写事件的文件描述符集合
exceptfds:其他事件的文件描述符集合
timeout:设置select监测时的超时时间(即阻塞等待的时间)
NULL :不设置超时时间,即一直阻塞等待
返回值:
成功:内核监测的到达事件的个数
失败:-1
0:超时时间到达,但没有事件发生
集合表
对于select的集合监听表,即fd_set,本质上是一段连续的内存空间(可以理解为数组),称之为位图,因为其按照位去管理文件描述符。
位图最多允许保存1024个文件描述符,会将文件描述符对应的位置置1,如果不关注则为0,以下图为例
此时只关注0和fifofd,所以在位图中如下,因为fifofd是3(文件描述符按照最小未被分配原则去分配),所以在3的位置置为1。
通过select将左侧表传递给右侧Linux内核空间,而内核空间则会遍历该数组寻找1的位置,根据文件描述符最小为被分配原则,虽然fifofd = 3,但是需要遍历到第4个位置
更直观地解释select的第一个参数,这是由于ndfds的定义是需要检查的文件描述符数量,而由于Linux内核文件描述符从0开始,所以数量=最大描述符+1
select成功后将表传给内核去监测,内核监测到结果时,会对进行修改,并将修改过后的表返回给应用层到原来的表上,假设管道事件到达而终端事件没到达,则会把表的第0个位置置为0,表示终端事件未到达,其余不变,于是表中置1处则为到达事件的文件描述符值得注意的是,修改过后的表返回给应用层时,会覆盖原先的表,这会导致原先的数据丢失,即描述符0在表中的位置一直是0而不是1,所以需要备份,使得无论集合表还是内核更改过后的表经过备份,而不会更改原本的数据,打个比方,交换a和b的数据需要用到另一个变量c



TCP并发

cs
#include<stdio.h>
#include"../../head.h"
#define N "192.168.115.210"
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(50000);
addr.sin_addr.s_addr = inet_addr(N);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 3);
//建立套接字,绑定服务端
fd_set TCP_select;
fd_set temp;
//创建集合表与临时集合表
FD_ZERO(&TCP_select);
FD_SET(sockfd, &TCP_select);
//清空并添加套接字对应的文件描述符
int maxfd = sockfd;
while(1)
{
temp = TCP_select;
//更新临时集合表
char buff[100] = "\0";
int cnt = select(maxfd + 1, &temp, NULL, NULL, NULL);
if(cnt < 0)
{
perror("select error");
return -1;
}
//传递文件描述符集合表
if(FD_ISSET(sockfd, &temp))
//判断是否为新接入的套接字
{
int connfds = accept(sockfd, NULL, NULL);
//进行三次握手后产生新的套接字
if(connfds < 0)
{
perror("accept error");
return -1;
}
FD_SET(connfds, &TCP_select);
//将新的套接字加入原始集合表中
maxfd = maxfd > connfd ? maxfd : connfd;
//更新最大关注的文件描述符
}
for(int i = sockfd + 1; i <= maxfd; i++)
//遍历集合表中的文件描述符
{
if(FD_ISSET(i, &temp))
{
ssize_t cnt = recv(i, buff, sizeof(buff), 0);
if(cnt <= 0)
//判断连接失败或者断开连接
{
if(cnt < 0) perror("recv error");
FD_CLR(i, &TCP_select);
close(i);
continue;
}
puts(buff);
strcat(buff, "----ok");
cnt = send(i, buff, strlen(buff), 0);
if(cnt < 0)
//判断发送失败
{
perror("send error");
FD_CLR(i, &TCP_select);
close(i);
continue;
}
memset(buff, 0, sizeof(buff));
//更新buff
}
}
}
close(sockfd);
return 0
}
epoll
步骤
1,创建文件描述符集合:epoll_create
2,添加关注的文件描述符:epoll_ctl
3,epoll通知内核开始进行事件监测:epoll_wait
4,epoll返回时,获取到达事件的结果
5,根据到达事件做任务处理

epoll_create


功能:通知内核创建文件描述符集合表
参数:监测的文件描述符个数
返回值:成功则为一个非负的文件描述符,代表了内核创建的集合表,失败为-1
epoll_ctl(control)

功能:对epoll的文件描述符集合进行操作
参数:
epfd:epoll_create创建的epoll集合
op:对文件描述符集合进行的操作
EPOLL_CTL_ADD:添加文件描述符到集合(add)
EPOLL_CTL_MOD:修改集合中的文件描述符(modify)
EPOLL_CTL_DEL:删除集合中的文件描述符(delete)
fd:要操作的文件描述符
event:文件描述符对应的事件(如果是NULL则不关注,一般用于删除)
events:关注的文件描述符事件
EPOLLIN:读事件
EPOLLOUT:写事件
data.fd:关注的文件描述符
返回值:成功则0失败-1
epoll_wait

功能:通知内核开始监测文件描述符的事件
参数:
epfd:监测的文件描述符集合
events:保存返回的到达事件的结果(数组)
传入方式:struct epoll_event evs[MAX_FD_CNT],传evs
因为到达事件的结果可能不止一个,所以要传入数组
maxevents:最大的事件个数
timeout:超时时间
-1:不设置超时,一直阻塞
返回值:
成功:到达的事件的个数
失败:-1
写端与select相同,下面为读端
cs
//向集合中添加文件描述符
int epoll_fd_add(int epfds, int fd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd;
int ret = epoll_ctl(epfds, EPOLL_CTL_ADD, fd, &ev);
if(ret < 0)
{
perror("epoll_ctl error");
return -1;
}
}
int main()
{
char buff[1024] = {0};
mkfifo("./myfifo", 0664);
int fifofd = open("./myfifo", O_RDONLY);
if (fifofd < 0)
{
perror("open fifo error");
return -1;
}
//创建内核中的文件描述符集合
int epfds = epoll_create(2);
if(epfds < 0)
{
perror("epoll error");
return -1;
}
//添加文件描述符的集合
epoll_fd_add(epfds, 0, EPOLLIN);
epoll_fd_add(epfds, fifofd, EPOLLIN);
//保存到达事件的集合
struct epoll_event evs[2];
while (1)
{
//阻塞等待事件到达
int cnt = epoll_wait(epfds, evs, 2, -1);
if(cnt < 0)
{
return -1;
}
for(int i = 0; i < cnt; i++)
{
if(0 == evs[i].data.fd)
{
fgets(buff, sizeof(buff), stdin);
printf("STDIN : %s\n", buff);
memset(buff, 0, sizeof(buff));
}
else if(fifofd == evs[i].data.fd)
{
read(evs[i].data.fd, buff, sizeof(buff));
printf("FIFO : %s\n", buff);
memset(buff, 0, sizeof(buff));
}
}
}
close(fifofd);
return 0;
}
TCP并发

cs
#include<stdio.h>
#include"../head.h"
#define N "192.168.112.210"
int epoll_ctl_add(int epfd, int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
//定义结构体接入文件描述符以及要加入的集合表
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if(ret < 0)
{
perror("epoll_add error");
return -1;
}
//调用epoll_ctl函数,第二个参数固定
}
//向集合表中添加文件描述符
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(50000);
addr.sin_addr.s_addr = inet_addr(N);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 3);
//绑定地址和端口号
int epfd = epoll_create(10);
if(epfd < 0)
{
perror("epoll_create error");
return -1;
}
//创建集合表
epoll_ctl_add(epfd, sockfd);
struct epoll_event evs[10];
//调用函数加入用来接收的套接字,并创建保存客户端的文件描述符
while(1)
{
char buff[100] = "\0";
int cnt = epoll_wait(epfd, evs, 10, -1);
if(cnt < 0)
{
perror("epoll_wait error");
return -1;
}
//阻塞等待集合表中的事件,并保存到达的事件
for(int i = 0; i < cnt; i++)
{
if(sockfd == evs[i].data.fd)
{
int connfd = accept(sockfd, NULL, NULL);
epoll_ctl_add(epfd, connfd);
}
//判断接入新的客户端,并加入新的套接字
else
{
int rec = recv(evs[i].data.fd, buff, sizeof(buff), 0);
if(rec <= 0)
{
if(rec < 0) perror("recv error");
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
close(evs[i].data.fd);
continue;
}
//判断是否断开
puts(buff);
strcat(buff, "---ok");
int sen = send(evs[i].data.fd, buff, strlen(buff), 0);
if(sen <= 0)
{
if(sen < 0) perror("send error");
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
close(evs[i].data.fd);
continue;
}
memset(buff, 0, sizeof(buff));
//清空buff
}
}
}
close(sockfd);
}
在此梳理一下,尤其是epoll_wait函数作用,以sockfd为例。先通过ADD将其添加到epfd中,然后wait阻塞等待,当sockfd事件触发时,内核监测到事件发生,便将sockfd对应的event结构体存入evs中,然后通过遍历evs,直到sockfd对应上里面的某个结构体的fd,然后执行后续程序。
在断开以及接收出错时,要先在集合表中删除该文件描述符再进行关闭,如过反过来先关闭则会报错无法进行删除操作





