当前位置: 首页 > backend >正文

网络编程(2)—多客户端交互

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.多进程接收多客户端

创建多个进程来实现接收多个客户端

#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 listenif (listen(fd,5) < 0){perror("listen fail");return -1;}struct sockaddr_in cliaddr;bzero(&cliaddr,0);socklen_t len = sizeof(cliaddr);//step4 acceptsignal(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.多线程接收多客户端

创建多个线程来实现接收多个客户端

#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);

3. 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;
}
}

缺点
1. 最大监听数受限:`FD_SETSIZE` 默认 1024(Linux)
2. 每次调用需重置 fd_set:内核会修改集合,必须每次重新 `FD_SET`
3. 用户态与内核态拷贝开销大
4. 返回后仍需遍历所有 fd 才能知道哪个就绪
5. 效率随 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 的改进
1. 无 1024 限制:只要系统允许打开足够多 fd
2. 无需重置集合:`events` 和 `revents` 分离
3. 更清晰的事件机制
4. 效率更高:仅遍历传入的数组,不遍历整个 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复用:

#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;
}

http://www.xdnf.cn/news/19023.html

相关文章:

  • Uniapp + UView + FastAdmin 性格测试小程序方案
  • Qt类-扩充_xiaozuo
  • 龙巍:探究青铜器在木雕中的运用
  • 学习:uniapp全栈微信小程序vue3后台(6)
  • 【国内电子数据取证厂商龙信科技】ES 数据库重建
  • 【Flask】测试平台开发,产品管理实现添加功能-第五篇
  • DevOps
  • 沃尔玛AI系统Wally深度拆解:零售业库存周转提速18%,动态定价争议与员工转型成热议点
  • GitHub宕机自救指南
  • 2024年12月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • 构建免费的音视频转文字工具:支持多语言的语音识别项目
  • 2025 DDC系统选型白皮书:构建高效低碳智慧楼宇的核心指南
  • MySQL 深分页:性能优化
  • SQL-Server分页查询多种方法讲解以及分页存储过程
  • Total PDF Converter多功能 PDF 批量转换工具,无水印 + 高效处理指南
  • 【Big Data】Alluxio 首个基于云的数据分析和开源AI数据编排技术
  • Zynq开发实践(FPGA之按键输入)
  • el-select多选下拉框出现了e611
  • 参数模板优化配置指南:从基础到进阶的完整解决方案
  • 学习游戏制作记录(音频的制作和使用)8.28
  • iOS开发之苹果系统包含的所有字体库
  • Node.js汉字转拼音指南:pinyin-pro全解析
  • R 语言 + 卒中 Meta 分析
  • 神经网络|(十六)概率论基础知识-伽马函数·中
  • vant Overlay 遮罩层内元素无法滚动解决方案
  • Java 大视界 -- Java 大数据在智能安防入侵检测系统中的多模态数据融合与检测精度提升(405)
  • 手写链路追踪
  • 新手向:从零开始理解百度语音识别API的Python实现
  • 跨境物流数字化转型怎么做?集运/转运系统定制,源码交付,助力企业降本增效,抢占市场先机
  • 【前端教程】JavaScript 对象与数组操作实战:从基础到优化