1.90行代码实现C语言版https服务器,基于openssl
2.使用OpenSSL生成自签名SSL/TLS证书和私钥
注意:证书和私钥文件(server.crt,server.key)的生成请参考此链接
#define SERVER_PORT 8080 //设置端口号 #include #include #include #include #include #include #include #include #ifdef _WIN32 #include // Windows 文件操作相关代码 #else #include // Unix/Linux 文件操作相关代码 #endif #pragma comment(lib, "Ws2_32.lib") struct client_mes {//客户端请求信息结构体 char IP[20]; //客户ip地址 int PORT; //客户端口号 char method[10];//请求方法 char url[1024]; //请求url char version[10];//协议及版本信息 }c_mes; struct kay_and_value {//每一个键值对结构体 char key[10]; char value[100]; }; struct url_mes { char path[100];//请求路径 //采用结构体数组来存储键值对 struct kay_and_value k_v[10]; int k_v_len;//实际键值对个数 }u_mes; char messages[1024] = {0};//存储返回信息的全局变量 //定义http响应行全局变量 char u200[] = "HTTP/1.0 200 OK\r\n"; char u400[] = "HTTP/1.0 400 BAD REQUEST\r\n"; char u404[] = "HTTP/1.0 404 NOT FOUND\r\n"; char u500[] = "HTTP/1.0 500 INTERNAL SERVER ERROR\r\n"; char u501[] = "HTTP/1.0 501 METHOD NOT IMPLEMENTED\r\n"; int main() { SSL_CTX* initSSL(); int creat_socket_listen(); char* get_path(); void do_http_request(char buf[1024]);//对缓冲区接受到的客户请求信息进行解析 int do_http_resolve(char url[1024], int clnt_sock);//对客户端请求进行响应 void do_http_url_process(char url[1024]);//对客户端的url进行解析 void do_http_response(int clnt_sock, const char* path); // 初始化键值对结构体数组 for (int k = 0; k < 10; k++) { strcpy_s(u_mes.k_v[k].key, ""); strcpy_s(u_mes.k_v[k].value, ""); } memset(&c_mes, 0, sizeof(c_mes));//将结构体里面的数据清零 memset(&u_mes, 0, sizeof(url_mes)); // 初始化 Winsock 库 WSADATA wsaData; int result = WSAStartup(MAKEWORD(2, 2), &wsaData); if (result != 0) { fprintf(stderr, "WSAStartup failed with error code: %d\n", result); return 1; } //初始化ssl库 SSL_CTX* ctx; ctx = initSSL(); //初始化socket库并实现监听 int serv_sock; serv_sock = creat_socket_listen(); //接收客户端请求 SSL* ssl; /*定义一个结构体,用于存储客户端的地址信息,包括IP地址和端口号 */ struct sockaddr_in clnt_addr; /*定义变量clnt_addr_size用来存储结构体clnt_addr的大小 socklen_t 被设计用来表示套接字地址长度的类型 遇到问题:若无#include socklen_t 会报错 */ socklen_t clnt_addr_size = sizeof(clnt_addr); /*accept()函数用于接受客户端的请求,并创建一个新的套接字用于与客户端通信 第一个参数:服务器套接字的文件描述符 第二个参数:函数调用成功后,客户端的信息将保存在结构体clnt_addr中 第三个参数:指定了clnt_addr结构体的大小 函数调用成功后返回一个新的文件描述符clnt_sock,代表与客户端通信的套接字 */ int clnt_sock; if ((clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size)) < 0) { perror("accept failed"); exit(EXIT_FAILURE); } printf("接收客户端请求成功\n"); char client_ip[64]; char buf[1024] = { 0 }; ssl = SSL_new(ctx); SSL_set_fd(ssl, clnt_sock); if (SSL_accept(ssl)<=0) { ERR_print_errors_fp(stderr); abort(); } int size = SSL_read(ssl,buf,sizeof(buf)); printf("ssl_read:%s\n",buf); /*打印客户端ip地址和端口号 inet_ntop()函数用于将网络字节序的ip地址转换为可读的字符串格式, 它被用来将客户端的 IP 地址从 clnt_addr.sin_addr.s_addr 转换为一个字符串,并将结果存储在 client_ip 数组中 ntohs()用来将网络字节序的端口号转换为主机字节序的端口号 */ /* printf("ip地址:%s\t port:%d\n", inet_ntop(AF_INET, &clnt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(clnt_addr.sin_port) ); */ //设置发出请求的客户端的ip地址和端口号 strcat_s(c_mes.IP,inet_ntop(AF_INET, &clnt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip))); c_mes.PORT=ntohs(clnt_addr.sin_port); /*读取客户端请求 用套接字从clnt_sock中接收数据,并将数据存储到缓冲区buf中,最多接受1024字节的数据 如果调用成功返回读取的字节数,否则返回-1 */ /* int valread; if ((valread = recv(clnt_sock, buf, 1024, 0)) < 0) { perror("read failed"); exit(EXIT_FAILURE); } */ printf("——————————————————————————\n"); printf("读取数据成功\n"); printf("客户端请求为:\n%s\n", buf); //解析客户端请求 do_http_request(buf); //检验解析成果 printf("——————————————————————————\n"); printf("客户端信息解析成功\n"); //printf("客户端信息解析如下:\nip地址:%s\n端口号:%d\n请求方法:%s 长度:%d\n请求url:%s 长度:%d\n请求协议和方法:%s 长度:%d\n",c_mes.IP, c_mes.PORT, c_mes.method,strlen(c_mes.method), c_mes.url, strlen(c_mes.url), c_mes.version, strlen(c_mes.version)); printf("——————————————————————————\n"); //实现http响应 printf("对客户端请求进行响应\n"); int t=do_http_resolve(c_mes.url,clnt_sock); char* paths = get_path(); if (t == 1) {//正常访问 do_http_response(clnt_sock, paths); } else if (t==2) {//404 do_http_response(clnt_sock, "./error.html");//无法获取文件信息 } else if (t==3) {//500 do_http_response(clnt_sock, "./unimplemented.html");//服务器不支持的请求方法 } if (messages[0]!='\0') { SSL_write(ssl,messages,sizeof(messages)); } printf("——————————————————————————\n"); printf("客户端返回数据成功!\n"); /*发送响应给客户端 向已连接的套接字clnt_sock(即客户端)发送数据 发送成功返回成功发送的字节数,发送失败返回-1 */ //关闭套接字 closesocket(clnt_sock); //关闭套接字 closesocket(serv_sock); SSL_shutdown(ssl); SSL_free(ssl); SSL_CTX_free(ctx); // 释放 Winsock 资源 WSACleanup(); return 0; } //对客户端请求进行解析 void do_http_request(char buf[1024]) {//对缓冲区接受到的客户请求信息进行解析 int i=0; int j = 0; //获取方法 j = 0; while (buf[i]!=' ') { c_mes.method[j++] = buf[i++]; } i++; //获取url if (buf[i] == '/'&&buf[i+1]==' ') {//无url int j = 0; c_mes.url[j++] = '/'; i = i + 1; } else {//有url i++;//跳过/ j = 0; while (buf[i] != ' ') { c_mes.url[j++] = buf[i++]; } } i++; //获取协议及版本信息 j = 0; while (buf[i] != '\r') { c_mes.version[j++] = buf[i++]; } } //对客户端进行url数据解析并反应(重写get方法) int do_http_resolve(char url[1024],int clnt_sock) { char* get_path(); void do_http_url_process(char url[1024]);//对客户端的url进行解析 //判断http请求是get方法 if ((strcmp(c_mes.method,"GET"))==0) { printf("客户请求是get方法\n"); //对url中的路径和参数进行解析 //计算ip地址和端口号的长度 int lens = strlen(c_mes.IP) + 4; lens = lens + 1;//冒号长度 //http://192.168.10.124:8080/index.html?name=123&psd=1234 //解析客户端路径和键值对参数 do_http_url_process(url); //输出处理后的数据 for (int i = 0; i < u_mes.k_v_len; i++) { printf("客户端path:%s\t键值对数组u_mes[%d].key=%s,u_mes[%d].value=%s\n", u_mes.path, i, u_mes.k_v[i].key, i, u_mes.k_v[i].value); } char* paths = get_path(); /*判断文件信息 int stat(const char *pathname, struct stat *buf); 第一个参数为文件路径名,第二个参数是一个关于文件信息的结构体 struct stat { dev_t st_dev; // 文件的设备编号 ino_t st_ino; // 文件的 inode 编号 mode_t st_mode; // 文件的类型和权限 nlink_t st_nlink; // 连接数 uid_t st_uid; // 文件所有者的用户 ID gid_t st_gid; // 文件所有者的组 ID dev_t st_rdev; // 如果是特殊文件,设备编号 off_t st_size; // 文件大小(以字节为单位) blksize_t st_blksize; // 文件系统 I/O 缓冲区大小 blkcnt_t st_blocks; // 分配的块数 time_t st_atime; // 最后一次访问时间 time_t st_mtime; // 最后一次修改时间 time_t st_ctime; // 最后一次更改时间 } */ struct stat filebuf; memset(&filebuf, 0, sizeof(struct stat)); if (stat(paths, &filebuf)==-1) {//获取文件信息失败 printf("stat %s find fail\n",paths); return 2; } else {//获取文件信息成功,发送响应 //拼接相对文件路径 printf("获取文件信息成功\n"); //正常入口 //1代表正常入口,2代表404,3代表服务器不支持的请求方法 return 1; } } else { printf("不是get方法,暂时无法响应!\n"); return 3; } } void do_http_url_process(char url[1024]) { int i = 0; int j = 0; //提取path while (url[i] != '?'&& url[i] != '\0') { u_mes.path[j++] = url[i++]; } u_mes.path[i] = '\0';//加上结束标志 i++; j = 0; int temp = 0;//用来标识是否出现等号 int k = 0;//控制键值对数组下标 while (url[i] != '\0') {//对键值对进行赋值 if (url[i] != '&') {//具体每队键值对的赋值 if (url[i] == '=') { temp = 1;//切换到value的赋值 j = 0; i++;//跳过= } else if (url[i] != '=') { if (temp == 0) {//对key赋值 u_mes.k_v[k].key[j++] = url[i++]; } else if (temp == 1) {//对value赋值 u_mes.k_v[k].value[j++] = url[i++]; } } } else { k++; i++; j = 0; temp = 0; } } u_mes.k_v_len = k;//记录实际参数个数 } //对客户端请求进行具体响应 char*path和char path[100]等价 void do_http_response(int clnt_sock,const char *path) {//传入请求网页和具体参数 //发送头部 void get_message(int clnt_sock, FILE * resource, const char* header); //发送主体 printf("进入do_http_response函数\tpath=%s\n",path); //确定http响应状态行 char* header = u200; //声明一个文件指针并将其初始化为null errno_t err; FILE *resource = NULL; //尝试打开文件,成功后返回文件指针,后续可通过文件指针来操作这个文件 err = fopen_s(&resource,path, "r"); if (resource == NULL) { printf("找不到请求资源%s\n", path); header = u404; return ; } if (strcmp(path, "./error.html") == 0) { header = u404; } else if (strcmp(path, "./unimplemented.html") == 0) { header = u501; } get_message(clnt_sock, resource, header); //关闭文件描述符 fclose(resource); } //获取发送头部及主体内容 void get_message(int clnt_sock, FILE* resource, const char* header) { printf("进入get_headers函数\n"); char buff[100] = { 0 };//存放主体信息 char mess[1024] = { 0 }; char clent_mes[2048] = { 0 }; struct stat st; int fileId = _fileno(resource);//获取文件描述符相关的文件标识符 if (fstat(fileId,&st)==-1) { printf("inner error\n"); header = u500; return ; } char buf[1024] = { 0 };//存放头部信息 char temp[64]; //写入状态行 strcat_s(buf,header); //消息报头 strcat_s(buf,"Server:Martin Server\r\n"); strcat_s(buf,"Content_Type:text/htmll\r\n"); strcat_s(buf,"Connection:Close\r\n"); /* sprintf()则将数据输出到指定的字符串中 */ sprintf_s(temp, "Contene-Length:%d\r\n\r\n", st.st_size); strcat_s(buf,temp); printf("头部信息为:\n%s\n",buf); while (fgets(buff, sizeof(buff), resource) != NULL) {//处理文件内容 size_t buf_len = strlen(buff); if (buf_len > 0) { size_t remain_space = sizeof(mess) - sizeof(int) * (strlen(mess) / sizeof(int)); if (remain_space >= buf_len) { memcpy((char*)mess + strlen((char*)mess), buff, buf_len); } else { printf("mess中没有剩余空间!\n"); break; } } } strcpy_s(clent_mes, buf); strcat_s(clent_mes, mess); printf("发送给客户的信息是:%s\n", clent_mes); strcpy_s(messages,clent_mes);//全局信息变量赋值 } SSL_CTX* initSSL() { SSL_CTX* ctx; //SSL库初始化 SSL_library_init(); //载入所有SSL算法 OpenSSL_add_all_algorithms(); //载入所有SSL错误消息 SSL_load_error_strings(); ctx = SSL_CTX_new(SSLv23_server_method()); if (ctx == NULL) { ERR_print_errors_fp(stderr); abort(); } //载入用户的数字证书,此证书用来发给客户端,证书里面包含公钥 if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); abort(); } //载入用户私钥 if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); abort(); } //检查用户私钥是否正确 if (!SSL_CTX_check_private_key(ctx)) { ERR_print_errors_fp(stderr); abort(); } return ctx; } int creat_socket_listen() { /*创建TCP套接字(通过IPv4族进行面向连接的通信) 返回值是新创建套接字的文件描述符,调用成功返回一个非负整数,如果调用失败,返回-1 第一个参数:地址族 AF_INET表示IPv4地址族 第二个参数:套接字类型 通过TCP连接传输 第三个参数:传输协议 0默认情况,根据上面两个参数自动选择*/ int serv_sock; if ((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } /* sockaddr_in这是一个存储IPv4地址信息的结构体 struct sockaddr_in { short sin_family; // 地址族 (AF_INET) unsigned short sin_port; // 端口号 struct in_addr sin_addr; // IPv4 地址 char sin_zero[8]; // 填充 0,保持与 sockaddr 结构体大小的兼容性 }; */ struct sockaddr_in serv_addr; /* 将结构体里面的数据清零,后续再次赋值 */ memset(&serv_addr, 0, sizeof(serv_addr)); /*指定地址族为IPv4*/ serv_addr.sin_family = AF_INET; /*设置了服务器的IP地址 INADDR_ANY 是一个特殊的常量,它表示服务器将接受来自任何网络接口的连接请求 htonl()函数用于将主机字节序转换成网络字节序,确保在不同架构的计算机上数据的正确的传输 主机字节序:大端或者小端 网络字节序:默认为大端*/ serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*设置了服务器的端口号,SERVER_PORT为代码顶部设置的宏 */ serv_addr.sin_port = htons(SERVER_PORT); /*绑定 将一个套接字与特定的ip地址和端口号关联起来,使服务器能够在该地址上监听来自客户端的请求 第一个参数:服务器套接字的文件描述符,通过此文件描述符对服务器套接字进行操作 第二个参数:&serv_addr为要绑定到套接字的结构体的指针,由于bind()函数要求的参数类型,所以进行类型转换 第三个参数:指定了serv_addr结构体的大小*/ int valbind; if ((valbind = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) < 0) { perror("bind failed"); fprintf(stderr, "Bind failed with error code: %d\n", WSAGetLastError()); exit(EXIT_FAILURE); } //进入监听状态,等待用户发起请求 int vallisten; if ((vallisten = listen(serv_sock, 3)) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("等待客户端连接...\n"); printf("——————————————————————————\n"); return serv_sock; } char* get_path() { char paths[20] = {}; int i = 0; paths[0] = '.'; paths[1] = '/'; while (u_mes.path[i] != '\0') { //printf("u_mes.path[i]=%c,i=%d\n",u_mes.path[i],i); paths[i + 2] = u_mes.path[i]; i++; } paths[i + 2] = '\0'; printf("拼接后的文件目录地址paths=%s\n", paths); return paths; }