基于socket实现一个简易的web服务器——非阻塞的模式

之前我写过一个利用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++ 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++ 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

package com.company;

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);
                    System.out.println(new String(byteBuffer.array()));
                    StringBuffer sb = new StringBuffer();
                    String data = "hello word";
                    sb.append("HTTP/1.1 200\r\n");
                    sb.append("Content-Length:"+data.length());
                    sb.append("Content-Type: text/html;charset=UTF-8\r\n\r\n");
                    sb.append(data);
                    socketChannel.write(ByteBuffer.wrap(sb.toString().getBytes(StandardCharsets.UTF_8)));
                    socketChannel.close();
                }
                //清除select里的信息
                iterator.remove();
            }
        }
    }
}

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 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网络编程

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值