Linux-C SOCKET-IO多路复用技术 04.03
Qt中编写客户端的思路总结(6 个细节问题)
1、客户端什么时候连接?
在第一一个窗口之前进行打通或者第一次做业务,确保所做的功能已经对接上,
否则都是在做无用功。
2、界面上数据的怎么传输?
(1)因为对读写与网络进行了单独的封装(写成单例),导致数据无法直接进
行获取与反馈,于是面向对象获取数据(创建set和get函数)对该问题进行
解决
(2)聊天与登录界面的结构体不同,在数据传输时,无法直接使用void* (在
Qt使用时只有8字节,无法完整传递),于是在传递数据时需要明确传递数据
的长度
3、头文件相互引用
使用emit自定义信号解决,自有其他文件的槽函数对信号进行接收
4、聊天时需要明确自己是谁,聊天对象是谁(在登录界面进行传参,明确自己
是谁)
5、业务处理
在界面跳转后,需要明确当前界面需要做什么业务(处理业务后,返回值进行
对应界面的查找与显示)
SOCKET-IO多路复用技术 04.03
碎碎念:
1000个客户端对应1000个进程
每个进程都有对应的:代码段,数据段,堆栈段
一个进程最多开290个线程
非阻塞式IO:持续执行read,持续判断,轮询;轮询占用大量的时间,CPU不休息,持续循环判断
阻塞式IO:等待数据到达;CPU闲置出来可以处理其他业务
多路复用型IO
事件队列:
就绪队列:
事件有三:客户端上线、客户端掉线、客户端发送数据


epoll负责"迎宾",线程池负责并发
三种IO复用技术:
| select | poll | epoll | |
|---|---|---|---|
| 存储结构(连接数) | 数组(数量有限制) | 链表(数量无限制) | 队列(数量有限制,但无伤大雅) |
| I/O效率 | 忙轮询 O(n) | 忙轮询 O(n) | 事件队列 O(1) |
| 消息传递方式 | 从内核传递到用户控件,有内核拷贝动作 | 从内核传递到用户控件,有内核拷贝动作 | 内核和用户空间共享一块内存 |
Select、poll对比epoll:
Select、poll进程只有在调用一定的方法之后,内核才会对所有监视的文件描述符进行扫描,而epoll会通过epoll_ctl()来注册一个文件描述符,一旦某个文件描述符就绪之后,内核会采用类似的回调机制,迅速激活对应的文件描述符。这样当有大量等待链接的时候epoll的效率会比select和poll高出很多。
这里我们对比一下select poll和epoll
select 链接数有限制,一般来说是1024个,poll和select本质上没有区别,但是链接数不存在限制,因为poll是基于链表来实现的,epoll虽然链接数有一定的限制,但是1G内存的机器可以打开10w左右的链接。
当链接数目剧增的时候select和poll每次都会对所有的链接进行线性遍历,所以当fd增加的时候就会出现性能下降的问题。但是epoll是通过callback回调函数来实现的所以即使有大量的链接,也不会出现性能下降的问题,但是当所有链接都十分活跃的时候可能会出现性能下降的问题。(epoll只管活跃的链接)
Select和poll在传递消息的时候需要将消息传递到用户空间,都需要内核进行拷贝动作,但是epoll是通过内核和用户之间一块共享内存来实现的。select poll的最大缺点:大量的fd数组从用户复制到内核,并且poll还有一个特点是水平触发,如果报告了fd没有被处理,那么下次poll还会再次报告这个fd文件描述符。随着文件描述符数量的增长效率会大大下降。
setsockopt()
头文件:#include <sys/types.h> #include <sys/socket.h>
功能:用来设置参数 sockfd 所指定的socket 状态
返回值:成功则返回0, 若有错误则返回-1, 错误原因存于errno.
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
//level 代表欲设置的网络层, 一般设成SOL_SOCKET 以存取socket 层
//optname 代表欲设置的选项
//optval 代表欲设置的值
//optlen 代表optval 的长度
optname有以下几种数值:
SO_DEBUG 打开或关闭排错模式
SO_REUSEADDR 允许在bind ()过程中本地地址可重复使用
SO_TYPE 返回socket 形态.
SO_ERROR 返回socket 已发生的错误原因
SO_DONTROUTE 送出的数据包不要利用路由设备来传输.
SO_BROADCAST 使用广播方式传送
SO_SNDBUF 设置送出的暂存区大小
SO_RCVBUF 设置接收的暂存区大小
SO_KEEPALIVE 定期确定连线是否已终止.
SO_OOBINLINE 当接收到OOB 数据时会马上送至标准输入设备
SO_LINGER 确保数据安全且可靠的传送出去.
getsockopt()
功能:会将参数 sockfd 所指定的socket 状态返回.
返回值:成功则返回0, 若有错误则返回-1, 错误原因存于errno
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
//level 代表欲设置的网络层, 一般设成SOL_SOCKET 以存取socket 层
//optname 代表欲取得何种选项状态
//optval 则指向欲保存结果的内存地址
//optlen 则为该空间的大小
struct epoll_event {
__uint32_t events; /* Epoll 事件 */
epoll_data_t data; /* 用户数据 */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
其中events成员描述事件类型。epoll支持的事件类型和epoll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上”E”,比如epoll的数据可读事件是EPOLLIN。但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。它们对于epoll的高效运作非常关键
| 事件 | 描述 |
|---|---|
| EPOLLIN | 可读取非高优先级数据 |
| EPOLLPRI | 可读取高优先级数据 |
| EPOLLOUT | 普通数据可写 |
| EPOLLHUP | 出现挂断 |
| EPOLLET | 采用边沿触发事件通知 |
| EPOLLONESHOT | 在完成事件通知后禁用检查 |
| EPOLLRDHUP | 套接字对端关闭 |
| EPOLLERR | 有错误发生 |
epoll_data_t是一个共用体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd
epoll_create()
功能:创建文件描述符
返回值:一个文件描述符fd,用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表
int epoll_create(int size);
//size参数现在并不起作用,只是给内核一个提示,告诉内核应该如何为内部数据结构划分初始大小
epoll被创建后,会占据acceptfd=4,因此后面客户端只能从acceptfd=5开始
epoll_ctl()
功能:修改epoll的内核事件表
返回值:epoll_ctl成功时返回0,失败时返回-1并设置errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd是创建epoll实例时返回的文件描述符
//op参数则指定操作类型
//fd参数是要操作的文件描述符
//event参数指定事件,它是epoll_event结构指针类型
op操作类型有如下三种:
- EPOLL_CTL_ADD,注册新的fd到epfd中;
- EPOLL_CTL_MOD,修改已注册fd的事件;
- EPOLL_CTL_DEL,从epfd中删除一个fd;
epoll_wait()
功能:在一段超时时间内等待一组文件描述符上的事件。
- epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。数组events的空间由调用者负责申请,所包含的元素个数在参数maxevents中指定
- events数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率
返回值:成功时返回就绪的文件描述符个数,失败时返回-1并设置errno
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//events数组只用于输出epoll_wait检测到的就绪事件
//maxevents参数指定最多监听多少个事件,它必须大于0
//timeout参数含义与poll相同:;;
timeout:
当timeout= -1时,阻塞直到有一个或多个文件描述符就绪
当timeout=0时,立即返回
当timeout>0时,等待timeout值的时间
LT模式(水平触发,默认)与ET模式(边缘触发):
epoll里边最大的特点在于水平触发和边缘触发,水平触发是当我们缓冲区有数据的时候就一直提醒,但是边缘触发是当有了消息来了之后只进行一次提醒,如果这次读取了之后缓冲区的数据没有读取完,下次就不再进行提醒。LT也就是水平触发是epoll默认的方式,也就是说当前如果有一个文件描述符就绪了,你可以不马上进行处理,会一直提醒你,但是ET也就是边缘触发模式,是高速工作方式,如果某个未就绪的事件变成了就绪状态,epoll会告诉你,并且不会再为那个描述符发送更多的请求通知,直到你做了某些事情让描述符的状态不再是就绪状态。ET模式很大程度上减少了epoll事件被重复触发的次数。ET模式下必须使用非阻塞的套接字,以免造成某一个文件描述符阻塞让其他文件描述符饿死的情况。
多路IO复用特点:没有线程和进程
#include <sys/types.h>
#include <sys/socket.h>
#include<netinet/in.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>
#include<iostream>
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
int main()
{
struct epoll_event epollEvent;
struct epoll_event epollEventArry[100];
int epollfd = 0;
int epollwaitfd = 0;
struct sockaddr_in s_addr;
int len = 0;
char buf[50] = { 0 };
int socketfd = 0;
int acceptfd = 0;
int client_num = 0;
int opt_val = 1;
socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd == -1)
{
perror("socket error");
}
else
{
s_addr.sin_family = AF_INET;//ipv4
s_addr.sin_addr.s_addr = INADDR_ANY;//Internet地址
s_addr.sin_port = htons(10086);
len = sizeof(s_addr);
//端口复用,解决 address already use问题,一定要放在bind()之前
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, (const void*)opt_val, sizeof(opt_val));
if (bind(socketfd, (struct sockaddr*)&s_addr, len) == -1)
{
perror("bind error");
}
sleep(1);
if (listen(socketfd, 10) == -1)//监听,判断 请求队列中的最大连接数为10
{
perror("listen error");
}
cout << "ready OK" << endl;//网络通道准备好了
bzero(&epollEvent, sizeof(epollEvent));
//绑定当前准备好的socketfd(可用网络对象)
epollEvent.data.fd = socketfd;
//绑定事件为客户端接入事件
epollEvent.events = EPOLLIN;
//创建epoll
epollfd = epoll_create(5);
//将已经准备好的网络描述符添加到epoll事件队列中
epoll_ctl(epollfd, EPOLL_CTL_ADD, socketfd, &epollEvent);
while (1)
{
epollwaitfd = epoll_wait(epollfd, epollEventArry, 5, -1);
cout << "epollwaitfd = " << epollwaitfd << endl;;
if (epollwaitfd < 0)
{
perror("epoll_wait error");
}
for (int i = 0; i < epollwaitfd; i++)
{
if (epollEventArry[i].data.fd == socketfd)
{
cout << "The network starts working and waits for the client to go online" << endl;
acceptfd = accept(socketfd, NULL, NULL);
cout << "acceptfd=" << acceptfd << endl;
epollEvent.data.fd = acceptfd;
epollEvent.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, acceptfd, &epollEvent);
}
else if (epollEventArry[i].events & EPOLLIN)//判断第i个文件描述符是否就绪
{//只需要将输出数组中的元素的events数据成员与我们关心的事件就行&操作,
//结果大于0,则代表该事件已就绪
bzero(buf, sizeof(buf));
int res = read(epollEventArry[i].data.fd, buf, sizeof(buf));//使用事件队列来的fd,而不是acceptfd
if (res > 0)//客户端主动下线
{
cout << "server accept data-----buf=" << buf << endl;
cout << "msg=" << buf << endl;
}
else if (res <= 0)//异常下线
{
cout << "The client is abnormally disconnected" << endl;
close(epollEventArry[i].data.fd);
//从epoll中删除客户端描述符
////此前,不确定epollEvent是否有被改变,因此好习惯在此重置一下epollEvent
epollEvent.data.fd = epollEvent.data.fd;
epollEvent.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_DEL, epollEventArry[i].data.fd, &epollEvent);
}
}
}
}
}
return 0;
}
注意:要实现在线聊天,依然需要onlineMap容器存放ID和acceptfd,因为存放在epollEventArray[]中的acceptfd没有与ID有特殊联系,不能通过找到特定的ID反馈消息
面试问题:
总结网络通信框架思路
通信协议的协议的作用是什么
查找资料总结TCP网络通信三次握手和四次挥手
更多推荐



所有评论(0)