后端-C-使用C语言实现一个websocket

        最近自己心血来潮想学习一下C语言,感觉C语言是不会过时的,所以爬坑开始。自己也写了段时间的游戏客户端所以想以一个游戏的方式来学习服务端,考虑到要和客户端交互所以第一件事就是先写一个websocket来进行长连接。奈何网上搜索了很多资料关于C语言来实现websocket的寥寥无几,更多的是泛泛而谈亦或是提供的源码运行环境复杂更或者是根本运行不起来。一怒之下本人放弃了~~~~

        两个月后心血来潮不能这么就算了,索性静下心先看了某个老师的教学视频(视频是花了19块钱某宝买来的,就我一个买家[笑抽]),在看这个视频时确实感觉讲的很不错,视频是某培训机构的(报名人数堪忧)。本人也很怀疑为什么这么好的教程没有人观看所以要是读者大大也想要学习的,评论联系本人就行

        当然视频中将了大量的基于Windows开发的select和IOCP相关的但是寻其根本道理是一样的。之后为了让代码能在linux上正常运行本人又去B站找了下epoll相关的视频看了下。结合这两者初步搭建了一个还算干净整洁能跑起来且运行方式简单的websocket服务(中途又去浅浅的了解了下makefile[菜鸟级别,有好的教程也请读者们评论推荐 ps:最好是视频,本人不喜欢看书^_^!])。

         在写代码之前先设计一下有哪些东西,第一 服务启动入口; 第二 建立普通的socket;第三 建立websocket;第四 session的管理;第五 三方库;ok开始建立目录结构。其中的红色框部分是我网上找来的一些工具用于加密和http请求的解析等等。

后端-C-使用C语言实现一个websocket_第1张图片

 我们从main.c开始讲,他的功能就是调用方法开启一个端口的监听,上代码

#include 
#include 
#include 
#include "./tcp/tcp_epoll.h"

int main(int argc,const char* argv[]){
	//启动
	start_server(9527);
	return 0;
}

这里调用了start_server()方法,也就是开启监听建立socket,因为我喜欢入口要很干净所以我写了tcp_epoll来实现这个功能

tcp_epoll.h

#ifndef __TCP_EPOLL_H__
#define __TCP_EPOLL_H__

/**启动一个服务*/
void start_server(int port);

#endif

 tcp_epoll.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "./tcp_epoll.h"
#include "./tcp_session.h"
#include "./tcp_ws.h"


void start_server(int port){
	printf("启动服务 port: %d\n", port);
	//初始化session管理
	init_session_manager();	
	struct sockaddr_in serv_addr;
	socklen_t serv_len = sizeof(serv_addr);
	//创建套接字
	int lfd = socket(AF_INET,SOCK_STREAM,0);
	//初始化socket_in
	memset(&serv_addr,0,serv_len);
	serv_addr.sin_family = AF_INET;					//地址族
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);	//监听本机所有的IP
	serv_addr.sin_port = htons(port); 				//设置端口
 
	//绑定端口和IP
	bind(lfd,(struct sockaddr*)&serv_addr,serv_len);
 
	//设置同时监听的最大个数
	listen(lfd,128);
	printf("start accept ...\n");
 
	struct sockaddr_in client_addr;
	socklen_t cli_len = sizeof(client_addr);
 
	//创建epoll树根节点
	int epfd = epoll_create(2000);
	//初始化epoll树
	struct epoll_event ev;
	ev.events = EPOLLIN;	
	ev.data.fd = lfd;
	epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
	struct epoll_event all[2000];
	while(1){
		clear_offline_session();
		//使用epoll通知内核fd文件IO检测
		int ret = epoll_wait(epfd,all,sizeof(all)/sizeof(all[0]),-1);
		int i;
		for(i=0;i0){
					if (s->is_shake_hand == 0){
						process_ws_shake_hand(s->c_sock, buf);
						s->is_shake_hand = 1;
					}else{
						//解析数据
						char msg[8196] = {0};
						int ret = on_ws_recv_data(s, (unsigned char*)buf, len, (char*)&msg);
						if(ret){
							//解析成功后处理数据
							ws_send_data(s, (unsigned char*)msg, strlen(msg));
						}
					}
				}
				if(len == -1){
					if(errno != EAGAIN){
						perror("recv error");
						exit(1);
					}
				}else if(len ==0){
					//客户端关闭
					//下树
					ret = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
					if(ret == -1){
						perror("epoll_ctl - del error");
						exit(1);
					}
					close_session(s);
				}
			}
		}
	}
	close(lfd);
	exit_session_manager();
}

好了我们要将socket变成websocket所以我又创建了tcp_ws来实现这个功能(毕竟强迫症)

tcp_ws.h

#ifndef __TCP_WS_H__
#define __TCP_WS_H__

/**握手协议*/
void process_ws_shake_hand(int sock, char* http_str);

/**发送信息给客户端*/
void ws_send_data(struct session* s, unsigned char* pkg_data, unsigned int pkg_len);

/**解析包*/
int on_ws_recv_data(struct session* s, unsigned char*pkg_data, int pkg_len, char* msg);

#endif

 tcp_ws.c

#include 
#include 
#include 
#include 
#include 
#include 

#include "./tcp_session.h"
#include "./tcp_ws.h"

#include "../3rd/http_parser/http_parser.h"
#include "../3rd/crypt/sha1.h"
#include "../3rd/crypt/base64_encoder.h"

static char header_key[64];
static char client_ws_key[128];

char *wb_accept = "HTTP/1.1 101 Switching Protocols\r\n" \
				  "Upgrade:websocket\r\n" \
				  "Connection: Upgrade\r\n" \
				  "Sec-WebSocket-Accept: %s\r\n" \
				  "WebSocket-Location: ws://%s:%d/chat\r\n" \
				  "WebSocket-Protocol:chat\r\n\r\n";


static int on_header_field(http_parser* p, const char *at,size_t length) {
	strncpy(header_key, at, length);
	header_key[length] = 0;
	return 0;
}
 
static int on_header_value(http_parser* p, const char *at,size_t length) {
	if (strcmp(header_key, "Sec-WebSocket-Key") != 0) {
		return 0;
	}
	strncpy(client_ws_key, at, length);
	client_ws_key[length] = 0;
	return 0;
}

//握手
void process_ws_shake_hand(int sock, char* http_str) {
	http_parser p;
	http_parser_init(&p, HTTP_REQUEST);
 
	http_parser_settings s;
	http_parser_settings_init(&s);
	s.on_header_field = on_header_field;
	s.on_header_value = on_header_value;
 
	http_parser_execute(&p, &s, http_str, strlen(http_str));
 
	// 回一个http的数据给我们的client,建立websocket链接
	static char key_migic[256];
	const char* migic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
	sprintf(key_migic, "%s%s", client_ws_key, migic);
 
	int sha1_size = 0; // 存放加密后的数据长度
	int base64_len = 0;
	char* sha1_content = crypt_sha1((uint8_t*)key_migic, strlen(key_migic), &sha1_size);
	char* b64_str = base64_encode((uint8_t*)sha1_content, sha1_size, &base64_len);
 
	strncpy(key_migic, b64_str, base64_len);
	key_migic[base64_len] = 0;
 
	// 将这个http的报文回给我们的websocket连接请求的客户端,
	// 生成websocket连接。
	static char accept_buffer[256];
	sprintf(accept_buffer, wb_accept, key_migic, "xxx.xxx.xxx.xxx", xxx);
	send(sock, accept_buffer, strlen(accept_buffer), 0);
}


//发送数据(没有mask)
void ws_send_data(struct session* s, unsigned char* pkg_data, unsigned int pkg_len) {
	static unsigned char send_buffer[8196];
	unsigned int send_len;
	// 固定的头
	send_buffer[0] = 0x81;
	if (pkg_len <= 125) {
		send_buffer[1] = pkg_len; // 最高bit为0,
		send_len = 2;
	}
	else if (pkg_len <= 0xffff) {                                                               
		send_buffer[1] = 126;
		send_buffer[2] = (pkg_len & 0x000000ff);
		send_buffer[3] = ((pkg_len & 0x0000ff00) >> 8);
		send_len = 4;
	}
	else { 
		send_buffer[1] = 127;
		send_buffer[2] = (pkg_len & 0x000000ff);
		send_buffer[3] = ((pkg_len & 0x0000ff00) >> 8);
		send_buffer[4] = ((pkg_len & 0x00ff0000) >> 16);
		send_buffer[5] = ((pkg_len & 0xff000000) >> 24);
 
		send_buffer[6] = 0;
		send_buffer[7] = 0;
		send_buffer[8] = 0;
		send_buffer[9] = 0;
		send_len = 10;
	}
	memcpy(send_buffer + send_len, pkg_data, pkg_len);
	send_len += pkg_len;
	send(s->c_sock, send_buffer, send_len, 0);
}

/**解析包*/
int on_ws_recv_data(struct session* s, unsigned char*pkg_data, int pkg_len, char* msg) {
	int ret =0;
	// 第一个字节是头,已经判断,跳过;
	unsigned char* mask = NULL;
	unsigned char* raw_data = NULL;
	unsigned int len = pkg_data[1];
	// 最高的一个bit始终为1,我们要把最高的这个bit,变为0;
	len = (len & 0x0000007f);
	if (len <= 125) {
		mask = pkg_data + 2; // 头字节,长度字节
	}
	else if (len == 126) { // 后面两个字节表示长度;
		len = ((pkg_data[2]) | (pkg_data[3] << 8));
		mask = pkg_data + 2 + 2;
	}
	else if (len == 127){ // 这种情况不用考虑,考虑前4个字节的大小,后面不管;
		unsigned int low = ((pkg_data[2]) | (pkg_data[3] << 8) | (pkg_data[4] << 16) | (pkg_data[5] << 24));
		unsigned int hight = ((pkg_data[6]) | (pkg_data[7] << 8) | (pkg_data[8] << 16) | (pkg_data[9] << 24));
		if (hight != 0) { // 表示后四个字节有数据int存放不了,太大了,我们不要了。
			return ret;
		}
		len = low;
		mask = pkg_data + 2 + 8;
	}
	// mask 固定4个字节,所以后面的数据部分
	raw_data = mask + 4;
	// 还原我们的发送过来的数据;
	// 从原始数据的第0个字节开始,然后,每个字节与对应的mask进行异或,得到真实的数据。
	// 由于mask只有4个字节,所以mask循环重复使用;(0, 1, 2, 3, 0, 1, 2, 3);
	static unsigned char recv_buf[8096];
	unsigned int i;
	for (i = 0; i < len; i++) {
		recv_buf[i] = raw_data[i] ^ mask[i % 4]; // mask只有4个字节的长度,所以,要循环使用,如果超出,取余就可以了。
	}
	recv_buf[len] = 0;
	int head = pkg_data[0];
	// if(pkg_data[0]==0x81 || pkg_data[0]==0x82){
	if(head==129 || head==130){
		printf("%s:%d recv data:%s\n",s->c_ip,s->c_port,recv_buf);
		memcpy(msg, recv_buf, len);
		ret = 1;
	}else{
		printf("the head is not : 0x81 or 0x82\n");
	}
	return ret;
}

这样websocket就建立好了。要考虑到链接会有很多所以对每个连接需要集体的管理,又因为自己的强迫症所以有分了一个tcp_session来专门管理

 tcp_session.h

#ifndef __TCP_SESSION_H__
#define __TCP_SESSION_H__

struct session {
	char c_ip[32];
	int c_port;
	int c_sock;
	int removed;
	int is_shake_hand;
	struct session* next;
	
};
 
void init_session_manager();
void exit_session_manager();
 
 
/**有客服端进来,保存这个sesssion*/
struct session* save_session(int c_sock, const char* ip, int port);

/**根据句柄获取会话*/
struct session* get_session(int c_sock);

/**关闭一个会话*/
void close_session(struct session* s);
 
/**遍历我们session集合里面的所有session*/
void foreach_online_session(int(*callback)(struct session* s, void* p), void*p);

/**清理下线的会话*/
void clear_offline_session();

#endif

tcp_session.c

#include 
#include 
#include 
#include 
#include 
#include 
#include "./tcp_session.h"

#define MAX_SESSION_NUM 6000
#define my_malloc malloc
#define my_free free
 
#define MAX_RECV_BUFFER 8096
 
struct {
	struct session* online_session;
 
	struct session* cache_mem;
	struct session* free_list;
 
 
	char recv_buffer[MAX_RECV_BUFFER];
	int readed; // 当前已经从socket里面读取的数据;
 
	int has_removed;
	int prot_mode; // 0 表示二进制协议,size + 数据的模式
	               // 1,表示文本协议,以回车换行来分解收到的数据为一个包
}session_manager;
 
static struct session* cache_alloc() {
	struct session* s = NULL;
	if (session_manager.free_list != NULL) {
		s = session_manager.free_list;
		session_manager.free_list = s->next;
	}
	else { // 调用系统的函数 malloc
		s = my_malloc(sizeof(struct session));
	}
	memset(s, 0, sizeof(struct session));
 
	return s;
}
 
//释放一个会话 
static void cache_free(struct session* s) {
	// 判断一下,是从cache分配出去的,还是从系统my_malloc分配出去的?
	if (s >= session_manager.cache_mem && s < session_manager.cache_mem + MAX_SESSION_NUM) {
		s->next = session_manager.free_list;
		session_manager.free_list = s;
	}
	else { 
		my_free(s);
	}
}

/**初始化会话管理*/
void init_session_manager() {
	memset(&session_manager, 0, sizeof(session_manager));
	// 将6000个session一次分配出来。
	session_manager.cache_mem = (struct session*)my_malloc(MAX_SESSION_NUM * sizeof(struct session));
	memset(session_manager.cache_mem, 0, MAX_SESSION_NUM * sizeof(struct session));
	int i;
	for (i = 0; i < MAX_SESSION_NUM; i++) {
		session_manager.cache_mem[i].next = session_manager.free_list;
		session_manager.free_list = &session_manager.cache_mem[i];
	}
}
 
void exit_session_manager() {
	//退出所有会话
}

//保存一个会话 
struct session* save_session(int c_sock, const char* ip, int port) {
	struct session* s = cache_alloc();
	s->c_sock = c_sock;
	s->c_port = port;
	s->removed = 0;
	int len = strlen(ip);
	if (len >= 32) {
		len = 31;
	}
	strncpy(s->c_ip, ip, len);
	s->c_ip[len] = 0;
	s->next = session_manager.online_session;
	session_manager.online_session = s;
	printf("client %s:%d connected...\n", s->c_ip, s->c_port);
	return s;
}

/**遍历各个会话并且调用函数*/ 
void foreach_online_session(int(*callback)(struct session* s, void* p), void*p) {
	if (callback == NULL) {
		return;
	}
	struct session* walk = session_manager.online_session;
	while (walk) {
		if (walk->removed == 1) {
			walk = walk->next;
			continue;
		}
		if (callback(walk, p)) {
			return;
		}
		walk = walk->next;
	}
}

struct session* get_session(int c_sock){
	struct session* walk = session_manager.online_session;
	while (walk) {
		if (walk->removed == 1) {
			walk = walk->next;
			continue;
		}
		if(walk->c_sock == c_sock){
			return walk;
		}
		walk = walk->next;
	}
	return NULL;
}
 
void close_session(struct session* s) {
	s->removed = 1;
	s->is_shake_hand = 0;
	session_manager.has_removed = 1;
	printf("client %s:%d exit\n", s->c_ip, s->c_port);
}
 
void clear_offline_session() {
	if (session_manager.has_removed == 0) {
		return;
	}
	struct session** walk = &session_manager.online_session;
	while (*walk) {
		struct session* s = (*walk);
		if (s->removed) {
			*walk = s->next;
			s->next = NULL;
			close(s->c_sock);
			s->c_sock = 0;
			// 释放session
			cache_free(s);
		}
		else {
			walk = &(*walk)->next;
		}
	}
	session_manager.has_removed = 0;
}

这样一来就差不多完成了,随手搞个页面调试一下

index.html



   
   
   菜鸟教程(runoob.com)
    
      
        
   
   
      
      
   

OK打完收工,还有个makefile(菜鸟级别的有时间我再学了别见谅)

CC = gcc
LIB = -L/
INC = -I./
CXXFLAGS = -Wall
 
TARGET=mana-svr
 
SRC=$(wildcard ./*.c ./tcp/*.c ./3rd/crypt/*.c ./3rd/http_parser/*.c)
 
OBJ=$(patsubst %.c, %.o, $(SRC))
 
$(TARGET): $(OBJ)
	$(CC) $(CXXFLAGS) -o $@ $^ $(LIB)

$(OBJ):%.o: %.c
	$(CC) $(CXXFLAGS)  $(INC) -o $@ -c $<
	
clean:
	rm -f *.o ./tcp/*.o ./3rd/crypt/*.o ./3rd/http_parser/*.o

clean-all:
	rm -f *.o ./tcp/*.o ./3rd/crypt/*.o ./3rd/http_parser/*.o
	rm -f $(TARGET)

最后附上项目下载地址:https://download.csdn.net/download/qq_35099224/11491055

你可能感兴趣的:(后端,C语言实现websocket,websocket,C)