socket
本来要写关于socket的内容。后来想了想,socket是将传输层和IP层的协议(TCP/IP协议簇)进行封装,供上层应用程序更加方便的使用传输层和IP层提供的服务。
想要真正了解socket的原理,还得是先掌握TCP/IP四层模型。TCP/IP四层模型和socket的关系大致如下图,在传输层之上对传输层协议进行封装供应用层调用。
图片来自知乎https://zhuanlan.zhihu.com/p/497267988
socket使用流程
socket服务端,socket() -> bind() -> listen() -> accept() -> recv() -> send()
socket客户端,socket() -> connect() -> send() -> recv() -> close()
想要了解socket的底层,操作系统是如何收发数据的可以看这篇文章https://zhuanlan.zhihu.com/p/373060740
IO模型
当进程在读写数据(磁盘设备中的、网络中的)时,将产生系统调用,由用户态切换到内核态,需要内核将数据拷贝到内核的缓冲区,再将数据从内核缓冲区拷贝到进程的缓冲区,最后交给进程。
所以在进程read数据时,需要经过内核“获取”数据、将数据拷贝到进程空间的过程。所以产生了以下五种IO模型
阻塞I/O
也称为同步阻塞I/O模型
进程发起系统调用后阻塞,直到数据准备就绪,内核返回调用结果,结束进程阻塞,进程到内核缓冲区读取数据。
非阻塞I/O
也称为同步非阻塞I/O模型
进程发起系统调用后,如果数据没有准备好,内核返回error,进程获得error而不阻塞进程,系统再次发起系统调用,直至可以从内核缓冲区读取数据。
I/O多路复用
I/O多路复用是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免非阻塞I/O模型中进程轮询等待的问题。
坏处是,在中间添加了select过程。
好处是,可以通过select实现一个进程(线程)监听多个I/O事件。
信号驱动的I/O
进程发起系统调用后,内核立即返回,进程可以执行其他任务,当数据准备完成后,内核为进程产生一个信号,进程就可以到内核缓冲区读取数据。
异步I/O
在其他四种I/O模型中,当内核将数据准备好后需要进程去读取数据,读取数据的过程是阻塞的。
而异步I/O模型,在进程发起系统调用后,内核立即返回,进程可以执行其他任务,内核读取数据,并将数据拷贝给进程,然后产生信号,进程就可以直接使用数据,而不用阻塞读取读取数据。
select poll epoll
在Linux下I/O多路复用使用select poll epoll来实现。
select poll epoll本质上还是同步I/O,当数据准备就绪后,都需要自己去内核空间读取数(将数据从内核缓冲区拷贝到进程缓冲区)。
select
先来上硬菜,Linux sys/select.h头文件中select函数原型
int select (int __nfds, fd_set *__restrict __readfds, // fd_set 使用bitmap实现
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
__nfds,表示监听的文件描述符中最大的文件描述符+1
__readfds,表示有可读数据到达的文件描述符集合
__writefds,表示有可写数据到达的文件描述符集合
__exceptfds,表示发生异常的文件描述符集合
__timeout,表示超时时间
存在的问题
(1) 每次调用select都需要把监听的文件描述符集合从用户空间拷贝到内核空间。
(2) select监听的文件描述符有上限。默认在32位机上为1024个,在64位机上是2048个。
(3) select只能监听文件描述符上是否有事件发生,也就是说当监听的文件描述符集合上有事件发生时,需要程序遍历文件描述符集合。
poll
poll在select的基础上针对select的问题做了优化
Linux sys/poll.h头文件中poll函数原型
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
__fds,文件描述符数组首地址
__nfds,文件描述符数量
__timeout,超时时间
poll将文件描述符的类型由bitmap
换成了pollfd*
(数组),解决了监听文件描述符上限的问题。
但是,还是需要在每次监听时,将所有文件描述符从用户空间拷贝到内核空间。
epoll
epoll主要有以下三个函数
创建epoll
int epoll_create (int __size) __THROW
__size,表示初始化epoll的大小,并不限制epoll的大小。在linux 2.6.8以后该参数就可以被忽略了,只要填写大于0的数就可以
epoll_ctl函数,事件注册
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
int epoll_ctl (int __epfd, int __op, int __fd,
struct epoll_event *__event) __THROW;
__epfd,epoll文件描述符
__op,表示操作,EPOLL_CTL_ADD,添加新的文件描述符;EPOLL_CTL_MOD,修改文件描述符;EPOLL_CTL_DEL,删除文件描述符
__fd,操作的文件描述符
__event,文件描述符的事件
epoll_wait函数,等待事件产生
int epoll_wait (int __epfd, struct epoll_event *__events,
int __maxevents, int __timeout);
__epfd,epoll文件描述符
__events,有事件发生的文件描述符数组
__maxevents,文件描述符的数量
__timeout,超时时间
(1) epoll使用红黑树
存储文件描述符
(2) epoll只有在第一次时才将文件描述符从用户空间拷贝到内核空间
(3) epoll_wait时,只返回有事件发生的文件描述符
epoll实例
附上一份epoll实例代码
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <string.h>
#include <sys/epoll.h>
// #include <poll.h>
#include <errno.h>
using namespace std;
int main(int argc, char *argv[]) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) { return -1; }
int old_flag = fcntl(listen_fd, F_GETFL, 0);
int new_flag = old_flag | O_NONBLOCK;
if (fcntl(listen_fd, F_SETFL, new_flag) == -1) {
close(listen_fd);
std::cout << "fcntl fail" << std::endl;
return -1;
}
struct sockaddr_in listen_addr;
bzero(&listen_addr, sizeof(listen_addr));
listen_addr.sin_family = AF_INET;
listen_addr.sin_port = htons(8080);
listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&listen_addr, sizeof(listen_addr)) == -1) {
std::cout << "bind fail" << std::endl;
close(listen_fd);
return -1;
}
if (listen(listen_fd, SOMAXCONN) == -1) {
std::cout << "listen fail" << std::endl;
close(listen_fd);
return -1;
}
int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on));
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, (void *)&on, sizeof(on));
int epoll_fd = epoll_create(1);
if (epoll_fd == -1) {
close(listen_fd);
std::cout << "epoll_create fail" std::endl;
return -1;
}
struct epoll_event listen_fd_event;
listen_fd_event.events = EPOLLIN;
listen_fd_event.data.fd = listen_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_fd_event) == -1) {
close(listen_fd);
std::cout << "epoll_ctl fail" std::endl;
return -1;
}
while (1) {
struct epoll_event epoll_fd_event[1024];
int epoll_fd_event_len = epoll_wait(epoll_fd, epoll_fd_event, 1024, 1000);
if (epoll_fd_event_len < 0) {
// close(listen_fd);
if (errno == EINTR) {
continue;
} else {
std::cout << "epoll_wait fail" << std::endl;
break;
}
// return -1;
} else {
for (size_t i = 0; i < epoll_fd_event_len; ++i) {
if (epoll_fd_event[i].events & EPOLLIN) {
if (epoll_fd_event[i].data.fd == listen_fd) {
// 新连接
struct sockaddr client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, &client_addr, &client_addr_len);
if (client_addr_len != -1) {
int old_flag = fcntl(client_fd, F_GETFL, 0);
int new_flag = old_flag | O_NONBLOCK;
if (fcntl(client_fd, F_SETFL, new_flag) == -1) {
close(client_fd);
std::cout << "fcntl fail" std::endl;
} else {
struct epoll_event client_fd_event;
client_fd_event.data.fd = client_fd;
client_fd_event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_fd_event) == -1) {
close(client_fd);
std::cout << "epoll_ctl fail" std::endl;
} else {
std::cout << "new client" << client_fd << std::endl;
}
}
}
} else {
// 读数据
char buf[64] = { 0 };
int len = recv(epoll_fd_event[i].data.fd, buf, 64, 0);
if (len > 0) {
std::cout << epoll_fd_event[i].data.fd << ": " << buf << std::endl;
} else if (len == 0) {
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_fd_event[i].data.fd, 0) != -1) {
std::cout << "client disconnect: " << epoll_fd_event[i].data.fd << std::endl;
}
close(epoll_fd_event[i].data.fd);
} else {
if (errno != EWOULDBLOCK && errno != EINTR) {
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_fd_event[i].data.fd, 0) != -1) {
std::cout << "client disconnect: " << epoll_fd_event[i].data.fd << std::endl;
}
close(epoll_fd_event[i].data.fd);
}
}
}
}
}
}
}
// 清空epoll_fd
close(listen_fd);
return 0;
}