参考
1.90行代码实现C语言版https服务器,基于openssl
2.使用OpenSSL生成自签名SSL/TLS证书和私钥
注意:证书和私钥文件(server.crt,server.key)的生成请参考此链接
代码
cpp
#define SERVER_PORT 8080 //设置端口号
#include<stdio.h>
#include<string.h>
#include<iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include<string.h>
#include<openssl/ssl.h>
#include<openssl/err.h>
#ifdef _WIN32
#include <Windows.h>
// Windows 文件操作相关代码
#else
#include <sys/stat.h>
// 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 <ws2tcpip.h> 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;
}