服务端使用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 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消息体的结构

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

//消息解码
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] = '\0';
    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

欢迎一起完善与学习。

参考文章:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值