Websocket Echo Server Demo
背景
设备的应用开发大都依靠来完成,我去研究如何用实现websocket服务器也是为了在嵌入式设备中实现一个ip camera的功能,用户通过网页访问到嵌入式设备的摄像头以及音频,在学习的过程中先实现echo server是最基本的。
主要参考资源
具体实现
整个websocket从握手到数据传输帧头的格式不在这里展开,具体参考,在这里只介绍一下websocket echo server的实现。
- 头文件及宏定义
#include#include #include #include #include #include #include /*在握手时需要进行sha1编码和base64编码, 在这里用openssl的库来实现*/ #include #include #include #include #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
- 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
- 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
- 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
- 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在控制台中查看输出
- 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
开源代码: