最近自己心血来潮想学习一下C语言,感觉C语言是不会过时的,所以爬坑开始。自己也写了段时间的游戏客户端所以想以一个游戏的方式来学习服务端,考虑到要和客户端交互所以第一件事就是先写一个websocket来进行长连接。奈何网上搜索了很多资料关于C语言来实现websocket的寥寥无几,更多的是泛泛而谈亦或是提供的源码运行环境复杂更或者是根本运行不起来。
在写代码之前先设计一下有哪些东西,第一 服务启动入口; 第二 建立普通的socket;第三 建立websocket;第四 session的管理;第五 三方库;ok开始建立目录结构。其中的红色框部分是我网上找来的一些工具用于加密和http请求的解析等等。
我们从main.c开始讲,他的功能就是调用方法开启一个端口的监听,上代码
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#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;i<ret;++i){
//遍历all数组中的前ret个元素
int fd = all[i].data.fd;
//判断是否有新连接
if(fd == lfd){
//有新连接
int cfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len);
if(cfd == -1){
perror("accept error");
exit(1);
}
//设置cfd为非阻塞模式
int flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
//将新得到的cfd上epoll树
ev.events = EPOLLIN | EPOLLET; //边沿模式
ev.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
char ip[64] = {0};
save_session(cfd, inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ip,sizeof(ip)), ntohs(client_addr.sin_port));
}else{
struct session* s = get_session(fd);
//处理已连接的客户端发过来的数据
if(!all[i].events & EPOLLIN){
//如果没有读的操作那么跳过
continue;
}
//读数据
char buf[1024] = {0};
int len;
while((len = recv(fd,buf,sizeof(buf),0))>0){
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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/socket.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#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
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script type="text/javascript">
var ws = null;
function WebSocketTest()
{
if ("WebSocket" in window)
{
console.log("您的浏览器支持 WebSocket!");
// 打开一个 web socket
ws = new WebSocket("ws://xxx.xxx.xxx.xxx:xxx");
ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
console.log("socket connected...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
console.log("recv msg: ",received_msg);
};
ws.onclose = function()
{
// 关闭 websocket
console.log("socket closed...");
};
}
else
{
// 浏览器不支持 WebSocket
console.log("您的浏览器不支持 WebSocket!");
}
}
function sendMsg(){
ws.send("数据测试"+Math.floor(Math.random()*1000));
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">链接WebSocket</a><br><br>
<a href="javascript:sendMsg()">发送数据</a>
</div>
</body>
</html>
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