我是靠谱客的博主 儒雅绿草,最近开发中收集的这篇文章主要介绍服务端使用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,将其进行加密再返回到浏览器端

//利用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;
}

//等待连接
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;
}

//握手的操作
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 Protocolsrn"
            "Upgrade: websocketrn"
            "Connection: Upgradern"
            "Sec-WebSocket-Accept: " + sec_websocket_accept + "rnrn";
    write(conn, buff.c_str(), buff.length());//将应答头返回给浏览器
    after_handshake(conn);
    epoll::epoll_add(this->epfd, conn, EPOLLIN);
    return true;
}

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

抓个包看看数据

websocket消息体的结构

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

//消息解码
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;
}

//消息编码
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] = '';
    out_message = frame;
    return true;
}

websocket连接断开的方式

抓连接断开的数据包

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

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 - Fall-Rain/websocekt

欢迎一起完善与学习。

参考文章:

  • 服务端使用c++实现websocket协议解析及通信
  • UNIX网络编程 卷1:套接字联网API

最后

以上就是儒雅绿草为你收集整理的服务端使用C++实现非阻塞的websocket的全部内容,希望文章能够帮你解决服务端使用C++实现非阻塞的websocket所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(46)

评论列表共有 0 条评论

立即
投稿
返回
顶部