用C语言实现websocket服务器
Websocket Echo Server Demo
背景
嵌入式设备的应用开发大都依靠C语言来完成,我去研究如何用c语言实现websocket服务器也是为了在嵌入式设备中实现一个ip camera的功能,用户通过网页访问到嵌入式设备的摄像头以及音频,在学习的过程中先实现echo server是最基本的。
主要参考资源
具体实现
整个websocket从握手到数据传输帧头的格式不在这里展开,具体参考编写 WebSocket 服务器——MDN,在这里只介绍一下websocket echo server的实现。
- 头文件及宏定义
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*在握手时需要进行sha1编码和base64编码,
在这里用openssl的库来实现*/
#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#define BUFFER_SIZE 1024
#define RESPONSE_HEADER_LEN_MAX 1024
#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 数据帧头
/*-----------为了便于理解,在这里吧数据帧格式粘出来-------------------
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 ... |
+---------------------------------------------------------------+
--------------------------------------------------------------------*/
typedef struct _frame_head {
char fin;
char opcode;
char mask;
unsigned long long payload_length;
char masking_key[4];
} frame_head;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 封装套接字函数
为了使套接字使用看起来简洁一些,封装一个被动套接字函数,只需要传入监听端口和监听队列个数就可以返回套接字描述符,调用者可以直接用这个描述符accept去接收客户端连接。
int passive_server(int port,int queue)
{
///定义sockfd
int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
///定义sockaddr_in
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(port);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
///bind,成功返回0,出错返回-1
if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
{
perror("bind");
exit(1);
}
///listen,成功返回0,出错返回-1
if(listen(server_sockfd,queue) == -1)
{
perror("listen");
exit(1);
}
printf("监听%d端口\n",port);
return server_sockfd;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- Base64编码函数
握手函数会用到
int base64_encode(char *in_str, int in_len, char *out_str)
{
BIO *b64, *bio;
BUF_MEM *bptr = NULL;
size_t size = 0;
if (in_str == NULL || out_str == NULL)
return -1;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_write(bio, in_str, in_len);
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bptr);
memcpy(out_str, bptr->data, bptr->length);
out_str[bptr->length-1] = '\0';
size = bptr->length;
BIO_free_all(bio);
return size;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 逐行读取函数
握手函数循环调用,每次获得一行字符串,返回下一行开始位置
/**
* @brief _readline
* read a line string from all buffer
* @param allbuf
* @param level
* @param linebuf
* @return
*/
int _readline(char* allbuf,int level,char* linebuf)
{
int len = strlen(allbuf);
for (;level<len;++level)
{
if(allbuf[level]=='\r' && allbuf[level+1]=='\n')
return level+2;
else
*(linebuf++) = allbuf[level];
}
return -1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 握手函数
负责处理新客户端的连接,接收客户端http格式的请求,从中获得Sec-WebSocket-Key对应的值,与魔法字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 进行连接后进行sha1 hash,再将结果(sha1的直接结果,不是转化为字符串后的结果)进行Base64编码。最后构造响应头部,发送响应,与客户端建立websocket连接。
int shakehands(int cli_fd)
{
//next line's point num
int level = 0;
//all request data
char buffer[BUFFER_SIZE];
//a line data
char linebuf[256];
//Sec-WebSocket-Accept
char sec_accept[32];
//sha1 data
unsigned char sha1_data[SHA_DIGEST_LENGTH+1]={0};
//reponse head buffer
char head[BUFFER_SIZE] = {0};
if (read(cli_fd,buffer,sizeof(buffer))<=0)
perror("read");
printf("request\n");
printf("%s\n",buffer);
do {
memset(linebuf,0,sizeof(linebuf));
level = _readline(buffer,level,linebuf);
//printf("line:%s\n",linebuf);
if (strstr(linebuf,"Sec-WebSocket-Key")!=NULL)
{
strcat(linebuf,GUID);
// printf("key:%s\nlen=%d\n",linebuf+19,strlen(linebuf+19));
SHA1((unsigned char*)&linebuf+19,strlen(linebuf+19),(unsigned char*)&sha1_data);
// printf("sha1:%s\n",sha1_data);
base64_encode(sha1_data,strlen(sha1_data),sec_accept);
// printf("base64:%s\n",sec_accept);
/* write the response */
sprintf(head, "HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade: websocket\r\n" \
"Connection: Upgrade\r\n" \
"Sec-WebSocket-Accept: %s\r\n" \
"\r\n",sec_accept);
printf("response\n");
printf("%s",head);
if (write(cli_fd,head,strlen(head))<0)
perror("write");
break;
}
}while((buffer[level]!='\r' || buffer[level+1]!='\n') && level!=-1);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 字符串反转函数
用于解决大端小端问题
void inverted_string(char *str,int len)
{
int i; char temp;
for (i=0;i<len/2;++i)
{
temp = *(str+i);
*(str+i) = *(str+len-i-1);
*(str+len-i-1) = temp;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 接收及存储数据帧头
调用者传一个数据帧头结构体指针用于获取解析后的帧头
解析过程依照MDN中说的结构解析就好。
int recv_frame_head(int fd,frame_head* head)
{
char one_char;
/*read fin and op code*/
if (read(fd,&one_char,1)<=0)
{
perror("read fin");
return -1;
}
head->fin = (one_char & 0x80) == 0x80;
head->opcode = one_char & 0x0F;
if (read(fd,&one_char,1)<=0)
{
perror("read mask");
return -1;
}
head->mask = (one_char & 0x80) == 0X80;
/*get payload length*/
head->payload_length = one_char & 0x7F;
if (head->payload_length == 126)
{
char extern_len[2];
if (read(fd,extern_len,2)<=0)
{
perror("read extern_len");
return -1;
}
head->payload_length = (extern_len[0]&0xFF) << 8 | (extern_len[1]&0xFF);
}
else if (head->payload_length == 127)
{
char extern_len[8];
if (read(fd,extern_len,8)<=0)
{
perror("read extern_len");
return -1;
}
inverted_string(extern_len,8);
memcpy(&(head->payload_length),extern_len,8);
}
/*read masking-key*/
if (read(fd,head->masking_key,4)<=0)
{
perror("read masking-key");
return -1;
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 去掩码函数
从客户端发来的数据是经过异或加密的,我们在解析帧头的时候获取到了掩码,我们通过掩码可以解码出原数据。
/**
* @brief umask
* xor decode
* @param data 传过来时为密文,解码后的明文同样存储在这里
* @param len data的长度
* @param mask 掩码
*/
void umask(char *data,int len,char *mask)
{
int i;
for (i=0;i<len;++i)
*(data+i) ^= *(mask+(i%4));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 发送数据帧头
int send_frame_head(int fd,frame_head* head)
{
char *response_head;
int head_length = 0;
if(head->payload_length<126)
{
response_head = (char*)malloc(2);
response_head[0] = 0x81;
response_head[1] = head->payload_length;
head_length = 2;
}
else if (head->payload_length<0xFFFF)
{
response_head = (char*)malloc(4);
response_head[0] = 0x81;
response_head[1] = 126;
response_head[2] = (head->payload_length >> 8 & 0xFF);
response_head[3] = (head->payload_length & 0xFF);
head_length = 4;
}
else
{
response_head = (char*)malloc(12);
response_head[0] = 0x81;
response_head[1] = 127;
memcpy(response_head+2,head->payload_length,sizeof(unsigned long long));
inverted_string(response_head+2,sizeof(unsigned long long));
head_length = 12;
}
if(write(fd,response_head,head_length)<=0)
{
perror("write head");
return -1;
}
free(response_head);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 主函数
接收一个连接并循环回射。
int main()
{
int ser_fd = passive_server(4444,20);
struct sockaddr_in client_addr;
socklen_t addr_length = sizeof(client_addr);
int conn = accept(ser_fd,(struct sockaddr*)&client_addr, &addr_length);
shakehands(conn);
while (1)
{
frame_head head;
int rul = recv_frame_head(conn,&head);
if (rul < 0)
break;
// printf("fin=%d\nopcode=0x%X\nmask=%d\npayload_len=%llu\n",head.fin,head.opcode,head.mask,head.payload_length);
//echo head
send_frame_head(conn,&head);
//read payload data
char payload_data[1024] = {0};
int size = 0;
do {
int rul;
rul = read(conn,payload_data,1024);
if (rul<=0)
break;
size+=rul;
umask(payload_data,size,head.masking_key);
printf("recive:%s",payload_data);
//echo data
if (write(conn,payload_data,rul)<=0)
break;
}while(size<head.payload_length);
printf("\n-----------\n");
}
close(conn);
close(ser_fd);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 客户端测试用例:
将以下代码保存为websocket_client.html,用Chrome浏览器打开测试。
代码中console.log是在浏览器中按F12在控制台中查看输出
<button onclick="svc_connectPlatform()"> connect</button>
<button onclick="svc_send('hello web')"> send</button>
<script>
function svc_connectPlatform() {
//alert("");
var wsServer = 'ws://192.168.25.157:4444/';
try {
svc_websocket = new WebSocket(wsServer);
} catch (evt) {
console.log("new WebSocket error:" + evt.data);
svc_websocket = null;
if (typeof(connCb) != "undefined" && connCb != null)
connCb("-1", "connect error!");
return;
}
//alert("");
svc_websocket.onopen = svc_onOpen;
svc_websocket.onclose = svc_onClose;
svc_websocket.onmessage = svc_onMessage;
svc_websocket.onerror = svc_onError;
}
function svc_onOpen(evt) {
console.log("Connected to WebSocket server.");
}
function svc_onClose(evt) {
console.log("Disconnected");
}
function svc_onMessage(evt) {
console.log('Retrieved data from server: ' + evt.data);
}
function svc_onError(evt) {
console.log('Error occured: ' + evt.data);
}
function svc_send(msg) {
if (svc_websocket.readyState == WebSocket.OPEN) {
svc_websocket.send(msg);
} else {
console.log("send failed. websocket not open. please check.");
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
开源代码:https://github.com/lhc3538/my-websocket-server
用C语言实现websocket服务器的更多相关文章
- 根据Unix哲学来编写你的HTML5 Websocket服务器来实现全双工通信
websocketd代表WebSocket的守护进程 websocketd处理的是浏览器和服务器之间的WebSocket连接,它会启动你所指定的服务器端应用来对WebSockets进行处理,然后在浏览 ...
- 实现一个websocket服务器-理论篇
本文是Writing WebSocket servers的中文文档,翻译自MDNWriting WebSocket servers.篇幅略长,个人能力有限难免有所错误,抛砖引玉共同进步. websoc ...
- 如何实现websocket服务器-理论篇
WebSocket 服务器简单来说就是一个遵循特殊协议监听服务器任意端口的tcp应用.搭建一个定制服务器的任务通常会让让人们感到害怕.然而基于实现一个简单的Websocket服务器没有那么麻烦. 一个 ...
- HTTPS请求HTTP接口被浏览器阻塞,python实现websocket客户端,websocket服务器,跨域问题,dwebsocket,https,拦截,服务端
HTTPS请求HTTP接口被浏览器阻塞,python实现websocket客户端,websocket服务器,跨域问题,dwebsocket,https,拦截,服务端 发表时间:2020-03-05 1 ...
- HTML5学习总结-08 WebSocket 服务器推送
一 WebSocket 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展 ...
- python实现websocket服务器,可以在web实时显示远程服务器日志
一.开始的话 使用python简单的实现websocket服务器,可以在浏览器上实时显示远程服务器的日志信息. 之前做了一个web版的发布系统,但没实现在线看日志,每次发布版本后,都需要登录到服务器上 ...
- node实现一个WEBSOCKET服务器
早点时候翻译了篇实现一个websocket服务器-理论篇,简单介绍了下理论基础,本来打算放在一起,但是感觉太长了大家可能都看不下去.不过发现如果拆开的话,还是不可避免的要提及理论部分.用到的地方就简要 ...
- Erlang cowboy websocket 服务器
Erlang cowboy websocket 服务器 原文见于: http://marcelog.github.io/articles/erlang_websocket_server_cowboy_ ...
- 【Netty】(7)---搭建websocket服务器
[Netty](7)---搭建websocket服务器 说明:本篇博客是基于学习某网有关视频教学. 目的:创建一个websocket服务器,获取客户端传来的数据,同时向客户端发送数据 一.服务端 1. ...
随机推荐
- 数据库的ACID 简谈
一.事务 定义:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位. 准备工作:为了说明事务的ACID原理,我们使用银行账户及资金管理的案例进行分析. 二.ACI ...
- thinkphp3.2使用PHPQrcode实现二维码
Thinkphp中没有二维码相关的生成库,百度有不少工具和库 这里就实例一下通过think3.2搭配phpqrcode来完成生成二维码的功能. 至于phpQrcode库文件 百度很容易找到这里也给大家 ...
- 【C++】使用find函数快速定位元素
当有了STL,你还在用遍历的土方法定位元素吗? 今天就来介绍一下,如何使用algorithm库里的find函数快速定位数组或向量中的元素. 首先当然要包含头文件: #include <algor ...
- 将文件大小kb转换成M
得到文件的大小的一般是直接到得到的是文件的字节大小,也就是kb,我们有的时候需要做单位换算成B或者M, 下面方法只是换成M,没有到G, 有更好的方法,请随时沟通,以便交流学习,谢谢. public s ...
- 第一个web项目
1) 创建Java Web Project 2) 创建相应的包 3) 创建类并继承于HttpServlet 4) 重写service()方法 5) ...
- PS学习笔记(05)
PS学习笔记(09) [2]马赛克背景 找一张图片.然后新建图层,让前景色背景色恢复到默认的状态(黑.白) 在新建图层上填充黑色-->滤镜-->渲染->云彩 像素化-->马赛克 ...
- C51 中断 个人笔记
总架构图 IE寄存器 控制各个中断源的屏蔽与允许 TCON寄存器 各个中断源的请求标志位&有效信号的规定 中断源及其优先级 中断号写程序的时候要用 CPU处理中断三原则 1.CPU同时接收到几 ...
- ajax导出excel文件并增加等待动画效果
html: <button class="btn btn-default" onclick="logToExcel('{:url('userLogToExcel', ...
- * SPOJ PGCD Primes in GCD Table (需要自己推线性筛函数,好题)
题目大意: 给定n,m,求有多少组(a,b) 0<a<=n , 0<b<=m , 使得gcd(a,b)= p , p是一个素数 这里本来利用枚举一个个素数,然后利用莫比乌斯反演 ...
- 【BZOJ4403】序列统计(Lucas定理,组合计数)
题意:给定三个正整数N.L和R, 统计长度在1到N之间,元素大小都在L到R之间的单调不降序列的数量. 输出答案对10^6+3取模的结果. 对于100%的数据,1≤N,L,R≤10^9,1≤T≤100, ...