【Websocket 】带着问题来看(一)

95 阅读19分钟

楔子

WebSocket 的主要优点和缺点

  1. 全双工通信

    • WebSocket 支持双向通信,客户端和服务器可以同时发送和接收数据,而不需要像 HTTP 那样依赖请求-响应模式。
  2. 低延迟

    • WebSocket 建立连接后,数据传输的延迟非常低,适合实时应用(如聊天、游戏、实时数据推送等)。
  3. 减少带宽消耗

    • WebSocket 的连接是持久的,不需要像 HTTP 那样频繁地建立和关闭连接,减少了握手和头信息的开销。
  4. 高效的数据传输

    • WebSocket 的数据帧较小,传输效率高,特别适合频繁的小数据包传输。
  5. 支持二进制和文本数据

    • WebSocket 可以传输二进制数据和文本数据,灵活性高。
  6. 跨域支持

    • WebSocket 支持跨域通信,可以通过 wss:// 实现安全的跨域数据传输。
  7. 心跳机制

    • WebSocket 支持 Ping/Pong 帧,可以用于检测连接是否存活,避免连接因超时被关闭。
  8. 易于扩展

    • WebSocket 协议支持扩展,可以通过扩展实现压缩、加密等功能。
  9. 兼容性好

    • 现代浏览器和主流编程语言都支持 WebSocket,易于集成到现有系统中。
  10. 减少服务器压力

  • 相比 HTTP 轮询,WebSocket 的持久连接减少了服务器的并发压力。

WebSocket 是基于什么协议实现的

WebSocket 是基于 TCP 协议 实现的。具体来说,WebSocket 是一个应用层协议,它依赖于传输层的 TCP 协议来提供可靠的双向通信。


WebSocket 和 TCP 的关系

  1. 传输层协议:TCP

    • WebSocket 使用 TCP 作为底层传输协议,确保数据的可靠传输(无丢失、无重复、按序到达)。
    • TCP 提供了面向连接、可靠的字节流服务,适合 WebSocket 的实时通信需求。
  2. 应用层协议:WebSocket

    • WebSocket 是一个独立的应用层协议,定义了自己的握手过程、数据帧格式和通信规则。
    • 它在 TCP 之上运行,利用 TCP 提供的可靠传输能力,实现了全双工通信。

WebSocket 的数据帧

WebSocket 定义了自己的数据帧格式,用于传输文本或二进制数据。数据帧包括以下部分:

  • FIN :指示是否是消息的最后一帧。
  • Opcode:指示帧的类型(如文本帧、二进制帧、关闭帧等)。
  • Mask :指示是否使用掩码(客户端到服务器的帧必须掩码)。
  • Payload length:指示数据的长度。
  • Payload data:实际传输的数据。

WebSocket 连接的建立分为两个阶段:

  1. HTTP 握手阶段:客户端和服务器通过 HTTP 协议进行握手,协商升级到 WebSocket 协议。
  2. WebSocket 通信阶段:握手成功后,客户端和服务器使用 WebSocket 协议进行双向通信。

WebSocket 连接建立的详细过程

step 1 :客户端发起握手请求

客户端发送一个 HTTP GET 请求,请求头中包含以下关键字段:

  • Upgrade: websocket:表示客户端希望升级到 WebSocket 协议。
  • Connection: Upgrade:表示客户端希望升级连接。
  • Sec-WebSocket-Key:一个随机生成的 Base64 编码字符串,用于握手验证。
  • Sec-WebSocket-Version:指定 WebSocket 协议版本(通常是 13)。

示例:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
step 2 服务器响应握手

服务器收到客户端的握手请求后,如果同意升级到 WebSocket 协议,会返回一个 HTTP 101 状态码(Switching Protocols),并在响应头中包含以下关键字段:

  • Upgrade: websocket:表示服务器同意升级到 WebSocket 协议。
  • Connection: Upgrade:表示服务器同意升级连接。
  • Sec-WebSocket-Accept:服务器根据客户端的 Sec-WebSocket-Key 计算出的验证字符串。

Sec-WebSocket-Accept 的计算方法:

  1. 将客户端的 Sec-WebSocket-Key 与固定的 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)拼接。
  2. 对拼接后的字符串进行 SHA-1 哈希计算。
  3. 将哈希结果进行 Base64 编码。

示例:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
step3 升级到 WebSocket 协议
-   握手完成后,客户端和服务器之间的通信将使用 WebSocket 协议,不再使用 HTTP。

WebSocket 的数据帧

WebSocket 定义了自己的数据帧格式,用于传输文本或二进制数据。数据帧包括以下部分:

  1. FIN:1 位,表示是否是消息的最后一帧。
  2. RSV1, RSV2, RSV3:各 1 位,保留字段,通常为 0。
  3. Opcode:4 位,表示帧的类型(如文本帧、二进制帧、关闭帧等)。
  4. Mask:1 位,指示是否使用掩码。
  5. Payload length:7 位、7+16 位或 7+64 位,表示数据的长度。
  6. Masking key:4 字节(如果 Mask 为 1),用于掩码计算。
  7. Payload data:实际的数据。
// WebSocket 数据帧结构体
typedef struct {
  uint8_t fin : 1;       // FIN 标志位
  uint8_t rsv1 : 1;      // RSV1 标志位
  uint8_t rsv2 : 1;      // RSV2 标志位
  uint8_t rsv3 : 1;      // RSV3 标志位
  uint8_t opcode : 4;    // 操作码(Opcode)
  uint8_t mask : 1;      // 掩码标志位
  uint8_t payload_len : 7; // 数据长度(7 位或扩展)
  uint32_t masking_key;  // 掩码键(4 字节)
  uint8_t* payload_data; // 数据内容
} WebSocketFrame;

: 1 表示这个成员只占用 1 个二进制位(bit)

  • 来个图KK

Screenshot 2025-03-25 at 11.39.12.png

WebSocket 是全双工还是半双工

WebSocket 是 全双工(Full-Duplex)  的通信协议。

全双工 vs 半双工

  1. 全双工(Full-Duplex)

    • 通信双方可以同时发送和接收数据。
    • 例如:电话通话,双方可以同时说话和听对方说话。
  2. 半双工(Half-Duplex)

    • 通信双方可以发送和接收数据,但不能同时进行。
    • 例如:对讲机,同一时间只能有一方说话,另一方听。

WebSocket 的握手请求头中有哪些关键字段

WebSocket 握手请求头中的关键字段包括:

  • Upgrade
  • Connection
  • Sec-WebSocket-Key
  • Sec-WebSocket-Version
  • Host
  • Origin(可选)
  • Sec-WebSocket-Protocol(可选)
  • Sec-WebSocket-Extensions(可选)

解释

1. Upgrade

  • 作用:表示客户端希望将连接升级到 WebSocket 协议。

  • :必须为 websocket

  • 示例

    Upgrade: websocket
    

2. Connection

  • 作用:表示客户端希望升级连接。

  • :必须为 Upgrade

  • 示例

    Connection: Upgrade
    

3. Sec-WebSocket-Key

  • 作用:客户端随机生成的一个 Base64 编码的字符串,用于握手验证。

  • :16 字节随机数的 Base64 编码。

  • 示例:

    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    

4. Sec-WebSocket-Version

  • 作用:指定客户端支持的 WebSocket 协议版本。

  • :当前标准版本为 13

  • 示例

    Sec-WebSocket-Version: 13
    

5. Host

  • 作用:指定服务器的域名或 IP 地址。

  • :服务器的地址。

  • 示例

    Host: example.com
    

6. Origin(可选)

  • 作用:表示请求的来源,用于跨域验证。

  • :客户端的源地址(协议 + 域名 + 端口)。

  • 示例

    Origin: http://example.com
    

7. Sec-WebSocket-Protocol(可选)

  • 作用:指定客户端支持的子协议(如 chatsuperchat 等)。

  • :子协议名称。

  • 示例

    Sec-WebSocket-Protocol: chat, superchat
    

8. Sec-WebSocket-Extensions(可选)

  • 作用:指定客户端支持的扩展(如压缩、加密等)。

  • :扩展名称。

  • 示例

    Sec-WebSocket-Extensions: permessage-deflate
    

完整的 WebSocket 握手请求示例

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate

WebSocket 的最大帧大小是多少

  1. 协议设计

    • WebSocket 数据帧的 Payload length 字段可以表示的最大长度为 2^63 - 1 字节(约 9.22 EB,Exabytes)。
    • 这是一个理论值,实际应用中不会达到这个大小。
  2. 实现限制

    • 客户端和服务器的实现可能会对帧大小设置限制。
    • 例如,某些 WebSocket 库可能默认限制帧大小为 16 MB 或更小。
  3. 网络和硬件限制

    • 网络带宽、内存和硬件资源也会影响帧大小的实际限制。
    • Java 的 Java-WebSocket 库
import org.java_websocket.server.WebSocketServer;

WebSocketServer server = new WebSocketServer(new InetSocketAddress(8765)) {
    @Override
    public void onOpen(WebSocket conn, ClientHandshake handshake) {}

    @Override
    public void onMessage(WebSocket conn, String message) {}

    @Override
    public void onError(WebSocket conn, Exception ex) {}

    @Override
    public void onStart() {}
};

server.setMaxFrameSize(100 * 1024 * 1024); // 设置最大帧大小为 100 MB
server.start();
  • WebSocket 协议本身没有明确规定最大帧大小,理论最大长度为 2^63 - 1 字节
  • 实际应用中,浏览器和服务器通常会对帧大小设置限制(如 16 MB)。
  • 如果需要传输大文件或大数据,可以将数据分片传输,或通过配置调整帧大小限制

WebSocket 的连接状态有哪些

WebSocket 的连接状态包括:

  1. CONNECTING:连接中。
  2. OPEN:连接已打开,可以通信。
  3. CLOSING:连接关闭中。
  4. CLOSED:连接已关闭。

通过监听 onopenonclose 和 onerror 事件,可以实时获取连接状态的变化。

WebSocket 的连接是如何关闭的?

1. 发送关闭帧(Close Frame)
  • 关闭帧的格式

    • Opcode0x8(表示关闭帧)。
    • Payload:可选的关闭原因(状态码和原因短语)。
  • 状态码

    • 2 字节的无符号整数,表示关闭原因。
    • 例如:1000(正常关闭)、1001(端点离开)等。
  • 原因短语

    • 可选的 UTF-8 编码字符串,描述关闭原因。
2. 接收关闭帧
  • 当一端收到关闭帧后,必须发送一个关闭帧作为响应。
  • 收到关闭帧后,连接进入 CLOSING 状态。
3. 关闭 TCP 连接
  • 在双方都发送和接收了关闭帧后,连接进入 CLOSED 状态。
  • 底层 TCP 连接被关闭。
  • WebSocket 关闭连接的示例

1. 客户端主动关闭连接

javascript

Copy

const socket = new WebSocket('ws://example.com');

// 监听连接打开事件
socket.onopen = () => {
    console.log('WebSocket 连接已打开');
    // 主动关闭连接
    socket.close(1000, 'Normal Closure');
};

// 监听连接关闭事件
socket.onclose = (event) => {
    console.log('WebSocket 连接已关闭');
    console.log('状态码:', event.code);
    console.log('原因:', event.reason);
};
2. 服务器主动关闭连接(Node.js + ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
    console.log('客户端已连接');

    // 服务器主动关闭连接
    ws.close(1000, 'Normal Closure');

    ws.on('close', (code, reason) => {
        console.log('客户端已断开连接');
        console.log('状态码:', code);
        console.log('原因:', reason.toString());
    });
});

WebSocket 关闭连接的生命周期

  1. OPEN → CLOSING

    • 当一端发送关闭帧后,连接进入 CLOSING 状态。
  2. CLOSING → CLOSED

    • 当另一端也发送关闭帧后,连接进入 CLOSED 状态。
  3. 异常关闭

    • 如果未收到关闭帧(如网络中断),连接直接进入 CLOSED 状态。

WebSocket 的关闭帧是什么 (opcode = 0x8)

WebSocket 连接的关闭是通过 关闭握手 实现的:

  1. 发送关闭帧(包含状态码和原因短语)。
  2. 接收关闭帧并响应。
  3. 关闭底层 TCP 连接。

通过监听 onclose 事件,可以获取关闭的状态码和原因,便于调试和处理异常情况。

WebSocket 的关闭帧是什么

WebSocket 的 关闭帧(Close Frame)  是 WebSocket 协议中用于关闭连接的一种特殊数据帧。 关闭帧的作用是通知对方连接即将关闭,并可以携带关闭原因(状态码和原因短语)。


关闭帧的结构

关闭帧是 WebSocket 数据帧的一种,其结构如下:

字段长度描述
FIN1 位表示是否是消息的最后一帧(关闭帧必须为 1)。
RSV1, RSV2, RSV3各 1 位保留字段,必须为 0
Opcode4 位操作码,关闭帧的值为 0x8
Mask1 位指示是否使用掩码(客户端到服务器的帧必须掩码)。
Payload length7/7+16/7+64 位数据长度(关闭帧的 Payload 长度可以为 0 或 2 字节以上)。
Masking key4 字节(如果 Mask 为 1)掩码键(客户端到服务器的帧必须掩码)。
Payload data可变长度关闭帧的有效载荷,包含状态码和原因短语(可选)。

关闭帧的有效载荷(Payload Data)

关闭帧的有效载荷可以包含以下内容:

  1. 状态码(Status Code)

    • 2 字节的无符号整数,表示关闭原因。
    • 例如:1000(正常关闭)、1001(端点离开)等。
  2. 原因短语(Reason Phrase)

    • 可选的 UTF-8 编码字符串,描述关闭原因。
    • 最大长度为 123 字节(因为 WebSocket 帧的最大 Payload 长度为 125 字节,状态码占 2 字节)。

关闭帧的示例

以下是一个关闭帧的示例:

  • 状态码1000(正常关闭)。
  • 原因短语"Normal Closure"
关闭帧的字节流
字节位置值(十六进制)描述
00x88FIN=1, RSV1=0, RSV2=0, RSV3=0, Opcode=0x8(关闭帧)。
10x82Mask=1, Payload length=2(状态码占 2 字节)。
2-50x37 0xFA 0x21 0x3D掩码键(随机生成的 4 字节)。
6-70x03 0xE8状态码 10000x03E8 是 1000 的十六进制表示)。
8-150x34 0x9F 0x45 0x6E 0x6F 0x72 0x6D 0x61原因短语 "Normal Closure" 的掩码数据。

关闭帧的发送与接收

1. 发送关闭帧
  • 客户端或服务器可以调用 close() 方法发送关闭帧。

  • 示例(JavaScript):

    const socket = new WebSocket('ws://example.com');
    socket.close(1000, 'Normal Closure');
    
2. 接收关闭帧
  • 当一端收到关闭帧后,必须发送一个关闭帧作为响应。

  • 示例(JavaScript):

    socket.onclose = (event) => {
        console.log('状态码:', event.code);
        console.log('原因:', event.reason);
    };
    

关闭帧的状态码

WebSocket 协议定义了一些标准的状态码,用于表示关闭原因:

状态码名称描述
1000Normal Closure正常关闭
1001Going Away端点离开(如服务器关闭或浏览器导航离开)
1002Protocol Error协议错误
1003Unsupported Data接收到不支持的数据(如文本帧接收二进制数据)
1004Reserved保留
1005No Status Rcvd未收到状态码(用于占位)
1006Abnormal Closure异常关闭(如未发送关闭帧)
1007Invalid Frame Payload Data接收到无效的数据(如非 UTF-8 文本数据)
1008Policy Violation策略违规
1009Message Too Big消息过大
1010Mandatory Extension需要扩展
1011Internal Server Error服务器内部错误
1015TLS HandshakeTLS 握手失败

总结

WebSocket 的关闭帧是用于关闭连接的特殊数据帧,其结构包括:

  • Opcode0x8
  • Payload:可选的关闭原因(状态码和原因短语)。

通过发送和接收关闭帧,WebSocket 连接可以优雅地关闭,并传递关闭原因

WebSocket 的心跳机制是什么

WebSocket 的 心跳机制(Heartbeat Mechanism)  是一种用于检测连接是否存活的机制。通过定期发送和接收 Ping/Pong 帧,客户端和服务器可以确认对方是否仍然在线,从而避免因长时间无通信而导致的连接超时或断开。

心跳机制的作用

  1. 检测连接状态

    • 通过定期发送 Ping 帧,检测对方是否仍然在线。
  2. 防止连接超时

    • 如果连接长时间无通信,可能会被防火墙、代理服务器或操作系统关闭。心跳机制可以保持连接活跃。
  3. 快速发现断线

    • 如果未收到 Pong 帧响应,可以快速发现连接已断开。

Ping/Pong 帧

WebSocket 协议定义了两种控制帧用于心跳机制:

  1. Ping 帧

    • Opcode:0x9
    • 用于发送心跳检测。
    • 可以携带少量数据(如时间戳)。
  2. Pong 帧

    • Opcode:0xA
    • 用于响应 Ping 帧。
    • 必须携带与对应 Ping 帧相同的数据。

心跳机制的实现

1. 客户端发送 Ping 帧

客户端可以定期向服务器发送 Ping 帧,检测服务器是否在线。

2. 服务器响应 Pong 帧

服务器收到 Ping 帧后,必须发送一个 Pong 帧作为响应。

3. 超时处理

如果客户端在指定时间内未收到 Pong 帧,可以认为连接已断开,并进行重连或其他处理。


心跳机制的示例

以下是一个简单的心跳机制实现示例:

1. 客户端实现(JavaScript)
const socket = new WebSocket('ws://example.com');

// 定时发送 Ping 帧
const heartbeatInterval = 30000; // 30 秒
let heartbeatTimer;

socket.onopen = () => {
    console.log('WebSocket 连接已打开');
    // 启动心跳机制
    heartbeatTimer = setInterval(() => {
        if (socket.readyState === WebSocket.OPEN) {
            socket.send('Ping'); // 发送 Ping 帧
        }
    }, heartbeatInterval);
};

// 监听 Pong 帧
socket.onmessage = (event) => {
    if (event.data === 'Pong') {
        console.log('收到 Pong 帧');
    }
};

// 监听连接关闭事件
socket.onclose = () => {
    console.log('WebSocket 连接已关闭');
    // 清除心跳定时器
    clearInterval(heartbeatTimer);
};
2. 服务器实现(Node.js + ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
    console.log('客户端已连接');

    // 监听 Ping 帧
    ws.on('ping', (data) => {
        console.log('收到 Ping 帧');
        // 发送 Pong 帧
        ws.pong(data);
    });

    // 监听连接关闭事件
    ws.on('close', () => {
        console.log('客户端已断开连接');
    });
});

心跳机制的优化

  1. 动态调整心跳间隔

    • 根据网络状况动态调整心跳间隔,避免频繁发送 Ping 帧。
  2. 超时重连

    • 如果未收到 Pong 帧,可以尝试重连。
  3. 携带数据

    • 在 Ping 帧中携带时间戳或其他数据,用于计算延迟或诊断问题。

总结

WebSocket 的心跳机制通过 Ping/Pong 帧 实现,用于检测连接是否存活。客户端定期发送 Ping 帧,服务器响应 Pong 帧。如果未收到 Pong 帧,可以认为连接已断开,并进行相应处理。心跳机制是保持 WebSocket 连接稳定的重要手段。

如何实现 WebSocket 的加密传输(wss)

实现 WebSocket 的加密传输(wss)的步骤如下:

  1. 获取 SSL/TLS 证书。
  2. 配置服务器支持 wss
  3. 客户端通过 wss:// 协议连接服务器。

通过 wss,可以确保 WebSocket 通信的安全性,防止数据被窃听或篡改。

如何处理 WebSocket 跨域问题

览器跨域问题是指在Web开发中,由于浏览器的同源策略(Same-Origin Policy)限制,导致在一个域下的网页无法与其他域(协议、域名、端口有任何一个不同即为不同域)的资源进行交互或通信的情况

使用websocket解决跨域,首先也是需要服务端支持websocket相关的协议,可以用于两个页面之间的通信,A页面和A服务器正常通信,B页面通过websocket和A服务器通信,从而达到A页面和B页面的通信。这其实也是一个比较彻底的方案,绕过了浏览器的限制,如果有两个页面的通信,可以使用这个方案。不过我的经验,在日常中,websocket主要还是做服务端向前端推送的工作,做跨域的比较少。

  • 当浏览器从一个域名下的页面向另一个域名服务器发起WebSocket连接时,就会涉及到跨域问题,
  • WebSocket 是一种在客户端和服务器之间建立持久连接的通信协议,它可以用于实时通信和数据传输。由于 WebSocket 是一种独立的协议,不受同源策略的限制,因此它可以轻松解决跨域问题。下面是 WebSocket 如何解决跨域问题的简要说明:
  1. 不受同源策略限制:WebSocket 协议不受同源策略的限制,允许在不同域之间建立连接。
  2. 独立的协议:WebSocket 使用自己的协议,与 HTTP/HTTPS 不同,因此不受同源策略的限制。在初始握手期间,服务器可以使用适当的响应头来允许跨域连接。
  3. 服务器设置:要允许跨域 WebSocket 连接,服务器端需要设置适当的响应头。通常,服务器会设置 Access-Control-Allow-Origin 头来指定允许的来源域。例如,可以将其设置为 * 表示允许来自任何域的连接。

服务器端如何设置允许跨域 WebSocket 连接:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
  });

  ws.send('something');
});

// 设置允许跨域连接
wss.on('headers', (headers, req) => {
  headers['Access-Control-Allow-Origin'] = '*';
});

通过上述设置,WebSocket 可以轻松解决跨域问题,实现不同域之间的实时通信和数据传输。

如何在 WebSocket 中实现身份认证

  • 建立连接时验证身份:在客户端发起 WebSocket 连接时,可以在握手阶段进行身份验证。这可以通过在连接 URL 中传递身份信息(如 token)或在连接的 HTTP 头部中包含身份验证信息来实现。

  • 验证身份信息:在服务端接收到 WebSocket 连接请求后,可以解析连接 URL 或读取 HTTP 头部中的身份验证信息,并进行验证。这可能涉及检查 token 的有效性、检查用户权限等操作。

  • 认证成功后建立连接:如果身份验证成功,服务端可以接受连接并与客户端建立 WebSocket 连接。否则,可以拒绝连接或断开连接。

  • 维护身份信息:一旦身份验证成功,服务端可以在连接建立后维护用户的身份信息,以便在后续通信中使用。

#include <libwebsockets.h>
#include <string.h>

static int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
    return 0;
}

static int callback_websocket(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
    switch (reason) {
        case LWS_CALLBACK_ESTABLISHED: {
            // 在连接建立时进行身份验证
            const char *token = lws_hdr_simple_ptr(wsi, WSI_TOKEN_GET_URI);
            if (strcmp(token, "valid_token") == 0) {
                // 身份验证成功,向客户端发送消息
                char msg[] = "Authentication successful. You are now connected.";
                lws_write(wsi, (unsigned char *)msg, strlen(msg), LWS_WRITE_TEXT);
            } else {
                // 身份验证失败,关闭连接
                char msg[] = "Authentication failed. Connection refused.";
                lws_write(wsi, (unsigned char *)msg, strlen(msg), LWS_WRITE_TEXT);
                lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, NULL, 0);
            }
            break;
        }
        default:
            break;
    }

    return 0;
}

static struct lws_protocols protocols[] = {
    {
        "http-only",
        callback_http,
        0,
    },
    {
        "websocket",
        callback_websocket,
        0,
    },
    { NULL, NULL, 0, 0 }
};

int main() {
    struct lws_context_creation_info info;
    memset(&info, 0, sizeof(info));

    struct lws_context *context = lws_create_context(&info);
    if (!context) {
        return -1;
    }

    int port = 8080;
    const char *iface = NULL;

    struct lws_vhost *vhost = lws_create_vhost(context, &port, iface, NULL, NULL, NULL, NULL);
    if (!vhost) {
        lws_context_destroy(context);
        return -1;
    }

    while (1) {
        lws_service(context, 50);
    }

    lws_context_destroy(context);

    return 0;
}