1.多客户端
1.单循环服务器
socket //listenfd
bind //绑定服务器端的地址
listen //监听客户端的连接请求 --- 请求队列
while(1) //提取多个客户端的连接请求 建立连接
{
confd = accpet //请求队列中提取已连接的请求 返回连接好的socket的fd
//通信
while(1)
{
read
sprintf
write
}
}
单循环服务器特点:
1.可以处理多个客户端 (不能同时)
2.效率不高
2.并发服务器
socket //listenfd
bind //绑定服务器端的地址
listen //监听客户端的连接请求 --- 请求队列
while(1) //提取多个客户端的连接请求 建立连接
{
confd = accpet //请求队列中提取已连接的请求 返回连接好的socket的fd
//通信
//进程
pid = fork();
if (pid < 0)
{
perror("fork fail");
return -1;
}
if (pid == 0) //子进程
{
while(1)
{
read
sprintf
write
}
}
}
注:当每次服务器端按ctrl+c结束时,马上再运行server.c,会导致无法连接到原地址,此时需要用到setsockopt
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
功能:
设置socket的属性
参数:
@sockfd --- 要设置的socket
@level --- 设置socket层次 //socket本身 tcp ip
@optname --- 选项名字
@optval --- 选项值
@optlen --- 长度
设置一个选项(开启一个功能) ---让地址重用
用法:
int on = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on, sizeof(int)); //表示 对 listenfd这个socket 开启 地址重用的功能
1.多进程接收多客户端
创建多个进程来实现接收多个客户端
cs
#include "head.h"
void do_child (int signo)
{
wait(NULL);
}
int main(int argc, char const *argv[])
{
//step1 socket
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
struct sockaddr_in seraddr;
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int on = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on, sizeof(int));
//step2 bind
if (bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
//step3 listen
if (listen(fd,5) < 0)
{
perror("listen fail");
return -1;
}
struct sockaddr_in cliaddr;
bzero(&cliaddr,0);
socklen_t len = sizeof(cliaddr);
//step4 accept
signal(SIGCHLD,do_child);
while (1)
{
int connfd = accept(fd,(struct sockaddr *)&cliaddr,&len);
if (connfd < 0)
{
perror("accept fail");
return -1;
}
printf("---client connect---\n");
printf("client ip:%s\n",inet_ntoa(cliaddr.sin_addr));
printf("port: %d\n",ntohs(cliaddr.sin_port));
//创建 子进程
//让子进程去通信
pid_t pid = fork();
if (pid < 0)
{
perror("fork fail");
return -1;
}
if (pid == 0)
{
char buf[1024];
while(1)
{
recv(connfd,buf,sizeof(buf),0);
printf("buf = %s\n",buf );
if (strncmp(buf,".quit",5) == 0)
{
close(connfd);
exit(0);
}
}
}
}
return 0;
}
2.多线程接收多客户端
创建多个线程来实现接收多个客户端
cs
#include "head.h"
void do_child (int signo)
{
wait(NULL);
}
void *thread(char *arg)
{
int connfd = *(int *)arg;
char str[256] = {0};
while(1)
{
recv(connfd, str, sizeof(str), 0);
printf("rec:%s\n", str);
if(0 == strcmp(str, ".quit"))
{
close(connfd);
return NULL;
}
}
}
int main(int argc, char const *argv[])
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
{
perror("fail to socket");
return -1;
}
struct sockaddr_in seraddr;
bzero(&seraddr, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int));
if(bind(listenfd, (const struct sockaddr *)&seraddr, sizeof(seraddr)) < 0)
{
perror("fail to bind");
return -1;
}
if(listen(listenfd, 5) < 0)
{
perror("fail to listen");
return -1;
}
struct sockaddr_in cliaddr;
bzero(&cliaddr, sizeof(cliaddr));
socklen_t len = sizeof(cliaddr);
pthread_t tid;
while(1)
{
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len);
if(connfd < 0)
{
perror("fail to accept");
return -1;
}
printf("连接成功\n");
int ret = pthread_create(&tid, NULL, thread, &connfd);
if(ret != 0)
{
errno = ret;
perror("pthread_create fail");
return -1;
}
pthread_detach(tid);
}
close(listenfd);
return 0;
}
注:并发服务器 ---多进程方式的效率 肯定 低于 多线程
3.多路IO复用
1.阻塞IO模型
- scanf
- getchar
- fgets
- read
- recv
以读为例:读操作--->内核中读取数据--->如果没有数据,一直等到,直到有数据--->之后将数据带回到用户空间
2.非阻塞IO模型
以读为例:读操作--->内核中读取数据--->如果没有数据,不等,直接返回用户空间
设置非阻塞:
函数接口:
- fcntl
int fcntl(int fd, int cmd, ... /* arg */ );
功能:
维护文件描述符
参数:
@fd --- 要操作的fd
@cmd --- 要做的一些操作 //command
@... --- 可变参数
返回值
取决于所做的操作
用法:
int flags;
flags = fcntl(fd,F_GETFL,0); //读文件描述符
flags = flags | O_NONBLOCK; //修改为非阻塞
fcntl(fd,F_SETFL,flags); //将非阻塞写入文件描述符
3.信号驱动IO
int flags = fcntl(fd, F_GETFL);
flags = flags | O_ASYNC; //开启异步信号
fcntl(fd, F_GETFL, flags);
fcntl(fd, F_SETOWN, getpid()); //设置和进程相关联
signal(SIGIO, do_handler); //signal SIGIO的处理函数
4.多路复用IO
1.select
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
);
功能:
实现IO多路复用
@nfds //是关心的文件描述符中最大的那个文件描述符 + 1
@readfds //代表 要关心 的 读操作的文件描述符的集合
@writefds //代表 要关心 的 写操作的文件描述符的集合
@exceptfds //代表 要关心 的 异常的文件描述符的集合
@timeout //超时 --- 设置一个超时时间
//NULL 表示select是一个阻塞调用
//设置时间
// 0 --- 非阻塞
// n (>0) --- 阻塞n这么长时间
//注意: 这个值 每次 自动在往下减少 --直到减少到0
struct timeval
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timeval t = {0,0};
返回值:
成功 返回就绪的文件描述符的数量
失败 -1
使用:
1.建立一张表 监控
fd_set readfds; //一张表
FD_ZERO(&readfds); //清空这张表
2.将要监控的文件描述符 添加表中
FD_SET(0,&readfds);
FD_SET(fd,&readfds);
- nfds = fd + 1;
select(nfs,&readfds,NULL,NULL,NULL)
void FD_CLR(int fd, fd_set *set); //将fd从set集合中清除
int FD_ISSET(int fd, fd_set *set);//判断fd是否在set中
void FD_SET(int fd, fd_set *set);//将fd添加到set集合中
void FD_ZERO(fd_set *set);//将set集合清空
多路IO复用:
listenfd = socket
bind
listen
1.准备表
fd_set readfds;
FD_ZERO(&readfds);
2.添加要监控的文件描述符
FD_SET(listenfd,&reafds);
3.准备参数
maxfds = listenfd + 1;
fd_set backfds;
while (1)
{
backfds = readfds;
int ret = select(maxfds,&backfds,NULL,NULL,NULL);
if (ret > 0)
{
int i = 0;
for (i = 0; i < maxfds;++i)
{
if (FD_ISSET(i,&backfds))
{
if (i == listenfd) //连接
{
connfd = accept();
//connfd 要被添加到 监控表
FD_SET(connfd,&readfds);
if (connfd + 1 > maxfds)
maxfds = connfd + 1;
}else //负责与客户端通信
{
// i = ?//fd 此时就绪
printf("buf = %s\n",buf);
if (strncmp(buf,"quit",4) == 0)
{
FD_CLR(i,&readfds); //清除对应的客户端的fd
close(i);
}
}
}
}
}
}
//优化
int i = maxfds;
for (i = maxfds-1; i >= 0; --i)
{
if (FD_ISSET(i,&readfds))
{
maxfds = i + 1;
}
}
缺点 :
-
最大监听数受限:`FD_SETSIZE` 默认 1024(Linux)
-
每次调用需重置 fd_set:内核会修改集合,必须每次重新 `FD_SET`
-
用户态与内核态拷贝开销大
-
返回后仍需遍历所有 fd 才能知道哪个就绪
-
效率随 fd 数量增长下降明显
2.poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
对文件描述符监控
参数:
@fds
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
events 事件:
POLLIN 读
POLLOUT 写
POLLERR 错误
@nfds 表示要监控的文件描述符的数量
@timeout 时间值
-1 //阻塞
时间值 单位 是 ms (毫秒)
0 //非阻塞
n(>0) //阻塞 n ms(毫秒)
返回值:
成功 表示 就绪的数量
0 超时情况下表示 没有就绪实际
失败 -1
点对点聊天:
1.准备监控表
struct pollfd fds[10]; //监控表示 10个fd 2.添加要监控的文件描述符 //点对点聊天
//两路io -- stdin / sockfd
int nfds = 0;
fds[0].fd = 0;
fds[0].events = POLLIN;
nfds++;
fds[1].fd = sockfd;
fds[1].events = POLLIN;
nfds++;
3.准备参数
while (1)
{
int ret = poll(fds,nfds,-1);
if (ret > 0)
{
int i = 0;
for (i = 0; i < nfds; ++i)
{
if(fds[i].revents&POLL_IN)
{
if (fds[i].fd == 0)
{
fgets
send
}else //sockfd
{
recv
printf
}
}
}
}
}
多路IO复用:
listenfd = socket
bind
listen
1.准备表
struct pollfd fds[10];
2.添加要监控的文件描述符
int nfds = 0;
fds[0].fd = listenfd;
fds[0].events = POLL_IN;
nfds++;
while (1)
{
int ret = poll(fds,nfds,-1);
if (ret > 0)
{
int i = 0;
for (i = 0; i < nfds;++i)
{
if (fds[i].revents&POLL_IN))
{
if (fds[i].fd == listenfd) //连接
{
connfd = accept();
//connfd 要被添加到 监控表
fds[nfds].fd = connfd;
fds[nfds].events = POLL_IN;
nfds++;
}else //负责与客户端通信
{
// i = ?//fd 此时就绪
recv(fds[i].fd,buf,sizeof(buf),0);
printf("buf = %s\n",buf);
if (strncmp(buf,"quit",4) == 0)
{
fds[i].fd = -1; //-1 不是有效的文件描述符
close(fds[i].fd);
}
}
}
}
}
}
改进与不足:
相比 select 的改进
-
无 1024 限制:只要系统允许打开足够多 fd
-
无需重置集合:`events` 和 `revents` 分离
-
更清晰的事件机制
-
效率更高:仅遍历传入的数组,不遍历整个 fd 范围
仍存在的问题
1.每次调用仍需将整个 `fds[]` 拷贝到内核
2.返回后仍需遍历全部元素查找就绪 fd
3.时间复杂度仍是 O(n),连接数多时性能下降
3.epoll
相关函数:
1.epoll_create
int epoll_create(int size);
功能:
创建一个epoll对象
参数:
@size 忽略,但是必须大于0
返回值:
成功 epoll对象的fd
失败 -1 &&errno
2. epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
控制 epoll对象
参数:
@epfd epoll对象的fd
@op
EPOLL_CTL_ADD //添加
EPOLL_CTL_MOD //修改
EPOLL_CTL_DEL //删除
@fd
//要关心的文件描述符
@event
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = 0; //stdin
EPOLLIN //读
EPOLLOUT //写
EPOLLERR //出错
EPOLLET //边沿触发
返回值:
成功 0
失败 -1 &&errno
3.epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:
监控对应的文件描述符 ,看是否有就绪
参数:
@epfd --epoll对象
@events ---保存就绪结果的 一个数组的首地址
@maxevents ---数组的大小
struct epoll_event ret_ev[2];
@timeout ---超时的时间
时间单位,还是ms (毫秒数)
返回值:
成功 就绪的数量
失败 -1 && errno被设置
使用:
1.epoll_create //创建了一个epoll对象 ---- 监控的表
int epfd = epoll_create(2);
2.添加文件描述符
//一个是 标准输入 0
//一个是 sockfd
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = 0; //标准输入
epoll_ctl(epfd,EPOLL_CTL_ADD, 0, &ev);
int add_fd(int fd, int epfd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd; //标准输入
if ( epoll_ctl(epfd,EPOLL_CTL_ADD, fd, &ev))
{
perror("epoll_ctl add fail");
return -1;
}
return 0;
}
int del_fd(int fd, int epfd) //删除
{
//struct epoll_event ev;
//ev.events = EPOLLIN;
//ev.data.fd = fd; //标准输入
if ( epoll_ctl(epfd,EPOLL_CTL_DEL, fd, NULL))
{
perror("epoll_ctl add fail");
return -1;
}
return 0;
}
add_fd(0,epfd);//标准输入
add_fd(clifd,epfd);//socket
3.监控文件描述符
while (1)
{
struct epoll_event ret_ev[10];
int ret = epoll_wait(epfd, ret_ev,3, -1);
if (ret > 0)
{
int i = 0;
for (i = 0; i < ret; ++i)
{
if (ret_ev[i].data.fd == 0)
{
}else //socket
{
}
}
}
}
多路IO复用:
cs
#include "../head.h"
int add_fd(int fd, int epfd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev))
{
perror("fail to epoll_ctl");
return -1;
}
return 0;
}
int del_fd(int fd, int epfd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
if(epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL))
{
perror("fail to epoll_ctl");
return -1;
}
return 0;
}
void set_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL);
flags = flags | O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
return ;
}
int main(void)
{
int serfd = socket(AF_INET, SOCK_STREAM, 0);
if(serfd < 0)
{
perror("fail to socket");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(bind(serfd, (const struct sockaddr *)&seraddr, sizeof(seraddr)) < 0)
{
perror("fail to bind");
return -1;
}
if(listen(serfd, 5) < 0)
{
perror("fail to listen");
return -1;
}
struct sockaddr_in cliaddr;
bzero(&cliaddr, sizeof(cliaddr));
socklen_t len = sizeof(cliaddr);
int connfd = 0;
char buf[1024] = {0};
int epfd = epoll_create(2);
if(epfd < 0)
{
perror("fail to epoll");
return -1;
}
add_fd(serfd, epfd);
while(1)
{
struct epoll_event ret_ev[2];
int ret = epoll_wait(epfd, ret_ev, 2, -1);
int i = 0;
if(ret > 0)
{
for(i =0; i < ret; i++)
{
if(ret_ev[i].data.fd == serfd)
{
connfd = accept(serfd, (struct sockaddr *)&cliaddr, &len);
if(connfd < 0)
{
perror("fail to accept");
return -1;
}
printf("---client connect---\n");
set_nonblock(connfd); //设置非阻塞
add_fd(connfd, epfd);
}
else
{
int n = recv(ret_ev[i].data.fd, buf, 1, 0);
printf("n = 0, buf = %s\n",n, buf);
if(n < 0 && errno != EAGAIN) //此时为接收完数据了没数据可接收了
{
perror("recv ");
del_fd(ret_ev[i].data.fd,epfd);
close(ret_ev[i].data.fd);
}
if(n== 0 || 0 == strcmp(buf, ".quit")) //此时为客户端退出了
{
del_fd(ret_ev[i].data.fd,epfd);
close(ret_ev[i].data.fd);
}
}
}
}
}
return 0;
}