从零实现WebSocket:实时通信的核心协议

前言

你有没有想过:网页上的实时聊天、股票行情推送、在线游戏,是怎么做到服务器主动给客户端发消息的?

HTTP是"一问一答":客户端问,服务器才能答。服务器不能主动推。

WebSocket解决了这个问题:建立一次连接后,双方可以随时互相发消息,真正的全双工通信。

今天我们用C语言从零实现WebSocket服务器:

· WebSocket协议帧解析

· 握手协议(HTTP升级)

· 数据帧编码/解码

· 完整的多客户端聊天室

· 支持文本和二进制消息


一、WebSocket核心原理

  1. 握手阶段(HTTP升级)

```

客户端请求:

GET /chat HTTP/1.1

Host: localhost:8080

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Sec-WebSocket-Version: 13

服务器响应:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

```

  1. 数据帧格式

```

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-------+-+-------------+-------------------------------+

|F|R|R|R| opcode|M| Payload len | Extended payload length |

|I|S|S|S| (4) |A| (7) | (16/64) |

|N|V|V|V| |S| | (if payload len==126/127) |

| |1|2|3| |K| | |

+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

| Extended payload length continued, if payload len == 127 |

                                • +-------------------------------+

| |Masking-key, if MASK set to 1 |

+-------------------------------+-------------------------------+

| Masking-key (continued) | Payload Data |

+-------------------------------- - - - - - - - - - - - - - - - +

: Payload Data continued ... :

| Payload Data continued ... |

+---------------------------------------------------------------+

```

  1. 操作码(Opcode)

值 含义

0x0 连续帧

0x1 文本帧

0x2 二进制帧

0x8 连接关闭

0x9 Ping

0xA Pong


二、完整代码实现

  1. 基础结构

```c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <pthread.h>

#include <signal.h>

#include <errno.h>

#include <fcntl.h>

#include <time.h>

#include <openssl/sha.h>

#include <openssl/evp.h>

#include <openssl/bio.h>

#include <openssl/buffer.h>

#define BUFFER_SIZE 65536

#define MAX_CLIENTS 100

#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

// WebSocket帧结构

typedef struct {

unsigned char fin; // 是否最后一帧

unsigned char opcode; // 操作码

unsigned char masked; // 是否掩码

unsigned long long payload_len;

unsigned char *masking_key;

unsigned char *payload;

} ws_frame_t;

// 客户端结构

typedef struct {

int fd;

struct sockaddr_in addr;

int connected;

pthread_t thread;

char nickname64;

} ws_client_t;

// 服务器结构

typedef struct {

int server_fd;

int port;

ws_client_t *clientsMAX_CLIENTS;

pthread_mutex_t clients_mutex;

} ws_server_t;

```

  1. Base64编码(用于握手)

```c

char *base64_encode(const unsigned char *input, int len) {

BIO *bio, *b64;

BUF_MEM *buffer_ptr;

b64 = BIO_new(BIO_f_base64());

bio = BIO_new(BIO_s_mem());

bio = BIO_push(b64, bio);

BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);

BIO_write(bio, input, len);

BIO_flush(bio);

BIO_get_mem_ptr(bio, &buffer_ptr);

char *output = malloc(buffer_ptr->length + 1);

memcpy(output, buffer_ptr->data, buffer_ptr->length);

outputbuffer_ptr-\>length = '\0';

BIO_free_all(bio);

return output;

}

```

  1. WebSocket握手

```c

// 计算握手响应

char *compute_websocket_accept(const char *key) {

char concat256;

snprintf(concat, sizeof(concat), "%s%s", key, GUID);

unsigned char hashSHA_DIGEST_LENGTH;

SHA1((unsigned char*)concat, strlen(concat), hash);

return base64_encode(hash, SHA_DIGEST_LENGTH);

}

// 执行握手

int websocket_handshake(int client_fd, const char *request) {

// 查找Sec-WebSocket-Key

char *key_start = strstr(request, "Sec-WebSocket-Key: ");

if (!key_start) {

return -1;

}

key_start += 19;

char *key_end = strstr(key_start, "\r\n");

if (!key_end) {

return -1;

}

int key_len = key_end - key_start;

char *key = malloc(key_len + 1);

memcpy(key, key_start, key_len);

keykey_len = '\0';

// 计算响应

char *accept = compute_websocket_accept(key);

// 发送响应

char response512;

snprintf(response, sizeof(response),

"HTTP/1.1 101 Switching Protocols\r\n"

"Upgrade: websocket\r\n"

"Connection: Upgrade\r\n"

"Sec-WebSocket-Accept: %s\r\n"

"\r\n",

accept);

free(key);

free(accept);

return send(client_fd, response, strlen(response), 0);

}

```

  1. WebSocket帧解码

```c

// 读取n字节

int recv_n(int fd, void *buf, int n) {

int received = 0;

while (received < n) {

int ret = recv(fd, (char*)buf + received, n - received, 0);

if (ret <= 0) return -1;

received += ret;

}

return received;

}

// 解码WebSocket帧

ws_frame_t *decode_frame(int fd) {

ws_frame_t *frame = calloc(1, sizeof(ws_frame_t));

unsigned char header2;

if (recv_n(fd, header, 2) < 0) {

free(frame);

return NULL;

}

frame->fin = (header0 & 0x80) >> 7;

frame->opcode = header0 & 0x0F;

frame->masked = (header1 & 0x80) >> 7;

frame->payload_len = header1 & 0x7F;

// 读取扩展长度

if (frame->payload_len == 126) {

unsigned char ext_len2;

if (recv_n(fd, ext_len, 2) < 0) {

free(frame);

return NULL;

}

frame->payload_len = (ext_len0 << 8) | ext_len1;

} else if (frame->payload_len == 127) {

unsigned char ext_len8;

if (recv_n(fd, ext_len, 8) < 0) {

free(frame);

return NULL;

}

// 简单处理:只取低4字节

frame->payload_len = 0;

for (int i = 4; i < 8; i++) {

frame->payload_len = (frame->payload_len << 8) | ext_leni;

}

}

// 读取掩码

if (frame->masked) {

frame->masking_key = malloc(4);

if (recv_n(fd, frame->masking_key, 4) < 0) {

free(frame->masking_key);

free(frame);

return NULL;

}

}

// 读取数据

if (frame->payload_len > 0) {

frame->payload = malloc(frame->payload_len);

if (recv_n(fd, frame->payload, frame->payload_len) < 0) {

if (frame->masking_key) free(frame->masking_key);

free(frame->payload);

free(frame);

return NULL;

}

// 解掩码

if (frame->masked) {

for (unsigned long long i = 0; i < frame->payload_len; i++) {

frame->payloadi ^= frame->masking_keyi % 4;

}

}

}

return frame;

}

```

  1. WebSocket帧编码

```c

// 编码WebSocket帧

unsigned char *encode_frame(ws_frame_t *frame, int *out_len) {

// 计算帧大小

int header_size = 2;

if (frame->payload_len < 126) {

header_size = 2;

} else if (frame->payload_len < 65536) {

header_size = 4;

} else {

header_size = 10;

}

*out_len = header_size + frame->payload_len;

unsigned char *buffer = malloc(*out_len + 1);

memset(buffer, 0, *out_len + 1);

// 第一个字节

buffer0 = (frame->fin << 7) | (frame->opcode & 0x0F);

// 第二个字节(服务端不设掩码)

if (frame->payload_len < 126) {

buffer1 = frame->payload_len;

} else if (frame->payload_len < 65536) {

buffer1 = 126;

buffer2 = (frame->payload_len >> 8) & 0xFF;

buffer3 = frame->payload_len & 0xFF;

} else {

buffer1 = 127;

for (int i = 0; i < 8; i++) {

buffer2 + i = (frame->payload_len >> (56 - i * 8)) & 0xFF;

}

}

// 数据

if (frame->payload_len > 0 && frame->payload) {

memcpy(buffer + header_size, frame->payload, frame->payload_len);

}

return buffer;

}

// 发送文本消息

int send_text_frame(int fd, const char *message) {

ws_frame_t frame;

frame.fin = 1;

frame.opcode = 0x01; // 文本帧

frame.masked = 0;

frame.payload_len = strlen(message);

frame.payload = (unsigned char*)message;

frame.masking_key = NULL;

int len;

unsigned char *buffer = encode_frame(&frame, &len);

int sent = send(fd, buffer, len, 0);

free(buffer);

return sent;

}

```

  1. 聊天室广播

```c

ws_server_t *g_server = NULL;

// 向所有客户端广播

void broadcast_message(const char *message, int exclude_fd) {

pthread_mutex_lock(&g_server->clients_mutex);

for (int i = 0; i < MAX_CLIENTS; i++) {

if (g_server->clientsi && g_server->clientsi->fd != exclude_fd) {

send_text_frame(g_server->clientsi->fd, message);

}

}

pthread_mutex_unlock(&g_server->clients_mutex);

}

// 处理Ping/Pong

void handle_ping(int fd) {

ws_frame_t pong_frame;

pong_frame.fin = 1;

pong_frame.opcode = 0x0A; // Pong

pong_frame.masked = 0;

pong_frame.payload_len = 0;

pong_frame.payload = NULL;

int len;

unsigned char *buffer = encode_frame(&pong_frame, &len);

send(fd, buffer, len, 0);

free(buffer);

}

// 处理客户端

void *handle_client(void *arg) {

ws_client_t *client = (ws_client_t *)arg;

printf("客户端 %s:%d 已连接\n",

inet_ntoa(client->addr.sin_addr), ntohs(client->addr.sin_port));

// 读取握手请求

char handshake4096;

int n = recv(client->fd, handshake, sizeof(handshake) - 1, 0);

if (n <= 0) {

close(client->fd);

free(client);

return NULL;

}

handshaken = '\0';

// 执行握手

if (websocket_handshake(client->fd, handshake) < 0) {

close(client->fd);

free(client);

return NULL;

}

// 发送欢迎消息

snprintf(client->nickname, sizeof(client->nickname), "Guest%d", client->fd);

char welcome256;

snprintf(welcome, sizeof(welcome), "欢迎 %s 加入聊天室!", client->nickname);

broadcast_message(welcome, -1);

// 处理消息循环

while (client->connected) {

ws_frame_t *frame = decode_frame(client->fd);

if (!frame) break;

switch (frame->opcode) {

case 0x01: // 文本帧

{

char *message = malloc(frame->payload_len + 64);

snprintf(message, frame->payload_len + 64,

"%s: %.*s", client->nickname,

(int)frame->payload_len, frame->payload);

broadcast_message(message, client->fd);

free(message);

break;

}

case 0x08: // 关闭帧

client->connected = 0;

break;

case 0x09: // Ping

handle_ping(client->fd);

break;

}

if (frame->payload) free(frame->payload);

if (frame->masking_key) free(frame->masking_key);

free(frame);

}

// 客户端离开

char leave_msg256;

snprintf(leave_msg, sizeof(leave_msg), "%s 离开了聊天室", client->nickname);

broadcast_message(leave_msg, -1);

close(client->fd);

// 从服务器列表中移除

pthread_mutex_lock(&g_server->clients_mutex);

for (int i = 0; i < MAX_CLIENTS; i++) {

if (g_server->clientsi == client) {

g_server->clientsi = NULL;

break;

}

}

pthread_mutex_unlock(&g_server->clients_mutex);

free(client);

return NULL;

}

```

  1. 服务器主循环

```c

ws_server_t *ws_server_create(int port) {

ws_server_t *server = malloc(sizeof(ws_server_t));

memset(server, 0, sizeof(ws_server_t));

server->port = port;

pthread_mutex_init(&server->clients_mutex, NULL);

// 创建socket

server->server_fd = socket(AF_INET, SOCK_STREAM, 0);

if (server->server_fd < 0) {

perror("socket");

free(server);

return NULL;

}

int opt = 1;

setsockopt(server->server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

struct sockaddr_in addr;

addr.sin_family = AF_INET;

addr.sin_addr.s_addr = INADDR_ANY;

addr.sin_port = htons(port);

if (bind(server->server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {

perror("bind");

close(server->server_fd);

free(server);

return NULL;

}

if (listen(server->server_fd, 128) < 0) {

perror("listen");

close(server->server_fd);

free(server);

return NULL;

}

printf("WebSocket服务器启动,端口: %d\n", port);

return server;

}

void ws_server_run(ws_server_t *server) {

g_server = server;

while (1) {

struct sockaddr_in client_addr;

socklen_t client_len = sizeof(client_addr);

int client_fd = accept(server->server_fd,

(struct sockaddr*)&client_addr, &client_len);

if (client_fd < 0) {

perror("accept");

continue;

}

// 创建客户端

ws_client_t *client = malloc(sizeof(ws_client_t));

client->fd = client_fd;

client->addr = client_addr;

client->connected = 1;

// 添加到服务器列表

pthread_mutex_lock(&server->clients_mutex);

int added = 0;

for (int i = 0; i < MAX_CLIENTS; i++) {

if (!server->clientsi) {

server->clientsi = client;

added = 1;

break;

}

}

pthread_mutex_unlock(&server->clients_mutex);

if (!added) {

printf("客户端已满,拒绝连接\n");

close(client_fd);

free(client);

continue;

}

// 创建处理线程

pthread_create(&client->thread, NULL, handle_client, client);

pthread_detach(client->thread);

}

}

int main(int argc, char *argv\[\]) {

int port = 8080;

if (argc > 1) port = atoi(argv1);

ws_server_t *server = ws_server_create(port);

if (!server) return 1;

ws_server_run(server);

close(server->server_fd);

free(server);

return 0;

}

```


三、客户端测试(HTML)

```html

<!DOCTYPE html>

<html>

<head>

<title>WebSocket聊天室</title>

<style>

#messages {

height: 400px;

overflow-y: auto;

border: 1px solid #ccc;

padding: 10px;

margin-bottom: 10px;

}

.message {

margin: 5px 0;

}

.system {

color: gray;

font-style: italic;

}

</style>

</head>

<body>

<h1>WebSocket聊天室</h1>

<div id="messages"></div>

<input type="text" id="input" placeholder="输入消息..." style="width: 80%">

<button onclick="send()">发送</button>

<script>

var ws = new WebSocket('ws://localhost:8080');

ws.onopen = function() {

addMessage('系统', '已连接到服务器', 'system');

};

ws.onmessage = function(event) {

var msg = event.data;

if (msg.startsWith('[')) {

// 带昵称的消息

addMessage('', msg, '');

} else {

addMessage('系统', msg, 'system');

}

};

ws.onclose = function() {

addMessage('系统', '连接已断开', 'system');

};

function addMessage(sender, text, cls) {

var div = document.createElement('div');

div.className = 'message ' + cls;

div.textContent = text;

document.getElementById('messages').appendChild(div);

div.scrollIntoView();

}

function send() {

var input = document.getElementById('input');

if (input.value && ws.readyState === WebSocket.OPEN) {

ws.send(input.value);

input.value = '';

}

}

document.getElementById('input').addEventListener('keypress', function(e) {

if (e.key === 'Enter') send();

});

</script>

</body>

</html>

```


四、编译和运行

编译

```bash

gcc -o websocket websocket.c -lpthread -lssl -lcrypto

```

运行

```bash

./websocket 8080

```

测试

  1. 用浏览器打开 index.html

  2. 开多个标签页,模拟多用户聊天


五、WebSocket vs HTTP

特性 HTTP WebSocket

协议 请求-响应 全双工

服务器推送 不支持 支持

头部开销 每次请求都有 连接时一次

实时性 差(轮询) 好

适用场景 REST API、网页 聊天、游戏、推送


六、总结

通过这篇文章,你学会了:

· WebSocket协议的握手流程

· 数据帧格式和编解码

· 掩码处理和安全

· Ping/Pong心跳机制

· 完整的多客户端聊天室

WebSocket是实现实时通信的核心技术。掌握它,你就能构建自己的聊天服务器、游戏服务器、推送服务。

下一篇预告:《从零实现一个RPC框架:远程调用与服务治理》


评论区分享一下你打算用WebSocket做什么~

相关推荐
Hello:CodeWorld1 小时前
深入浅出 C++:静态多态与动态多态的业务应用场景与源码级实战
开发语言·c++·架构
星恒随风1 小时前
C++入门(一):第一个 C++ 程序、命名空间、输入输出和缺省参数
开发语言·c++·笔记·学习
thisiszdy1 小时前
<C++&C#> lambda表达式
java·c++·c#
晚风叙码1 小时前
C++类和对象(中)| 深挖四大默认成员函数:构造/析构/拷贝/赋值重载原理全解
c++
混迹中的咸鱼1 小时前
游戏开发核心架构指南
c++·游戏·架构
-凌凌漆-2 小时前
【Qt】C++中protected与private的区别
开发语言·c++·qt
草莓熊Lotso2 小时前
【Linux网络】深入理解 HTTP 协议(四):完善 C++ HTTP 服务器:从协议原理到生产级实现
linux·运维·服务器·c语言·网络·c++·http
牛油果子哥q2 小时前
【C++前置声明与头文件】C++前置声明与头文件深度精讲:重复包含、循环依赖、重复定义报错、工程编译架构与实战解决方案
开发语言·c++
少司府2 小时前
C++进阶:map和set的使用
开发语言·数据结构·c++·容器·stl·set·map