用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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值