字节序
字节序测试
#include<iostream>
union
{
short value;
char b[sizeof(value)];
}test;
int main()
{
/*
字节序:计算机内部存储字节的顺序,
如“你好”,在计算机中可以存储为“你好”,也可以存储为“好你”
因此计算机不同的字节序存储方法对编码解释至关重要
大端字节序:数据高位(左端)字节存储在内存高位地址(右端)
如字节0x 01 02,按照大端字节序存储应该为0x 02 01
小端字节序:数据高位字节存储在内存低位地址
如字节0x 01 02,按照大端字节序存储应该为0x 01 02
*/
test.value = 0x0102;
if (test.b[0] == 1 && test.b[1] == 2)
printf("小端字节序!\n");
else
printf("大端字节序!\n");
return 0;
}
注:网络字节序采用大端模式
字节序转换函数
/*
- h host主机
- n network 网络
- s short int(2B)
- l long int(4B)
*/
#include <arpa/inet.h>
//转换端口
uint16_t ntohs(uint16_t netshort);// 网络字节序 - 主机字节序
uint16_t htons(uint16_t hostshort);// 主机字节序 - 网络字节序
//转换IP
uint32_t ntohl(uint32_t netlong);// 网络字节序 - 主机字节序
uint32_t htonl(uint32_t hostlong);// 主机字节序 - 网络字节序
socket编程
Socket原理详解 - 慢慢毛毛慢慢 - 博客园 (cnblogs.com)
TCP通信流程
特点 | TCP | UDP |
---|---|---|
创建连接 | 面向连接 | 无连接 |
可靠性 | 可靠 | 不可靠 |
连接对象个数 | 一对一 | 一对一、一对多、多对多、多对一 |
传输方式 | 面向字节流 | 面向用户数据报 |
首部开销 | 20字节 | 8字节 |
适用场景 | 文件传输等 | 视频会议、直播等 |
socket函数
// 服务端通信流程
//1.创建socket
int socket(int domain, int type, int protocol)
参数:
- domain 协议族:
AF_INET:IPV4
AF_INET6:IPV6
AF_UNIX,AF_LOCAL:本地套接字通信(进程间通信)
- type 协议类型:
SOCKET_STREAM:流式协议
SOCKET_DGRAM:报式协议
- protocol 具体协议:
一般设置为0,此时有
当协议类型为SOCKET_STREAM时,默认表示TCP协议
当协议类型为SOCKET_DGRAM时,默认表示UDP协议
返回值:
- 成功返回文件描述符
- 失败返回-1
//2.将服务端的ip与端口号绑定到socket上
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
- sockfd:服务端socket文件描述符
- addr:一个结构体指针,该结构体内封装了服务端的IP与端口号
- AF_INET:由于历史原因,我们一般用sockaddr_in(该结构体填入ip与端口号较为方便)填入IP与端口号,之后给bind函数传入参数时再 将其转换为addr类型
struct sockaddr_in {
sa_family_t sin_family; /* address family(协议): AF_INET */
in_port_t sin_port; /* port in network byte order(端口号) */
struct in_addr sin_addr; /* internet address(IP) */
};
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
- addrlen:第2个参数所占内存大小
//3.监听客户端
int listen(int sockfd, int backlog);
参数:
- sockfd:服务端文件描述符
- backlog:未链接和已连接之和的最大数
//4.等待接收客户端发送信息
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd:用于监听的文件描述符
- addr:传出参数,表示客户端相关信息(IP、端口号)
- addrlen:第2个参数大小
返回值:
- 成功:用于通信的文件描述符
- 失败:-1
//5.与客户端进行通信
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
- sockfd:通信文件描述符
- addr:服务端地址信息
- addrlen:第2个参数大小
//6.输出客户端信息
服务端开发
server.cpp
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
using namespace std;
int main()
{
//创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(-1);
}
//bind绑定
struct sockaddr_in ser_add;//封装服务端地址信息
ser_add.sin_family = AF_INET;//协议-ipv4
ser_add.sin_port = htons(8888);//指定端口号
//inet_pton(AF_INET, "192.168.214.128",&ser_add.sin_addr.s_addr);//指定一个ip,并将其转换未字节流地址
ser_add.sin_addr.s_addr = htonl(INADDR_ANY);//使用本地任意IP
int ret = bind(lfd,(struct sockaddr*)&ser_add, sizeof(ser_add));
if (ret == -1)
{
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd, 8);
if (ret == -1)
{
perror("listen");
exit(-1);
}
//接收客户端信息
struct sockaddr_in clientadd;//用于接收客户端地址信息
socklen_t addlen=sizeof(clientadd);//接收客户端地址信息长度
int cfd = accept(lfd,(struct sockaddr*)&clientadd,&addlen);
if (cfd == -1)
{
perror("accept");
exit(-1);
}
//输出客户端信息
char clientIP[16];
unsigned short clientPort;
//提取IP信息
inet_ntop(AF_INET, &clientadd.sin_addr.s_addr, clientIP, sizeof(clientIP));
//提取端口信息
clientPort = ntohs(clientadd.sin_port);
cout << "客户端ip:" << clientIP << endl;
cout << "客户端端口号:" << clientPort << endl;
//输出客户端数据
char buf[1024];
while (1)
{
memset(buf, 0, sizeof(buf));
int n = read(cfd, &buf, sizeof(buf));
if (n < 0)
{
perror("read");
exit(-1);
}
if (n > 0)
{
cout << "客户端信息:" << buf << endl;
}
else
{
cout << "客户端已断开连接!" << endl;
}
//向客户端发送数据
char* data = { "你好,我是服务端,已收到您的信息!" };
write(cfd, data, strlen(data));
}
//关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
linux编译
g++ server.cpp -o server
服务端启动
./server
客户端开发
#include<iostream>
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
using namespace std;
int main()
{
//创建套接字
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
{
perror("socket");
exit(-1);
}
//连接服务端
//封装地址信息
struct sockaddr_in client_add;
client_add.sin_family = AF_INET;
client_add.sin_port = htons(8888);
inet_pton(cfd, "192.168.214.128", &client_add.sin_addr.s_addr);
int ret = connect(cfd, (struct sockaddr*)&client_add, sizeof(client_add));
if (ret == -1)
{
perror("connect");
exit(-1);
}
int n = 0;
char buf[1024] = { 0 };
while (1)
{
//重定向,读取标准输入数
n = read(STDIN_FILENO, &buf, sizeof(buf));
if (n > 0)
write(cfd, &buf, n);//发送数据
//读取客户端信息
memset(&buf, 0, sizeof(buf));
n = read(cfd, &buf, sizeof(buf));
if (n < 0)
{
perror("read");
exit(-1);
}
if (n > 0)
cout << "服务端信息:" << buf << endl;
else
cout << "服务端已断开!" << endl;
}
close(cfd);
return -1;
}
三次握手与四次挥手
三次握手
字段解释
- SYN:同步位,SYN=1表示连接请求报文或者连接接收报文
- SYN=1,ACK=0时表示连接请求报文
- SYN=1,ACK=1时表示连接接收报文
- ACK:ACK=1时确认号ack才起作用
- seq:报文序号,如客户端发送序号为500,长度为200的报文段,则seq=500
- ack:确认号字段,指接收到对方发送的数据后,期望下一次接收到的序号,如上,ack=701
三次握手连接建立过程
- 客户端置SYN=1,表示请求建立连接,不携带数据,但消耗掉一个序号,置客户端seq=x
- 服务端监听过程中接收到连接请求,置SYN=1,表示接受连接请求,置ACK=1,表示要回答该报文,置服务端seq=y,置确认号ack=x+1
- 客户端收到服务端的确认,发送对服务端的确认,置ACK=1,seq=x+1,ack=y+1,双方连接建立成功
第三次握手作用
为了防止已经失效的连接请求报文突然又传送回了TCP服务器,造成服务器的资源浪费
为了解决网络中存在延迟的重复分组的问题
考虑一种异常情况:
客户端第一次握手发送连接请求报文在网络中延迟,于是客户端认为连接请求失败,关闭客户端
服务端接收到延迟的连接请求后认为有客户端进行连接请求,于是发送确认请求
如果此时不进行第三次握手机制,则客户端接受到错误的确认报文后将不予理睬,而服务端则会一直等待客户端发送数据,这样会消耗掉服务端大量资源
上述异常情况举例:
A:我们去玩吧
B:好的
如果通信过程中A的信息“我们去玩吧”延迟了,那么A误以为B不想搭理A,于是不想和B去玩了
但B收到了A的延迟请求后又向A发送了确认数据“好的”
当A收到了B的确认后,由于此时A已经不想和B玩耍,如果此时A不向B发送数据表述情况,则B一直等待A的玩耍时间和玩耍地点等信息,于是B此时就把A认定为渣男,关系崩解
另外一种异常情况:
客户端第一次发送的连接请求在网络中延迟,由于超时重传机制使得客户端重新发送了一个连接请求
客户端第二次的连接请求发送成功并且服务端接收连接,此时服务端发送连接请求确认报文,如果此时只进行两次握手,则此时客户端认为连接已经成功建立,因此双方开始通信
当双方通信结束后,客户端进入关闭状态
然而此时客户端第一次延迟的连接请求报文突然到达了服务器,而服务器又无法区别这是一个延迟且重复的请求,因此向客户端发送确认,但客户端已经进入关闭状态将无法接收确认段,则此时服务器将消耗大量资源进行测试。
如下图所示
四次挥手
- 三次握手是单向连接,即客户端单方面要求与服务端建立连接,然而四次挥手时通信双方均可优先提出断开连接
- 假如上图A端欲断开连接,则A置FIN=1,seq=u(等于前面已经传送过的数据序号+1),则A进入FIN-WAIT-1(终止等待1)状态,等待B的确认
- B收到A的断开信号后,向A发送断开确认,置ACK=1,seq=v,sck=u+1,此时B就进入CLOSE-WAIT(关闭等待)状态,且此时A到B的连接方向断开,A进入半关闭状态,A不能再向B发送数据,但仍可接收到B发送来的数据(因为A在发起断开连接时B可能仍旧有数据还未传输完毕)
- 若B已经没有要发送给A的数据,则B向A发送断开连接请求,置FIN=1,ACK=1,seq=w(不等于v+1是因为在半关闭状态下B可能又发送数据了),ack=u+1,这时B就进入LAST-ACK(最后确认)状态,等待A的确认。
- 当A收到B的断开请求后,向B发送确认,置ACK=1,seq=u+1,ack=w+1,此后A进入TIME-WAIT(时间等待)状态,此后A必须等待2MSL时间后,才进入CLOSE状态。时间MSL叫做最长报文段寿命
- 之所以A并未立即断开连接:
- 是为了保证A发送给B的确认迟到或丢失时能够进行超时重传
- A在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
高并发服务器
Linux C编程之十八 高并发服务器 - pointerC++ - 博客园 (cnblogs.com)
多进程并发服务器
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
using namespace std;
/*
思路:让父进程只进行监听,子进程进行收发数据
*/
int main()
{
//创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(-1);
}
//bind绑定
struct sockaddr_in ser_add;//封装服务端地址信息
ser_add.sin_family = AF_INET;//协议-ipv4
ser_add.sin_port = htons(9999);//指定端口号
//inet_pton(AF_INET, "192.168.214.128",&ser_add.sin_addr.s_addr);//指定一个ip,并将其转换未字节流地址
ser_add.sin_addr.s_addr = htonl(INADDR_ANY);//使用本地任意IP
int ret = bind(lfd, (struct sockaddr*)&ser_add, sizeof(ser_add));
if (ret == -1)
{
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd, 8);
if (ret == -1)
{
perror("listen");
exit(-1);
}
while (1)
{
//接收客户端信息
struct sockaddr_in clientadd;//用于接收客户端地址信息
socklen_t addlen = sizeof(clientadd);//接收客户端地址信息长度
int cfd = accept(lfd, (struct sockaddr*)&clientadd, &addlen);
if (cfd == -1)
{
perror("accept");
exit(-1);
}
//当监听到客户端信号时,就创建一个子进程与其进行通信
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
exit(-1);
}
if (pid > 0)//父进程关闭通信描述符
close(cfd);
if (pid == 0)
{
close(lfd);//子进程关闭监听描述符
//输出客户端信息
char clientIP[16];
unsigned short clientPort;
//提取IP信息
inet_ntop(AF_INET, &clientadd.sin_addr.s_addr, clientIP, sizeof(clientIP));
//提取端口信息
clientPort = ntohs(clientadd.sin_port);
cout << "客户端ip:" << clientIP << endl;
cout << "客户端端口号:" << clientPort << endl;
//与客户端进行通信
char buf[1024];
while (1)
{
memset(buf, 0, sizeof(buf));
int n = read(cfd, &buf, sizeof(buf));//读取客户端数据
if (n < 0)
{
perror("read");
exit(-1);
}
if (n > 0)
{
cout << "客户端信息:" << buf << endl;
}
else
{
cout << "客户端已断开连接!" << endl;
break;
}
//向客户端发送数据
char* data = (char*)"你好,我是服务端,已收到您的信息!" ;
write(cfd, data, strlen(data));
}
close(cfd);//关闭通信描述符
exit(0);
}
}
close(lfd);//关闭监听描述符
return 0;
}
多线程并发服务器
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<pthread.h>
using namespace std;
//封装客户端传过来的地址信息,并将其作为参数传给子线程
typedef struct cliinfo
{
struct sockaddr_in addr;//客户端地址信息
pthread_t tid;//子线程号
int cfd;//通信描述符
}cliinfo;
cliinfo cli_infos[128];//存放每个客户端信息
//线程执行函数
void* working(void* arg)
{
struct cliinfo* c_info = (struct cliinfo*)arg;
int cfd = c_info->cfd;
pthread_t tid = c_info->tid;
char cip[16];
inet_ntop(AF_INET, &c_info->addr.sin_addr, cip, sizeof(cip));
unsigned short port = ntohs(c_info->addr.sin_port);
cout << "客户端" << cip << "已连接!" << endl;
char buf[1024];
while (1)
{
memset(buf, 0, sizeof(buf));
int n = read(cfd, &buf, sizeof(buf));
if (n < 0)
{
perror("read");
exit(0);
}
if (n > 0)
{
cout << "端口" << port << "信息:" << buf << endl;
}
else
{
cout << "客户端已断开连接!" << endl;
break;
}
char* data = (char*)"我是服务端,我已收到您的信息!";
write(cfd, data, strlen(data));
}
close(cfd);
return NULL;
}
int main()
{
//创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
//将服务端ip与端口号绑定到套接字上
struct sockaddr_in seraddr;//封装服务端地址信息
seraddr.sin_family = AF_INET;//协议
seraddr.sin_port = htons(8787);//端口号
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);//使用本地任意IP
socklen_t seraddr_len = sizeof(seraddr);
int ret = bind(lfd, (struct sockaddr*)&seraddr, seraddr_len);
if (ret == -1)
{
perror("bind");
exit(0);
}
//监听客户端信号
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
//sockinfos初始化
int max = sizeof(cli_infos) / sizeof(cli_infos[0]);
for (int i = 0; i < max; i++)
{
bzero(&cli_infos[i], sizeof(cli_infos[i]));
cli_infos[i].cfd = -1;
cli_infos[i].tid = -1;
}
while (1)
{
struct sockaddr_in cliaddr;//封装客户端信息
socklen_t cliaddr_len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
struct cliinfo* pinfo;
//寻找可用的cliinfo元素
for (int i = 0; i < max; i++)
{
if (cli_infos[i].cfd == -1)//说明该通信描述符还未使用
{
pinfo = &cli_infos[i];
break;
}
if (i == max)//说明服务端已经达到最大连接数,则让服务端休眠一秒钟等待其他客户端释放连接
{
sleep(1);
i--;
}
}
//将得到的客户端地址信息封装给结构体
pinfo->cfd = cfd;
memcpy(&pinfo->addr, &cliaddr, cliaddr_len);
//每当接收到一个客户端连接就创建一个子线程与其进行通信
//pthread_t tid;
ret = pthread_create(&pinfo->tid, NULL,working, pinfo);
if (ret != 0)
{
perror("pthread_create");
exit(0);
}
pthread_detach(pinfo->tid);//设置该子线程分离,进行自动回收
}
close(lfd);
return 1;
}
由于线程创建时与主线程共享内存空间,因此子线程不能关闭lfd(监听描述符),同样主线程也不能关闭cfd(通信描述符)
多路IO转接(复用)服务器
概述
该类服务器是指不再由应用程序自己监视客户端进行连接,而是由内核应用程序替代文件
- 构造一张有关文件描述符的列表,将要监听的文件描述符添加其中
- 调用一个函数,监听上表中的文件描述符,直到有一个进行IO操作时,该函数才返回
- 返回时告诉进程有哪些描述符进行IO操作
select
API介绍
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
- nfds:委托内核检测的最大文件描述符的值
- readfds:集合中为读操作的文件描述符,传出参数
- writefds:集合中为写操作的文件描述符,传出参数
- exceptfds:检测发生异常的文件描述符集合
- timeout:设置超时时间
timeout=NULL表示永久阻塞
返回值:
- -1:失败
- n(>0):有几个集合发生了变化
- 等于0:表示超时
//将参数文件描述符对应位设置为0
void FD_CLR(int fd, fd_set *set);
//判读对应的fd标志位是0还是1,如果是0就返回0,是1就返回1
int FD_ISSET(int fd, fd_set *set);
//将参数文件描述符fd对应标志位设置为1
void FD_SET(int fd, fd_set *set);
//将fd_sets所有标志位全部设置为0
void FD_ZERO(fd_set *set);
工作原理
select需要一个fds集合辅助完成并发服务器创建
fds集合是系统维护的一个数组,其数组下标表示文件描述符,其数值元素为0或者1
初始时,我们指fds中哪些文件描述符需要进行检测,将其值置为1,使用select函数将该数组传入内核
在内核中,内核将负责遍历fds数组集合,不断轮询检测哪些文件描述符发生了变化,如果发生变化说明该文件描述符对应的缓冲区传入了数据
当检测到fds中有一个文件描述符发生变化时,内核再将该数组集合传回用户态,用户再遍历该集合检查哪些文件描述符发生了变化(即检测哪些数组元素标志位为1)
当检测到某文件描述符发生变化时,就进行通信,通信结束后再将该文件描述符标志位清空,传入内核继续进行检测信号
举例(如上图):
- 初始时,我们设定fds[3]=1、fds[4]=1、fds[100]=1、fds[101]=1,表示我们需要检测3号、4号、100号和101号文件描述符指向的缓冲区是否发送来了数据
- 在用户态设定结束后,我们通过select将fds传入内核,将上述检测任务交由内核完成
- 当内核遍历fds,检测到3号和4号发送来了数据,而100号、101号并未发送数据,于是令fds[3]=1、fds[4]=1、fds[100]=0、fds[101]=0,并将检测结果返回到用户态
- 用户态接收到检测结果后遍历fds,检测到fds[3]=1,fds[4]=1,于是读取3号和4号数据进行通信,通信结束后再将其标志位清除
- 如我们还需要检测100号和101号数据是否发送了数据,则再将fds传入内核检测,以此循环
注:在开发过程中,我们通常设定两个fds,一个用于表明我们希望检测到信号的文件描述符集合,另一个则用于传入内核表示实际检测到信号的文件描述符集合
缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,开销较大
- 每次调用select,也都需在内核中遍历传递进来的所有fd,其开销也较大
- select支持的文件描述符只有1024个
- fds集合不能重用,每次都需要重置
代码编写
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/select.h>
using namespace std;
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8787);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
if (ret == -1)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
//创建一个fd_set集合,存放需要检测的文件描述符
fd_set rdset,temp;
FD_ZERO(&rdset);//初始化
FD_SET(lfd, &rdset);//将监听描述符添加到检测集合中,当其传入内核中检测到发生变化时,说明有客户端建立连接
int maxfd = lfd;
while (1)
{
temp = rdset;
//创建select函数,将检测集合传入内核,检测哪些描述符发生了变化
ret = select(maxfd + 1, &temp, NULL, NULL, NULL);
if (ret == -1)
{
perror("select");
exit(0);
}
if (ret == 0)
{
continue;
}
else {//说明集合中有处于被检测的文件描述符发生了变化
//如果监听描述符发生了变化,说明有客户端建立连接
if (FD_ISSET(lfd, &temp))
{
struct sockaddr_in cli_addr;
socklen_t cliaddr_len = sizeof(cli_addr);
int cfd = accept(lfd, (struct sockaddr*)&cli_addr, &cliaddr_len);
if (cfd == -1)
{
perror("accept");
exit(0);
}
char cip[16] = { 0 };
//unsigned short port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cip, sizeof(cip));
cout << "客户端" << cip << "连接成功!" << endl;
//将新的客户端文件描述符添加到集合中继续检测
FD_SET(cfd, &rdset);
//更新最大文件描述符
maxfd=maxfd > cfd ? maxfd : cfd;
}
// 遍历文件描述符集合,检查其余是否有文件描述符发生了变化
for (int i = lfd + 1; i <= maxfd; i++)
{
if (FD_ISSET(i, &temp))//说明该文件描述符对应的缓冲区有数据(即对应的客户端发送来了数据)
{
//与该文件描述符对应的客户端进行通信
char buf[1024] = { 0 };
int n = read(i, &buf, sizeof(buf));
if (n < 0)
{
perror("read");
exit(0);
}
if (n == 0)
{
cout << "客户端已断开!" << endl;
close(i);
FD_CLR(i,&rdset);//清空该文件描述符在集合中的状态
}
else
{
cout << "客户端数据:"<<buf << endl;
//回射客户端数据
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 1;
}
poll
API介绍
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
- fds:要检测的文件描述符集合
- nfds:第一个参数数组中最后一个有效元素的下标+1
- timeout:阻塞时长
0:不阻塞
-1:阻塞,当检测到文件描述符有变化时,解除阻塞
>0:阻塞的时长
返回值:
- -1:失败
- >0:成功,表示检测集合中有n个文件描述符发生了变化
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符什么事件,如POLLIN时,检测文件描述符数据是否可读 */
short revents; /* 文件描述符实际发生的事件 */
};
工作原理
- 在select基础上,将用于检测信号的数据结构从数组fds转换成了结构体,该结构体内封装了我们希望检测的信息,如fd,检测事件(即我们希望检测读取数据的fd还是检测写入数据的fd发来了信号)
- 开发过程中,我们可以自定义检测数组大小,每一个数组元素都是上述一个结构体,同样利用poll函数将该数组传入内核进行检测
- 当内核检测到信号时,就将某一个数组元素内的结构体fd置为客户端的文件描述符,并传回用户态
- 用户态接收到检测信号后继续遍历数组,当检测到某一个元素发送了数据时就进行通信,通信结束后再次传入内核进行检测
与select比较:
- 检测范围不只是1024个文件描述符
- 不需要每次重置检测数据结构
代码编写
#include<iostream>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<poll.h>
using namespace std;
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(9999);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
if (ret == -1) {
perror("bind");
exit(0);
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
const int maxfd=1024;
struct pollfd fds[maxfd];//定义要检测的文件描述符数组
//将fds[0]的文件描述符设置为监听描述符
fds[0].fd = lfd;
fds[0].events = POLLIN;
//初始化其他要检测的文件描述符位置
for (int i = 1; i < maxfd; i++)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
int nfd = 0;
while (1)
{
//调用poll函数让内核检测哪些文件描述符有变化
ret = poll(fds, nfd + 1, -1);
if (ret == -1)
{
perror("poll");
exit(0);
}
if (ret == 0)
continue;
else//说明检测到文件描述符发生变化
{
if (fds[0].revents & POLLIN)//如果监听文件描述符号发生变化,说明有客户都拿请求连接
{
struct sockaddr_in cli_addr;
socklen_t cliaddr_len = sizeof(cli_addr);
int cfd = accept(lfd, (struct sockaddr*)&cli_addr, &cliaddr_len);
if (cfd == -1)
{
perror("accept");
exit(0);
}
char cliIP[16] = { 0 };
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cliIP, sizeof(cliIP));
cout << "客户端" << cliIP << "连接成功!" << endl;
//将新检测的通信描述符加入检测数组中
for (int i = 1; i < maxfd; i++)
{
if (fds[i].fd == -1)
{
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
//更新
nfd = nfd > cfd ? nfd : cfd;
}
//检查其余是否有文件描述符发生变化
for (int i = 1; i <= nfd; i++)
{
if (fds[i].revents & POLLIN)
{//表示该文件描述符指向的客户端发送了数据
char buf[1024] = { 0 };
int n = read(fds[i].fd, buf, sizeof(buf));
if (n == -1)
{
perror("read");
exit(0);
}
if (n > 0)
{
cout << "客户端信息:"<<buf << endl;
//服务端反射回客户端
write(fds[i].fd, buf, strlen(buf)+1);
}
else {
cout << "客户端已断开连接!" << endl;
close(fds[i].fd);
fds[i].fd = -1;
}
}
}
}
}
close(lfd);
return 1;
}
epoll
API介绍
int epoll_create(int size);
作用:创建一个新的epoll实例,在内核中创建一个数据区,
其中有两个比较重要的数据,一个是需要检测的文件描述符信息集合(红黑树),另外一个是实际检测出来的文件描述符信息集合(双链表)
参数:无意义,但必须填入,且大于0
返回值:返回一个文件描述符,指向该函数在内核中创建的数据区
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
作用:对创建出来的内核数据区进行操作,包括添加信息、删除信息、修改信息
参数:
- epfd:由epoll_create函数返回的文件描述符,指向其创建出的内核数据区
- op:对该数据区要执行的操作,其操作参数由一些宏指定
EPOLL_CTL_ADD:添加数据区信息
EPOLL_CTL_DEL:删除数据区信息
EPOLL_CTL_MOD:修改数据区信息
- fd:具体操作该数据区内的某一个文件描述符
- event:对fd操作的事件,该事件信息由结构体struct epoll_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操作事件,即检测某种行为(如读取数据)的文件描述符*/
epoll_data_t data; /* User data variable */
};
//常见的Epoll操作事件:
EPOLLIN:检测属于读取数据的fd
EPOLLOUT:检测属于写入数据的fd
EPOLLERR:检测fd的错误信息
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);//检测函数
参数:
- epfd:内核数据区的文件描述符
- epoll_event:传出参数,传出检测到信号的fd集合
- maxevents:第二个参数结构体数据大小
- timeout:阻塞时间
- 0:不阻塞
- -1:阻塞,直到检测到变化解除
- >0:阻塞时间(毫秒)
返回值:
- 成功:返回发生变化的文件描述符个数
- 失败:-1
工作原理
代码编写
#include<iostream>
#include<unistd.h>
#include<sys/epoll.h>
#include<string.h>
#include<arpa/inet.h>
using namespace std;
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if (ret == -1)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
//调用epoll在内核中创建一个监听信号的实例
int epfd = epoll_create(1);
//管理监听实例,将监听描述符添加其中
struct epoll_event epev;//封装需要监听的文件描述符信息,其数据结构为红黑树
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
const int maxfds = 1024;
struct epoll_event epevs[maxfds];//该参数用于返回发生变化的文件描述符集合,数据结构为双链表
while (1)
{
ret = epoll_wait(epfd, epevs, maxfds, -1);//开始检测信号,ret返回检测到信号的个数
if (ret == -1)
{
perror("epoll_wait");
exit(0);
}
cout << "检测到" << ret <<"个客户端发来了数据!"<< endl;
//取出发生数据的客户端进行通信
for (int i = 0; i < ret; i++)
{
int curfd = epevs[i].data.fd;
struct sockaddr_in cliaddr;
char cliIP[16] = { 0 };
unsigned short port;
if (curfd == lfd)//如果是监听信号表明有数据到达,有客户端需要连接
{
socklen_t cliaddr_len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
if (cfd == -1)
{
perror("accept");
exit(0);
}
port = htons(cliaddr.sin_port);
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
cout << "客户端" << cliIP << "已连接" << endl;
//将新需要检测的文件描述符添加到需要检测的集合中
epev.data.fd = cfd;
epev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}
else {//表明有数据到达,需要通信
char buf[1024] = { 0 };
int n = read(curfd, buf, sizeof(buf));
if (n < 0)
{
perror("read");
exit(0);
}
if (n == 0)
{
cout << "端口"<<port<<"已断开!" << endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else
{
cout << "端口" << port << "数据:" << buf << endl;
write(curfd, buf, strlen(buf) + 1);//回射客户端数据
}
}
}
}
close(lfd);
close(epfd);
return 1;
}
epoll两种工作模式
LT模式(水平触发)
指当对检测到的文件描述符进行读取数据时,如果无法一次性读取完缓冲区的数据,则在下次循环时epoll_wait会再次通知用户该fd尚有数据。
即只要缓冲区中有数据,epoll_wait就会检测到并通知用户
ET模式(边沿触发)
指当一次性无法读取完缓冲区所有数据时,epoll_wait也将不再提醒,因此在该模式下,要将读取设置为非阻塞进行循环读取,直到读取完毕