服务端使用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...

欢迎一起完善与学习。

参考文章:

相关推荐
咖啡教室7 小时前
每日一个计算机小知识:Socket
后端·websocket
paishishaba9 小时前
HTTP、HTTPS 和 WebSocket 协议和开发
websocket·http·https·实时聊天
小范同学_1 天前
Spring集成WebSocket
java·spring boot·websocket·spring·1024程序员节
YUELEI1183 天前
Springboot WebSocket
spring boot·后端·websocket
Greedy Alg6 天前
Socket编程学习记录
网络·websocket·学习
Cxiaomu6 天前
React Native 项目中 WebSocket 的完整实现方案
websocket·react native·react.js
Arva .7 天前
WebSocket实现网站点赞通知
网络·websocket·网络协议
火星数据-Tina7 天前
LOL实时数据推送技术揭秘:WebSocket在电竞中的应用
网络·websocket·网络协议
paopaokaka_luck7 天前
基于SpringBoot+Vue的社区诊所管理系统(AI问答、webSocket实时聊天、Echarts图形化分析)
vue.js·人工智能·spring boot·后端·websocket
歪歪1007 天前
使用 Wireshark 进行 HTTP、MQTT、WebSocket 抓包的详细教程
网络·websocket·测试工具·http·wireshark