1. 背景
在WebSocket协议出现之前,基于HTTP协议的Web客户端和服务器在需要进行双向通信的时候,往往要求客户端实现基于轮询的方式不断向服务器发送请求以查询最新的状态信息,这些方式包括轮询和长轮询。
1. 轮询(Polling):在轮询方式中,客户端通过定期向服务器发送HTTP请求来查询新的数据或状态更新。服务器在接收到请求后,检查是否有新的数据可用,并将其返回给客户端。如果没有新数据可用,服务器将保持请求挂起,直到有数据可返回或达到超时时间。一旦客户端收到响应,它会立即发送下一个请求。
2. 长轮询(Long Polling):长轮询是一种改进的轮询方式。在这种方式中,客户端发送一个HTTP请求到服务器,服务器保持请求挂起,直到有新的数据可用或达到超时时间。如果有新数据可用,服务器会立即返回响应给客户端。如果没有新数据可用,服务器将保持连接打开,直到有数据可返回或达到超时时间。
这两种方式的共同缺点是高延迟和频繁的请求和响应。由于每次通信都需要发送HTTP请求和等待响应,因此会产生较大的开销和网络流量。此外,由于HTTP是无状态的协议,每个请求都需要携带所有必要的头部信息,这也会增加数据传输的负载。
另外,轮询和长轮询方式也存在一些问题,如:
1. 频繁的请求:由于客户端需要定期发送请求来获取更新,这会导致不必要的网络流量和服务器负载。
2. 延迟:由于轮询和长轮询方式无法实时地传输数据,而是需要等待下一个轮询周期或超时时间,因此会引入一定的延迟。
3. 服务器资源占用:由于每个客户端都需要保持一个或多个打开的连接,服务器需要为每个连接分配资源,这可能导致服务器资源的浪费和限制并发连接数。
WebSocket的出现解决了这些问题,通过提供持久化的双向通信连接,减少了延迟和网络流量,并提供了更高效的实时通信机制。
2. WebSocket协议的特点
WebSocket是一种在Web应用程序中实现双向通信的协议。它提供了一种持久化的连接,允许服务器和客户端之间实时地交换数据。
以下是WebSocket协议的一些关键特点:
-
建立连接:WebSocket协议使用HTTP协议进行握手,并在握手成功后建立持久化的连接。这意味着一旦连接建立,服务器和客户端可以直接通过该连接进行通信,而无需每次请求都重新建立连接。
-
双向通信:WebSocket协议支持全双工通信,即服务器和客户端可以同时发送和接收数据。这种实时的双向通信使得WebSocket非常适合实时应用程序,如聊天应用、实时游戏和股票市场报价等。
-
低延迟:由于WebSocket使用持久化连接,相比传统的HTTP请求-响应模式,它可以减少延迟。这是因为在WebSocket连接上,数据可以立即传输,而无需等待每次请求的响应。
-
跨域支持:WebSocket协议支持跨域通信,允许在不同域名下的服务器和客户端之间进行通信。
-
二进制和文本数据传输:WebSocket协议支持传输二进制和文本数据。这使得它非常灵活,可以用于传输各种类型的数据,例如图像、音频、视频等。
-
心跳机制:WebSocket协议支持心跳机制,用于检测连接的健康状态。服务器和客户端可以定期发送心跳消息以确保连接处于活动状态。
3. 协议概览
WebSocket协议由握手协议和数据传输协议两部分组成。
客户端发出的握手协议报文看起来大致是这样子的:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
服务器发出的握手协议报文看起来大致是这样子的:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
握手成功之后,客户端和服务器就可以以帧的形式传输数据了。
4. 部分细节
1. 为了兼容现有HTTP服务器,WebSocket以一个HTTP Upgrade请求开始握手过程,使用的是GET方法。
2. WebSocket协议默认端口是80和443。
3. 关键头域Sec-WebSocket-Key:客户端必须随机产生一个长度为16字节的以Base64编码的字符串作为它的值,每个链接都使用不同的值,主要作安全校验之用。
4. 关键头域Sec-WebSocket-Accept:这是服务器收到客户端的握手报文后,返回给客户端的响应报文里面携带的头域。服务器先获取了Sec-WebSocket-Key的值,然后把它和“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接起来,并取其SHA-1哈希值后再做Base64编码,客户端也可以根据这个来对响应报文进行安全校验。
5. 头域Sec-WebSocket-Version的值必须是13.
6. WebSocket协议报文分为控制帧和非控制帧。目前定义的控制帧由Close帧、Ping帧、Pong帧。控制帧不支持报文分片(fragmentation)。非控制帧(即数据帧)包括Text帧和Binary帧。
7. 所有从客户端发往服务器的帧都必须经一个32bit的masking-key做掩码操作,目的是为了防止某些代理服务器或者中间网络设备误以为这是一个常规的HTTP报文而对其进行错误的处理。
8. 预定义的状态码:1000 - 1015, 其中1000为正常关闭指示状态码。
5. libwebsockets
libwebsockets是一个轻量级的C库,用于实现WebSocket协议和其他相关协议的服务器和客户端。它提供了一个简单、高效的方式来构建基于WebSocket的应用程序。
以下是libwebsockets库的一些关键特点:
-
跨平台支持:libwebsockets可以在多个平台上使用,包括Linux、Windows、Mac OS等。它提供了与操作系统无关的API,使得应用程序可以在不同平台上进行移植。
-
完整的WebSocket支持:libwebsockets实现了WebSocket协议的完整功能,包括握手、数据传输、关闭连接等。它支持文本和二进制数据传输,并提供了WebSocket扩展的支持。
-
多协议支持:除了WebSocket,libwebsockets还支持其他协议,如HTTP、HTTPS、MQTT等。这使得它能够适应不同类型的应用需求,并在同一个应用程序中处理多种协议。
-
异步和多线程支持:libwebsockets提供了异步和多线程的特性,允许同时处理多个连接和请求。这使得应用程序可以高效地处理大量并发连接,实现高性能和可扩展性。
-
SSL/TLS支持:libwebsockets支持通过SSL/TLS进行安全的WebSocket和HTTP连接。它提供了对OpenSSL和mbedTLS等常见加密库的集成,以确保数据的安全性和保密性。
-
灵活的事件驱动模型:libwebsockets使用事件驱动的编程模型,应用程序可以通过回调函数处理不同类型的事件,如新连接、接收数据、发送数据等。这种灵活性使得开发者可以根据应用程序的需求来处理各种事件。
libwebsockets是一个功能丰富、易于使用的WebSocket库,适用于构建实时通信和即时应用程序。它已经得到广泛使用,并在许多项目和框架中被成功应用,包括实时聊天应用、实时数据传输、游戏服务器等。
6. 在Ubuntu环境编译libwebsockets
1. 获取代码
git clone https://github.com/warmcat/libwebsockets.git
2. 准备编译环境
sudo apt install build-essential cmake libssl-dev
3. 编译
cd <path to libwebsockets>
mkdir build
cd build
cmake -S .. -B .
make
4. 最后附上一个简单的基于libwebsockets的客户端程序
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <openssl/ssl.h>
5 #include <libwebsockets.h>
6
7 #ifndef LWS_PROTOCOL_LIST_TERM
8 #define LWS_PROTOCOL_LIST_TERM {NULL, NULL, 0, 0, 0, NULL, 0}
9 #endif
10
11 static int interrupted = 0;
12
13 static void sigint_handler(int sig)
14 {
15 if (sig != SIGINT)
16 return;
17 interrupted = 1;
18 }
19
20 static int client_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
21 {
22 #if defined(LWS_WITH_TLS)
23 union lws_tls_cert_info_results ci;
24 #if defined(LWS_HAVE_CTIME_R) && !defined(LWS_WITH_NO_LOGS)
25 char date[32];
26 #endif
27 #endif
28 switch (reason)
29 {
30 case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
31 lwsl_notice("connection error\n");
32 break;
33 case LWS_CALLBACK_CLIENT_ESTABLISHED:
34 lwsl_notice("connection success\n");
35 break;
36 case LWS_CALLBACK_CLOSED:
37 lwsl_notice("connection closed\n");
38 break;
39 case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
40 break;
41 case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
42 lwsl_notice("lws_http_client_http_response %d\n",
43 lws_http_client_http_response(wsi));
44 #if defined(LWS_WITH_TLS)
45 if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_COMMON_NAME,
46 &ci, sizeof(ci.ns.name)))
47 lwsl_notice(" Peer Cert CN : %s\n", ci.ns.name);
48
49 if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_ISSUER_NAME,
50 &ci, sizeof(ci.ns.name)))
51 lwsl_notice(" Peer Cert issuer : %s\n", ci.ns.name);
52
53 if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_VALIDITY_FROM,
54 &ci, 0))
55 #if defined(LWS_HAVE_CTIME_R)
56 lwsl_notice(" Peer Cert Valid from: %s",
57 ctime_r(&ci.time, date));
58 #else
59 lwsl_notice(" Peer Cert Valid from: %s",
60 ctime(&ci.time));
61 #endif
62 if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_VALIDITY_TO,
63 &ci, 0))
64 #if defined(LWS_HAVE_CTIME_R)
65 lwsl_notice(" Peer Cert Valid to : %s",
66 ctime_r(&ci.time, date));
67 #else
68 lwsl_notice(" Peer Cert Valid to : %s",
69 ctime(&ci.time));
70 #endif
71 if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_USAGE,
72 &ci, 0))
73 lwsl_notice(" Peer Cert usage bits: 0x%x\n", ci.usage);
74 #endif
75 break;
76 case LWS_CALLBACK_CLIENT_RECEIVE:
77 lwsl_hexdump_notice(in, len);
78 break;
79 case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
80 X509 *cert;
81 char buf[256];
82 X509_STORE_CTX *ctx = (X509_STORE_CTX *)user;
83
84 cert = X509_STORE_CTX_get_current_cert(ctx);
85 X509_NAME_oneline(X509_get_subject_name(cert), buf, 256);
86 lwsl_notice("subject name: %s\n", buf);
87 X509_NAME_oneline(X509_get_issuer_name(cert), buf, 256);
88 lwsl_notice("SSL issuer name: %s\n", buf);
89 break;
90 default:
91 break;
92 }
93
94 return 0;
95 }
96
97 static const struct lws_protocols protocols[] = {
98 {
99 "lws-minimal-client",
100 client_callback,
101 0,
102 0,
103 0,
104 NULL,
105 0
106 },
107 LWS_PROTOCOL_LIST_TERM
108 };
109
110 int main(void)
111 {
112 int ret = 0;
113 struct sigaction client_sigact;
114 struct lws_context_creation_info info;
115 struct lws_context *context;
116 struct lws_client_connect_info conn_info = { 0 };
117
118 client_sigact.sa_handler = sigint_handler;
119 sigemptyset(&client_sigact.sa_mask);
120 client_sigact.sa_flags = 0;
121
122 if (sigaction(SIGINT, &client_sigact, NULL))
123 {
124 perror("can't register signal handler for SIGINT\n");
125 exit(-1);
126 }
127
128 memset(&info, 0, sizeof(info));
129 info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
130 info.port = CONTEXT_PORT_NO_LISTEN;
131 info.protocols = protocols;
132
133 lws_set_log_level(0x0FFF, NULL);
134 context = lws_create_context(&info);
135 if (!context)
136 {
137 lwsl_err("lws init failed\n");
138 exit(-1);
139 }
140
141 conn_info.context = context;
142 conn_info.port = 443;
143 conn_info.address = "libwebsockets.org";
144 conn_info.path = "/";
145 conn_info.host = conn_info.address;
146 conn_info.origin = conn_info.address;
147 conn_info.ssl_connection = LCCSCF_USE_SSL;
148 conn_info.protocol = "dumb-increment-protocol";
149 conn_info.local_protocol_name = "lws-minimal-client";
150
151 if (!lws_client_connect_via_info(&conn_info))
152 {
153 lwsl_err("lws connect to client failed\n");
154 lws_context_destroy(context);
155 exit(-1);
156 }
157
158 while (!interrupted)
159 {
160 lws_service(context, 100);
161 }
162
163 lws_context_destroy(context);
164
165 return ret;
166 }
167
参考资料:
1. RFC 6455 - The WebSocket Protocol (ietf.org)
2. libwebsockets.org lightweight and flexible C networking library