前言
你有没有想过:网页上的实时聊天、股票行情推送、在线游戏,是怎么做到服务器主动给客户端发消息的?
HTTP是"一问一答":客户端问,服务器才能答。服务器不能主动推。
WebSocket解决了这个问题:建立一次连接后,双方可以随时互相发消息,真正的全双工通信。
今天我们用C语言从零实现WebSocket服务器:
· WebSocket协议帧解析
· 握手协议(HTTP升级)
· 数据帧编码/解码
· 完整的多客户端聊天室
· 支持文本和二进制消息
一、WebSocket核心原理
- 握手阶段(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=
```
- 数据帧格式
```
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 ... |
+---------------------------------------------------------------+
```
- 操作码(Opcode)
值 含义
0x0 连续帧
0x1 文本帧
0x2 二进制帧
0x8 连接关闭
0x9 Ping
0xA Pong
二、完整代码实现
- 基础结构
```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;
```
- 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;
}
```
- 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);
}
```
- 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;
}
```
- 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;
}
```
- 聊天室广播
```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;
}
```
- 服务器主循环
```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
```
测试
-
用浏览器打开 index.html
-
开多个标签页,模拟多用户聊天
五、WebSocket vs HTTP
特性 HTTP WebSocket
协议 请求-响应 全双工
服务器推送 不支持 支持
头部开销 每次请求都有 连接时一次
实时性 差(轮询) 好
适用场景 REST API、网页 聊天、游戏、推送
六、总结
通过这篇文章,你学会了:
· WebSocket协议的握手流程
· 数据帧格式和编解码
· 掩码处理和安全
· Ping/Pong心跳机制
· 完整的多客户端聊天室
WebSocket是实现实时通信的核心技术。掌握它,你就能构建自己的聊天服务器、游戏服务器、推送服务。
下一篇预告:《从零实现一个RPC框架:远程调用与服务治理》
评论区分享一下你打算用WebSocket做什么~