之前我写过一个利用socket写的web服务器,但是他是阻塞的就是经常会无响应,所以这次尝试用select与epoll的非阻塞模式来写一个非阻塞的web服务器
首先我们来了解一下select与epoll的区别
(1)、select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
(2)、epoll==>时间复杂度O(1)
epoll可以理解为event poll ,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd) 的,此时我们对这些流的操作都是有意义的。 (复杂度降低到了O(1))
select,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
这里我提供C++,java与python的实现以供参考。
C++的实现
C++
//C++ select实现
#include <iostream>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <unistd.h>
#include <vector>
#include <fcntl.h>
#define BUF_SIZE 4096
#define MAX_CLIENT 10
int start_socket(int port) {
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
int oldSocketFlag = fcntl(server_socket, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(server_socket, F_SETFL, newSocketFlag) == -1) {
close(server_socket);
std::cout << "非阻塞失败" << std::endl;
exit(0);
}
struct sockaddr_in server_addr;
int server_len = sizeof(server_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
std::cout << "bind:" << bind(server_socket, (struct sockaddr *) &server_addr, server_len) << std::endl;
std::cout << "listen:" << listen(server_socket, 5) << std::endl;
return server_socket;
}
int main() {
int server_socket = start_socket(8090); //构造socket
fd_set fds; //定义一个selecter fd
std::vector<int> client_socket; //声明客户端
char buffer[BUF_SIZE]; //定义缓冲区
FD_ZERO(&fds); //初始化select
while (true) {
FD_SET(server_socket, &fds); //将serversocket放入select队列里面去
int maxfd = server_socket;
for (int i: client_socket) { //找出最大的socket
maxfd = maxfd > i ? i : maxfd;
}
switch (select(maxfd + 1, &fds, NULL, NULL, NULL)) { //select监听
case -1:
std::cout << "select错误" << std::endl;
break;
case 0:
continue;
default: {
///判断是否为连接服务端的socket,接受连接并将其加载到select里
if (FD_ISSET(server_socket, &fds)) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn = accept(server_socket, (struct sockaddr *) &client_addr, &client_len);
FD_SET(conn, &fds);
client_socket.push_back(conn);
}
std::vector<int>::iterator conn = client_socket.begin();
int flag = false;
//当有socket的数据来临时,遍历数组判断是哪个客户端发的请求,读取请求头,创建应答头,并返回给客户端,并清除客户端与select的数据
for (; conn != client_socket.end(); conn++) {
if (!FD_ISSET(*conn, &fds)) continue;
read(*conn,buffer,BUF_SIZE);
std::cout << buffer << std::endl;
std::string data = "hello word";
std::string buff = "HTTP/1.1 200 \r\n";
buff += "Content-Type: text/html;charset=UTF-8 \r\n";
buff += "Content-Length: " + data.length();
buff += "\r\n\r\n";
buff += data;
write(*conn,buff.c_str(),buff.length());
close(*conn);
FD_CLR(*conn, &fds);
flag = true;
break;
}
if (flag)client_socket.erase(conn);
}
}
}
close(server_socket);
return 0;
}
C++
//C++ epoll的实现
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>
#include "stdio.h"
#include <fcntl.h>
#define BUF_SIZE 4096
#define EVENT_SIZE 10
int start_socket(int port) {
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
int oldSocketFlag = fcntl(server_socket, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(server_socket, F_SETFL, newSocketFlag) == -1) {
close(server_socket);
std::cout << "非阻塞失败" << std::endl;
exit(0);
}
int server_len = sizeof(server_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
std::cout << "bind:" << bind(server_socket, (struct sockaddr *) &server_addr, server_len) << std::endl;
std::cout << "listen:" << listen(server_socket, 5) << std::endl;
return server_socket;
}
int main() {
char buffer[BUF_SIZE];
int server_socket = start_socket(8090); //构造socket
struct epoll_event event, events[EVENT_SIZE]; //定义epoll信息
int epfd = epoll_create(10); //创建epoll数组
event.data.fd = server_socket;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_socket, &event); //将服务端的socket放入epoll里
while (true) {
int nfds = epoll_wait(epfd, events, EVENT_SIZE, 5); //等待连接
for (int i = 0; i < nfds; i++) { //循环去遍历
if (events[i].data.fd == server_socket) { //判断是否为连接服务端的socket,接受连接并将其加载到epoll里
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn = accept(server_socket, (struct sockaddr *) &client_addr, &client_len);
event.events = EPOLLIN;
event.data.fd = conn;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &event);
} else if (events[i].events & EPOLLIN) { //当有socket的数据来临时,读取请求头,创建应答头,并返回给客户端
int conn = events[i].data.fd;
read(conn, buffer, BUF_SIZE);
std::cout << buffer << std::endl;
std::string data = "hello word"; //构造数据
std::string buff = "HTTP/1.1 200 \r\n "; //构造头
buff += "Content-Type: text/html;charset=UTF-8 \r\n ";
buff += "Content-Length: " + data.length();
buff += "\r\n\r\n";
buff += data;
write(conn, buff.c_str(), buff.length());
epoll_ctl(epfd, EPOLL_CTL_DEL, conn, NULL);
close(conn);
}
}
}
}
java使用nio,当前系统为windows时实现方式为select,当前系统为linux时,实现方式为epoll
C++
package org.company;
import com.sun.javafx.binding.StringFormatter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class Main {
public static void main(String[] args) throws IOException {
//构造socket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//将服务器端socket注册到select里
while (true) {
if (selector.select(500) == 0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); //获取当前的SelectionKey
while (iterator.hasNext()) {//迭代器迭代
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) { //如果是连接的key,那么获取客户端的连接socket并标记为读,注册到select里
SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} else if (selectionKey.isReadable()) { //当有socket的数据来临时,读取请求头,创建应答头,并返回给客户端
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
socketChannel.read(byteBuffer);
String data = "hello word";
System.out.println(new String(byteBuffer.array()));
String message = String.format("HTTP/1.1 200\r\n" +
"Content-Length:%d\r\n" +
"Content-Type: text/html;charset=UTF-8\r\n\r\n" +
"%s", data.length(), data);
socketChannel.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
socketChannel.close();
}
//清除select里的信息
iterator.remove();
}
}
}
}
python
#python epoll实现 只可在linux环境下运行
import socket
import select
if __name__ == '__main__':
# 构造socket
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socket_server.bind(('127.0.0.1', 8090))
socket_server.listen(5)
socket_server.setblocking(False)
epoll = select.epoll()
epoll.register(socket_server.fileno(), select.EPOLLIN) #将服务端的socket放入epoll里
connections = {}
while True:
for fileno, event in epoll.poll(1):
if fileno is socket_server.fileno(): #判断是否为连接服务端的socket,接受连接并将其加载到epoll里
conn, addr = socket_server.accept()
conn.setblocking(False)
epoll.register(conn.fileno(), select.EPOLLIN)
connections[conn.fileno()] = conn
elif event & select.EPOLLIN: #当有socket的数据来临时,读取请求头,创建应答头,并返回给客户端
data = connections[fileno].recv(4096)
print(data)
data = 'hello word' # 返回的参数
buff = 'HTTP/1.1 200 \r\n'
'Content-Type: text/html;charset=UTF-8 \r\n'
'Content-Length: {}\r\n\r\n'
'{}'.format(len(data), data) # 构造请求头
connections[fileno].send(buff.encode('UTF-8'))
connections[fileno].close()
del connections[fileno]
epoll.unregister(fileno)
socket_server.close()
到这里有关select与epoll的相关知识介绍完毕,有问题欢迎留言与我探讨
参考文章列表:
面试官:select、poll、epoll有何区别?我:阿巴阿巴...
Python-select详解(select、epoll)
unix网络编程