一直在写C#代码好多年不写C语言代码了,记录一下之前某个项目里用C写的一个websocket服务,用C的优势是写的东西体积小性能高,但是写业务的话还得用C#、Java之类的语言,不然会折腾死人。。。
用Visual Studio新建一个C++(因为不能直接建C语言项目)项目,我演示就创建一个控制台项目。项目创建完后首先要添加socket编程需要的依赖库ws2_32.lib,添加方式如下图
也可以在代码文件里添加这句代码:#
添加完成后就可以开始写代码了,说句题外话,Visual Studio写C语言最好把SDL检查也关掉。
新建一个wsserver.h头文件,头文件相关定义代码如下
#pragma once #include <WinSock2.h> #include <stdint.h> #include <stdbool.h> #include <stdio.h> #include <string.h> #include <ctype.h> #include <windef.h> #include <stdlib.h> #include "sha1.h" #include "b64.h" #include "cJSON.h" typedef enum FrameType { frameType_continuation, frameType_text, frameType_binary, frameType_connectionClose, frameType_ping, frameType_pong } FrameType; typedef enum FrameState { frameState_init, // 未读取任何字节 frameState_firstByte, // 已读取首字节FIN、RSV、opcode frameState_mask, // 已读取掩码 frameState_7bitLength, // 已读取7bit长度 frameState_16bitLengthWait, // 等待读取16bit长度 frameState_63bitLengthWait, // 等待读取63bit长度 frameState_16bitLength, // 已读取16bit长度 frameState_63bitLength, // 已读取63bit长度 frameState_maskingKey, // 已读取Masking-key frameState_readingData, // 正在读取载荷数据 frameState_success, // 读取完毕 frameState_failure // 读取错误 } FrameState; typedef struct WsFrame { FrameState state; bool FIN; FrameType frameType; uint8_t mask[4]; unsigned char* buff; // 数据存放的空间 uint64_t buffSize; // 当前申请的buff大小 uint64_t handledLen; // 已处理的帧长度 uint64_t headerLen; // 帧头长度 只有在state为'已读取掩码'及之后才有意义 uint64_t payloadLen; // 载荷长度 只有在state为'已读取xbit长度'后才有意义 struct WsFrame* next; // 下一帧的指针 } WsFrame; void initWsFrameStruct(WsFrame* wsFrame); char* convertToWebSocketFrame(const char* data, FrameType type, size_t len, size_t* newLen); int readWebSocketFrameStream(WsFrame* wsFrame, const char* buff, int len); void freeWebSocketFrame(WsFrame* wsFrame); int wsShakeHands(const char* recvBuff, int recvLen, SOCKET socket, const char* path); int wsFrameSend(SOCKET socket, const char* buff, int len, FrameType type); void wsFrameSendToAll(const char* buff, int len, FrameType type); int serverStart(const char* address, u_short port, const char* path); void serverStop(void);
新建一个wsserver.c代码文件,我们一步一步的来实现这些方法。
首先是serverStart方法,顾名思义,启动ws服务,第一个参数是地址(一般传本机IP),第二个参数是要监听的端口,第三个参数是路径,完整代码如下
#include "wsserver.h" #include <time.h> #define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS typedef enum { socketProtocol, websocketProtocol } Protocol; typedef struct { Protocol protocol; SOCKET socket; WsFrame wsFrame; } Client; #define MAX_CLIENT_NUM FD_SETSIZE static struct { int total; Client clients[MAX_CLIENT_NUM]; } clientSockets; static SOCKET serverSocket; // 打印日志 void printLog(const char* type, const char* format, ...) { char buff[512] = { 0 }; va_list arg; va_start(arg, format); vsnprintf(buff, sizeof(buff) - 1, format, arg); va_end(arg); char rbuf[512] = { 0 }; time_t log_time = time(NULL); struct tm* tm_log = localtime(&log_time); printf("[%04d-%02d-%02d %02d:%02d:%02d] ", tm_log->tm_year + 1900, tm_log->tm_mon + 1, tm_log->tm_mday, tm_log->tm_hour, tm_log->tm_min, tm_log->tm_sec); snprintf(rbuf, 512, "%s->%s ", type, buff); printf(rbuf); } char* UTF8ToGBK(const char* str) { // GB18030代码页 const int CODE_PAGE = 54936; int n = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); wchar_t u16str[10000]; MultiByteToWideChar(CP_UTF8, 0, str, -1, u16str, n); n = WideCharToMultiByte(CODE_PAGE, 0, u16str, -1, NULL, 0, NULL, NULL); char* gbstr = malloc(n + 1); WideCharToMultiByte(CODE_PAGE, 0, u16str, -1, gbstr, n, NULL, NULL); return gbstr; } char* GBKToUTF8(const char* str) { const int CODE_PAGE = 54936; int n = MultiByteToWideChar(CODE_PAGE, 0, str, -1, NULL, 0); wchar_t u16str[10000]; MultiByteToWideChar(CODE_PAGE, 0, str, -1, u16str, n); n = WideCharToMultiByte(CP_UTF8, 0, u16str, -1, NULL, 0, NULL, NULL); char* u8str = malloc(n + 1); WideCharToMultiByte(CP_UTF8, 0, u16str, -1, u8str, n, NULL, NULL); return u8str; } int serverStart(const char* address, u_short port, const char* path) { // 调用 WSAStartup() 函数进行初始化,并指明要使用的版本号。 WSADATA wsaData; // WSAStartup 函数启动进程使用 Winsock DLL。 int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printLog("ServerStart", "WSAStartup failed"); return -1; } struct sockaddr_in sockAddr; // ZeroMemory 宏 等价于 memset((buf),0,(BUF_SIZE)) ZeroMemory(&sockAddr, sizeof(sockAddr)); sockAddr.sin_family = PF_INET; // 等价于 AF_INET TCP UDP etc.. sockAddr.sin_addr.s_addr = inet_addr(address); sockAddr.sin_port = htons(port); // 构建一个socket对象 serverSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (serverSocket == INVALID_SOCKET) { printLog("ServerStart","Error at socket(): %d", WSAGetLastError()); WSACleanup(); return -1; } // 给socket绑定地址 if (bind(serverSocket, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)) == SOCKET_ERROR) { printLog("ServerStart", "Bind failed with error: %d", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return -1; } // 开始启动监听 if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) { printLog("ServerStart", "Listen failed with error: %d", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return -1; } clientSockets.total = 0; DWORD dwThreadId; // 创建线程开始接收socket数据 HANDLE hHandle = CreateThread(NULL, 0, (void*)receiveComingData, (PVOID)path, 0, &dwThreadId); return 0; }
serverStart方法中最后创建线程开始接收socket数据的方法receiveComingData代码
void receiveComingData(const char* path) { #define RECV_BUFLEN 0X40000 char recvbuf[RECV_BUFLEN]; int iResult; int ret; fd_set fdread; struct timeval tv = { 1, 0 }; receivingDataLoop: FD_ZERO(&fdread); // 清空socket集合 FD_SET(serverSocket, &fdread); // 设置socket数据读取集合 for (int i = 0; i < clientSockets.total; i++) { FD_SET(clientSockets.clients[i].socket, &fdread); } // 检查socket是否有数据可读 ret = select(0, &fdread, NULL, NULL, &tv); if (ret == 0) { goto receivingDataLoop; // select的等待时间到达,开始下一轮等待 } // 检查socket是否在这个集合里 if (FD_ISSET(serverSocket, &fdread)) { acceptConnect(); // 处理socket连接 } for (int i = 0; i < clientSockets.total; i++) { Client* client = &clientSockets.clients[i]; if (!FD_ISSET(client->socket, &fdread)) { continue; } // 接收数据 iResult = recv(client->socket, recvbuf, RECV_BUFLEN, 0); if (iResult > 0) { printLog("receiveComingData", "Bytes received: %d", iResult); // 协议升级 if (client->protocol == socketProtocol) { int result = wsShakeHands(recvbuf, iResult, client->socket, path); if (result != 0) { removeClient(i--); } else { client->protocol = websocketProtocol; initWsFrameStruct(&client->wsFrame); // 初始化ws帧结构 } } // WebSocket通信 else if (client->protocol == websocketProtocol) { int result = wsClientDataHandle(recvbuf, iResult, client); if (result == -1) { removeClient(i--); } } } else { if (iResult == 0) { // 客户端礼貌的关闭连接 printLog("receiveComingData", "Connection closing..."); } else { // 客户端异常关闭连接等情况 printLog("receiveComingData", "Recv failed: %d", WSAGetLastError()); } removeClient(i--); } } goto receivingDataLoop; }
receiveComingData方法里处理socket协议升级的代码
// 不区分大小写的比较字符串,相等返回true bool stricasecmp(const char* a, const char* b) { do { if (*a == '