服务端使用C++实现非阻塞的websocket

客户端有socket,但网页端有类似socket的websocekt,那么webscoekt到底是如何实现的,今天我们来研究一下。

先抓个包看看websocket通信都发生了啥。

tcp的握手过程暂时不管,先看websocket的握手过程

浏览器的get请求

服务器的回复

websocket握手过程就一个http请求,请求头多带了俩个参数

Upgrade: websocket

Connection: Upgrade

这个时候浏览器要告诉服务器,要升级到websocket服务,并且会带一个Sec-WebSocket-Key值,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,这时就要服务器去验证并且加密再通过Sec-WebSocket-Accept 应答头返回给浏览器

思路明确了,我们写代码,先写一个socket接收到httpt请求,然后取出来Sec-WebSocket-Key,将其进行加密再返回到浏览器端

C++ 复制代码
//利用epoll创建一个非阻塞的socket
bool Socket::start_socket(int port) {
    _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;
    epoll::epoll_add(this->epfd, this->_server_socket, EPOLLIN);
    Accept();
    return true;
}
C++ 复制代码
//等待连接
int Socket::Accept() {
    while (true) {
        int nfds = epoll_wait(this->epfd, this->events, EVENT_SIZE, 5);
        if (nfds < 0) {
            if (errno == EINTR) {
                continue;
            } else {
                break;
            }
        }
        for (int i = 0; i < nfds; i++) {
            if (this->events[i].data.fd == this->_server_socket) {
                handshake(events[i]);//当第一次请求时进行握手
            } else {
                switch (events[i].events) {//如果不是第一次连接就对消息进行处理
                    case EPOLLIN:
                        std::string socket_message = Socket::Read(events[i].data.fd);
                        std::string message;
                        int ret = utils::code::decode_message(socket_message.c_str(), message);
                        for (const auto &item: poccess) {
                            if (item(message, ret, events[i].data.fd)) break;
                        }
                        break;
                }
            }
        }
    }
    return 0;
}
C++ 复制代码
//握手的操作
bool Socket::handshake(epoll_event event) {
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int conn = accept(this->_server_socket, (struct sockaddr *) &client_addr, &client_len);
    char buffer[BUF_SIZE];
    memset(buffer, 0, sizeof(buffer));
    read(conn, buffer, BUF_SIZE);//取出来get请求体
    std::map<std::string, std::string> map;
    utils::code::decode_accept(buffer, &map);//提取请求头
    std::string sec_websocket_accept;
    utils::code::encode_accept(&map.find("Sec-WebSocket-Key")->second, &sec_websocket_accept);//对Sec-WebSocket-Key进行加密
    std::string buff =
            "HTTP/1.1 101 Switching Protocols\r\n"
            "Upgrade: websocket\r\n"
            "Connection: Upgrade\r\n"
            "Sec-WebSocket-Accept: " + sec_websocket_accept + "\r\n\r\n";
    write(conn, buff.c_str(), buff.length());//将应答头返回给浏览器
    after_handshake(conn);
    epoll::epoll_add(this->epfd, conn, EPOLLIN);
    return true;
}

接下来我们看看websocket消息的数据帧,研究他是如何编码的

抓个包看看数据

websocket消息体的结构

编写消息编码和解码的代码:

C++ 复制代码
//消息解码
int utils::code::decode_message(std::string in_messaage, std::string &out_messsage) {
    int ret = WS_OPENING_FRAME;
    const char *frameData = in_messaage.c_str();
    const int frameLength = in_messaage.size();


    if (frameLength < 2) {
        ret = WS_ERROR_FRAME;
    }
    //拓展位暂不处理
    if ((frameData[0] & 0x70) != 0x0) {
        ret = WS_ERROR_FRAME;
    }

    // fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文
    ret = (frameData[0] & 0x80);
    if ((frameData[0] & 0x80) != 0x80) {
        ret = WS_ERROR_FRAME;
    }

    // mask位, 为1表示数据被加密
    if ((frameData[1] & 0x80) != 0x80) {
        ret = WS_ERROR_FRAME;
    }

    uint16_t payloadLength = 0;
    uint8_t payloadFieldExtraBytes = 0;
    uint8_t opcode = static_cast<uint8_t >(in_messaage[0] & 0x0f);

    if (opcode == WS_TEXT_FRAME) {
        payloadLength = static_cast<uint8_t >(in_messaage[1] & 0x7f);
        if (payloadLength == 0x7e) {
            uint16_t payloadLength16b = 0;
            payloadFieldExtraBytes = 2;
            memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
            payloadLength = ntohs(payloadLength16b);
        } else if (payloadLength == 0x7f) {
            // 数据过长,暂不支持
            return WS_ERROR_FRAME;
//            ret = WS_ERROR_FRAME;
        }
        const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
        char *payloadData = new char[payloadLength + 1];
        memset(payloadData, 0, payloadLength + 1);
        memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
        for (int i = 0; i < payloadLength; i++) {
            payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
        }
        out_messsage = payloadData;
        delete[] payloadData;
        return WS_TEXT_FRAME;
    }
    return opcode;
}
C++ 复制代码
//消息编码
int utils::code::encode_message(std::string in_messaage, std::string &out_message, uint8_t frameType) {
    int ret = WS_EMPTY_FRAME;
    const uint32_t message_length = in_messaage.size();
    if (message_length > 32767) {
        return WS_ERROR_FRAME;
    }
    uint8_t payload_fiel_extr_bytes = (message_length <= 0x7d) ? 0 : 2;
    uint8_t frame_header_size = 2 + payload_fiel_extr_bytes;
    uint8_t *frame_header = new uint8_t[frame_header_size];
    memset(frame_header, 0, frame_header_size);
    frame_header[0] = static_cast<uint8_t >(0x80 | frameType);
    // 填充数据长度
    if (message_length <= 0x7d) {
        frame_header[1] = static_cast<uint8_t>(message_length);
    } else {
        frame_header[1] = 0x7e;
        uint16_t len = htons(message_length);
        memcpy(&frame_header[2], &len, payload_fiel_extr_bytes);
    }

    // 填充数据
    uint32_t frameSize = frame_header_size + message_length;
    char *frame = new char[frameSize + 1];
    memcpy(frame, frame_header, frame_header_size);
    memcpy(frame + frame_header_size, in_messaage.c_str(), message_length);
    frame[frameSize] = '\0';
    out_message = frame;
    return true;
}

websocket连接断开的方式

抓连接断开的数据包

这里我们只需要判断opcode是否为8,如果是就将该连接直接close掉。

C++ 复制代码
bool ws_closing_frame(std::string message, int ret, int fd) {
    if (ret != 8) return false;
    Socket::Close(fd);
    return true;
}

接下来将他们组合起来就可以了,我自己组合了一边,并开源在github,项目使用了epoll做io复用的处理。消息的处理上我使用了责任链模式可在不更改大体框架的情况下更加灵活的更改消息的处理方式。

代码开源地址:github.com/Fall-Rain/w...

欢迎一起完善与学习。

参考文章:

相关推荐
网络抓包与爬虫3 小时前
如何在Android studio用flutter开发app
websocket·网络协议·tcp/ip·http·网络安全·https·udp
色的归属感3 小时前
android studio 安装flutter插件
websocket·网络协议·tcp/ip·http·网络安全·https·udp
我也爱吃馄饨7 小时前
项目复盘:websocket不受跨域限制的原理
网络·websocket·网络协议
网络抓包与爬虫9 小时前
android应用崩溃了,通过崩溃手机连接电脑查找日志方法
websocket·网络协议·tcp/ip·http·网络安全·https·udp
iOS技术狂热者10 小时前
Flutter Mac上使用VSCode支持Flutter开发
websocket·网络协议·tcp/ip·http·网络安全·https·udp
鹅肝手握高V五色16 小时前
android studio 运行flutter项目
websocket·网络协议·tcp/ip·http·网络安全·https·udp
flying jiang1 天前
HttpServletRequest
java·网络·websocket·http
Jiaberrr1 天前
Vue3 实战:基于 mxGraph 与 WebSocket 的动态流程图构建
前端·javascript·vue.js·websocket·流程图
EdmundXjs1 天前
浅谈WebSocket-FLV
websocket·网络协议
金丝猴也是猿1 天前
手机抓取崩溃的log日志(安卓/ios)
websocket·网络协议·tcp/ip·http·网络安全·https·udp