C语言实现简单的Web服务器

个人博客:coonaa.cn本文博客同步地址


在之前的文章中使用C语言实现基于TCP的WinSock套接字编程。基于此,同样可以使用C语言来实现简单的Web服务器。


一. 基础知识

1. TCP协议和WinSock套接字
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议的通信需要经过创建连接(三次握手)、数据传送、终止连接(四次挥手)三个步骤。
TCP三次握手
TCP三次握手
TCP四次挥手
TCP四次挥手

WinSock套接字是Windows操作系统所提供的网络编程接口,是一个抽象层,应用程序可以通过套接字来实现数据的发送和接收。
基于TCP的WinSock工作流程
基于TCP的WinSock工作流程

2. HTTP协议
超文本传输协议(HTTP,Hypertext Transfer Protocol),是一个基于请求与响应模式的、无状态的、应用层的协议,通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码的形式给出,消息内容则具有一个类似MIME的格式。
可以抽象的认为,基于TCP的WinSock编程已经铺好了一条路,接下来需要的就是选用一辆好的卡车来输送客户端和服务器端的“交流”信息了。HTTP协议正是这辆卡车。
HTTP请求报文的基本格式构成
HTTP请求报文的基本格式构成

二. 详细设计

Web服务器与客户端之间的大致工作流程
Web服务器与客户端之间的大致工作流程
整个Web服务器设计可分为以下几个部分:
1. 初始化Windows Socket,为TCP连接的建立做准备
①加载Windows Socket;
②创建套接字;
③根据服务器端的相关IP地址和端口号信息,进行套接字绑定。

2. 启动监听,接受客户端请求建立TCP连接,接收HTTP请求报文
①Web服务器启动监听;
②接收来自客户端的连接请求,建立TCP连接;
③接收来自客户端的TCP传输数据,即HTTP请求报文。

3. 处理HTTP请求报文并做出响应
①处理HTTP请求报文的请求行,提取“请求方法”、“URL”、“HTTP版本“三个关键要素;
②判断处理“请求方法”,根据判断处理的结果,构造相应的响应报文,反馈相关给客户端,并且在服务器端打印反馈的结果;
③判断处理“URL”,根据判断处理的结果,构造相应的响应报文,反馈相关给客户端,并且在服务器端打印反馈的结果;
④由于Web服务器的要求不太高以及目前主流的客户端浏览器都会采用较“统一”的HTTP协议版本,所以“HTTP版本”的关键信息可以做忽略处理。

4. 关闭连接及Windows Socket
①关闭所建立连接的套接字;
②关闭Windows Socket。

注:根据实际情况,可运用循环多线程等方式对以上步骤进行重构。


三. 代码实现

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNNINGS

#define SERVER_IP_ADDR "127.0.0.1"	//服务器IP地址
#define SERVER_PORT 80				//服务器端口号
#define BACKLOG 10
#define BUF_SIZE 1024
#define OK 1
#define ERROR 0

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h> 
#include <time.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

const char* Server_name = "Server: Web Server 1.0 - BooLo\r\n";
//Web服务器信息 

int Server_Socket_Init();
int Handle_Request_Message(char* message, int Socket);
int Judge_URI(char* URI, int Socket);
int Send_Ifon(int Socket, const char* sendbuf, int Length);
int Error_Request_Method(int Socket);
int Inquire_File(char* URI);
int File_not_Inquire(int Socket);
int Send_File(char* URI, int Socket);
int Logo();
const char* Judge_Method(char* method, int Socket);
const char* Judge_File_Type(char* URI, const char* content_type);
const char* Get_Data(const char* cur_time);
const char* Post_Value(char* message);

int Server_Socket_Init() {
	//初始化和构造套接字 
	WORD wVersionrequested;
	WSADATA wsaData;
	SOCKET ServerSock;
	struct sockaddr_in ServerAddr;
	int rval;

	/* 加载Winsock */
	wVersionrequested = MAKEWORD(2, 2);
	if (WSAStartup(wVersionrequested, &wsaData) != 0) {
		printf("Failed to load Winsock!\n");
		system("pause");
		return -1;
	}
	printf("Succeed to load Winsock!\n");

	/* 创建套接字 */
	ServerSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ServerSock == INVALID_SOCKET) {
		printf("Failed to create socket!\n");
		system("pause");
		exit(1);
	}
	printf("Succeed to create socket!\n");

	/* 配置服务器IP、端口信息 */
	memset(&ServerAddr, 0, sizeof(struct sockaddr));	//每一个字节都用0来填充
	ServerAddr.sin_family = AF_INET;
	ServerAddr.sin_port = htons(SERVER_PORT);
	ServerAddr.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);

	/* 绑定 */
	rval = bind(ServerSock, (SOCKADDR*)&ServerAddr, sizeof(struct sockaddr));
	if (rval == SOCKET_ERROR) {
		printf("Failed to bind stream socket!\n");
		system("pause");
		exit(1);
	}
	printf("Succeed to bind stream socket!\n");

	return ServerSock;
}

int Handle_Request_Message(char* message, int Socket) {
	//处理HTTP请求报文信息 
	int rval = 0;
	char Method[BUF_SIZE];
	char URI[BUF_SIZE];
	char Version[BUF_SIZE];

	if (sscanf(message, "%s %s %s", Method, URI, Version) != 3) {
		printf("Request line error!\n");
		return ERROR;
	}	//提取"请求方法"、"URL"、"HTTP版本"三个关键要素 

	if (Judge_Method(Method, Socket) == ERROR) {
		return ERROR;
	}
	else if(Judge_Method(Method, Socket) == "POST") {
		Post_Value(message);
	}	//判断处理"请求方法" 

	if (Judge_URI(URI, Socket) == ERROR) {
		return ERROR;
	}	//判断处理"URI" 
	else
		rval = Send_File(URI, Socket);

	if (rval == OK) {
		printf("The process is successfully finished!\n");
	}

	return OK;
}

const char* Judge_Method(char* method, int Socket) {
	//判断请求方式 
	if (strcmp(method, "GET") == 0) {
		return "GET";
	}
	else if (strcmp(method, "POST") == 0) {
		return "POST";
	}
	else{
		Error_Request_Method(Socket);
		return ERROR;
	}
}

int Judge_URI(char* URI, int Socket) {
	//判断请求URI 
	if (Inquire_File(URI) == ERROR) {
		File_not_Inquire(Socket);
		return ERROR;
	}
	else
		return OK;
}

int Send_Ifon(int Socket, const char* sendbuf, int Length) {
	//发送信息到客户端 
	int sendtotal = 0, bufleft, rval = 0;

	bufleft = Length;
	while (sendtotal < Length) {
		rval = send(Socket, sendbuf + sendtotal, bufleft, 0);
		if (rval < 0) {
			break;
		}
		sendtotal += rval;
		bufleft -= rval;
	}

	Length = sendtotal;

	return rval < 0 ? ERROR : OK;
}

int Error_Request_Method(int Socket) {
	//501 Not Implemented响应 
	const char* Method_err_line = "HTTP/1.1 501 Not Implemented\r\n";
	const char* cur_time = "";
	const char* Method_err_type = "Content-type: text/plain\r\n";
	const char* File_err_length = "Content-Length: 41\r\n";
	const char* Method_err_end = "\r\n";
	const char* Method_err_info = "The request method is not yet completed!\n";

	printf("The request method from client's request message is not yet completed!\n");

	if (Send_Ifon(Socket, Method_err_line, strlen(Method_err_line)) == ERROR) {
		printf("Sending method_error_line failed!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, Server_name, strlen(Server_name)) == ERROR) {
		printf("Sending Server_name failed!\n");
		return ERROR;
	}

	cur_time = Get_Data(cur_time);
	Send_Ifon(Socket, "Data: ", 6);
	if (Send_Ifon(Socket, cur_time, strlen(cur_time)) == ERROR) {
		printf("Sending cur_time error!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, Method_err_type, strlen(Method_err_type)) == ERROR) {
		printf("Sending method_error_type failed!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, Method_err_end, strlen(Method_err_end)) == ERROR) {
		printf("Sending method_error_end failed!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, Method_err_info, strlen(Method_err_info)) == ERROR) {
		printf("Sending method_error_info failed!\n");
		return ERROR;
	}

	return OK;
}

int Inquire_File(char* URI) {
	//查找文件 
	struct stat File_info;

	if (stat(URI, &File_info) == -1)
		return ERROR;
	else
		return File_info.st_size;
}

int File_not_Inquire(int Socket) {
	//404 Not Found响应 
	const char* File_err_line = "HTTP/1.1 404 Not Found\r\n";
	const char* cur_time = "";
	const char* File_err_type = "Content-type: text/plain\r\n";
	const char* File_err_length = "Content-Length: 42\r\n";
	const char* File_err_end = "\r\n";
	const char* File_err_info = "The file which is requested is not found!\n";

	printf("The request file from client's request message is not found!\n");

	if (Send_Ifon(Socket, File_err_line, strlen(File_err_line)) == ERROR) {
		printf("Sending file_error_line error!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, Server_name, strlen(Server_name)) == ERROR) {
		printf("Sending Server_name failed!\n");
		return ERROR;
	}

	cur_time = Get_Data(cur_time);
	Send_Ifon(Socket, "Data: ", 6);
	if (Send_Ifon(Socket, cur_time, strlen(cur_time)) == ERROR) {
		printf("Sending cur_time error!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, File_err_type, strlen(File_err_type)) == ERROR) {
		printf("Sending file_error_type error!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, File_err_length, strlen(File_err_length)) == ERROR) {
		printf("Sending file_error_length error!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, File_err_end, strlen(File_err_end)) == ERROR) {
		printf("Sending file_error_end error!\n");
		return ERROR;
	}

	if (Send_Ifon(Socket, File_err_info, strlen(File_err_info)) == ERROR) {
		printf("Sending file_error_info failed!\n");
		return ERROR;
	}

	return OK;
}

int Send_File(char* URI, int Socket) {
	//200 OK响应 
	const char* File_ok_line = "HTTP/1.1 200 OK\r\n";
	const char* cur_time = "";
	const char* File_ok_type = "";
	const char* File_ok_length = "Content-Length: ";
	const char* File_ok_end = "\r\n";

	FILE* file;
	struct stat file_stat;
	char Length[BUF_SIZE];
	char sendbuf[BUF_SIZE];
	int send_length;

	if (Judge_File_Type(URI, File_ok_type) == ERROR) {
		printf("The request file's type from client's request message is error!\n");
		return ERROR;
	}

	file = fopen(URI, "rb");
	if (file != NULL) {
		fstat(fileno(file), &file_stat);
		itoa(file_stat.st_size, Length, 10);

		if (Send_Ifon(Socket, File_ok_line, strlen(File_ok_line)) == ERROR) {
			printf("Sending file_ok_line error!\n");
			return ERROR;
		}

		if (Send_Ifon(Socket, Server_name, strlen(Server_name)) == ERROR) {
			printf("Sending Server_name failed!\n");
			return ERROR;
		}

		cur_time = Get_Data(cur_time);
		Send_Ifon(Socket, "Data: ", 6);
		if (Send_Ifon(Socket, cur_time, strlen(cur_time)) == ERROR) {
			printf("Sending cur_time error!\n");
			return ERROR;
		}

		File_ok_type = Judge_File_Type(URI, File_ok_type);
		if (Send_Ifon(Socket, File_ok_type, strlen(File_ok_type)) == ERROR) {
			printf("Sending file_ok_type error!\n");
			return ERROR;
		}

		if (Send_Ifon(Socket, File_ok_length, strlen(File_ok_length)) != ERROR) {
			if (Send_Ifon(Socket, Length, strlen(Length)) != ERROR) {
				if (Send_Ifon(Socket, "\n", 1) == ERROR) {
					printf("Sending file_ok_length error!\n");
					return ERROR;
				}
			}
		}

		if (Send_Ifon(Socket, File_ok_end, strlen(File_ok_end)) == ERROR) {
			printf("Sending file_ok_end error!\n");
			return ERROR;
		}

		while (file_stat.st_size > 0) {
			if (file_stat.st_size < 1024) {
				send_length = fread(sendbuf, 1, file_stat.st_size, file);
				if (Send_Ifon(Socket, sendbuf, send_length) == ERROR) {
					printf("Sending file information error!\n");
					continue;
				}
				file_stat.st_size = 0;
			}
			else {
				send_length = fread(sendbuf, 1, 1024, file);
				if (Send_Ifon(Socket, sendbuf, send_length) == ERROR) {
					printf("Sending file information error!\n");
					continue;
				}
				file_stat.st_size -= 1024;
			}
		}
	}
	else {
		printf("The file is NULL!\n");
		return ERROR;
	}

	return OK;
}

const char* Judge_File_Type(char* URI, const char* content_type) {
	//文件类型判断 
	const char* suffix;

	if ((suffix = strrchr(URI, '.')) != NULL)
		suffix = suffix + 1;

	if (strcmp(suffix, "html") == 0) {
		return content_type = "Content-type: text/html\r\n";
	}
	else if (strcmp(suffix, "jpg") == 0) {
		return content_type = "Content-type: image/jpg\r\n";
	}
	else if (strcmp(suffix, "png") == 0) {
		return content_type = "Content-type: image/png\r\n";
	}
	else if (strcmp(suffix, "gif") == 0) {
		return content_type = "Content-type: image/gif\r\n";
	}
	else if (strcmp(suffix, "txt") == 0) {
		return content_type = "Content-type: text/plain\r\n";
	}
	else if (strcmp(suffix, "xml") == 0) {
		return content_type = "Content-type: text/xml\r\n";
	}
	else if (strcmp(suffix, "rtf") == 0) {
		return content_type = "Content-type: text/rtf\r\n";
	}
	else
		return ERROR;
}

const char* Get_Data(const char* cur_time) {
	//获取Web服务器的当前时间作为响应时间 
	time_t curtime;
	time(&curtime);
	cur_time = ctime(&curtime);

	return cur_time;
}

const char* Post_Value(char* message) {
	//获取客户端POST请求方式的值 
	const char* suffix;
	
	if ((suffix = strrchr(message, '\n')) != NULL)
		suffix = suffix + 1;
	printf("\n\nPost Value: %s\n\n", suffix);
	
	return suffix;
}

int Logo() {
	//Web服务器标志信息 
	printf("___________________________________________________________\n");
	printf("  __          ________ _______\n");
	printf("  \\ \\        / /  ____|  _____\\\n");
	printf("   \\ \\  /\\  / /| |____  |____) )\n");
	printf("    \\ \\/  \\/ / |  ____|  ____(   __  __     __ ___\n");
	printf("     \\  /\\  /  | |____  |____) )(__ |_ \\  /|_ |___)\n");
	printf("      \\/  \\/   |______|_______/  __)|__ \\/ |__|   \\\n");
	printf("\n");
	printf("            Welcome to use the Web Server!\n");
	printf("                     Version 1.0\n\n");
	printf("                         BooLo\n");
	printf("___________________________________________________________\n\n");

	return OK;
}

int main() {
	//实现主要功能 
	SOCKET ServerSock, MessageSock;
	struct sockaddr_in ClientAddr;
	int rval, Length;
	char revbuf[BUF_SIZE];

	Logo();
	printf("Web Server 1.0 is starting......\n\n");
	ServerSock = Server_Socket_Init();
	printf("\n-----------------------------------------------------------\n");

	while (OK) {
		/* 启动监听 */
		rval = listen(ServerSock, BACKLOG);
		if (rval == SOCKET_ERROR) {
			printf("Failed to listen socket!\n");
			system("pause");
			exit(1);
		}
		printf("Listening the socket ......\n");

		/* 接受客户端请求建立连接 */
		Length = sizeof(struct sockaddr);
		MessageSock = accept(ServerSock, (SOCKADDR*)&ClientAddr, &Length);
		if (MessageSock == INVALID_SOCKET) {
			printf("Failed to accept connection from client!\n");
			system("pause");
			exit(1);
		}
		printf("Succeed to accept connection from [%s:%d] !\n\n", inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port));

		/* 接收客户端请求数据 */
		memset(revbuf, 0, BUF_SIZE);	//每一个字节都用0来填充 
		rval = recv(MessageSock, revbuf, BUF_SIZE, 0);
		revbuf[rval] = 0x00;
		if (rval <= 0)
			printf("Failed to receive request message from client!\n");
		else {
			printf("%s\n", revbuf);	//输出请求数据内容
			rval = Handle_Request_Message(revbuf, MessageSock);
		}

		closesocket(MessageSock);
		printf("\n-----------------------------------------------------------\n");
	}

	closesocket(ServerSock);	//关闭套接字 
	WSACleanup();	//停止Winsock

	return OK;
}


四. 功能测试

测试准备工作
功能测试需要两个准备工作:准备测试文件、将文件存放到正确的文件目录。

(1)准备测试文件
准备几个常见文件类型的测试文件,内容随意。需要本文中测试文件的小伙伴也可以在我的博客留言联系我。

(2)将文件存放到正确的文件目录
由于在设计本程序时采用了“绝对路径”,因此需要将程序和测试文件存放到同一个磁盘的特定目录内(不理解“绝对路径”和“相对路径”的小伙伴自行某度)。

①在某个磁盘的根目录下创建一个文件夹,将测试文件和服务器程序存放到在该文夹内;
文件目录
②运行服务器程序后,打开浏览器,在地址栏输入对应的文件目录即可请求成功。注意:IP地址后的第一个斜杠“ / ”即表示对应磁盘的“根目录”。
l浏览器请求
程序运行测试
程序运行结果

index.html页面请求测试
服务器端信息
响应页面
响应状态信息
响应头信息
响应头信息

picture1.jpg图片请求测试
服务器端信息
响应页面
响应状态信息
响应头信息

download_test.rtf文档下载测试
服务器端信息
响应状态信息
响应头信息
下载文件

POST请求方式测试
客户端POST请求数据
服务器端信息

404响应状态测试
服务器端信息
响应页面
响应状态信息

响应头信息

五. 内容总结

本文中Web服务器程序的设计和开发,关键在于TCP协议、Windows Socket网络接口、HTTP协议三大知识点。在实现最基本的Web服务器核心功能的过程中,这三者缺一不可。
在Web服务器程序的设计和开发过程中,需要充分了解Web服务器的工作过程:从浏览器发起请求到浏览器显示请求的页面内容的整个过程,来逐一完成各个阶段的设计和开发。
开发过程中,特别是编码的过程中,会遇到很多的难题。这些难题通过参考一些官方的代码示例能够得到一定的启示,从而解决问题。


本文中的程序并未实现多线程,各位有兴趣的小伙伴可自行进行改进。

  • 38
    点赞
  • 265
    收藏
    觉得还不错? 一键收藏
  • 56
    评论
实现一个简单Web服务器,可以使用C语言和Socket编程。 以下是实现一个简单Web服务器的步骤: 1. 创建一个Socket,绑定IP地址和端口号。 2. 监听客户端的连接请求。 3. 接收客户端的请求,解析HTTP报文。 4. 处理请求,生成HTTP响应报文。 5. 发送HTTP响应报文给客户端。 6. 关闭连接。 以下是一个简单C语言Web服务器的代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define SERVER_PORT 8080 int main(void) { int server_socket, client_socket; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len = sizeof(client_addr); // 创建Socket server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { printf("Failed to create socket.\n"); return -1; } // 绑定IP地址和端口号 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERVER_PORT); if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { printf("Failed to bind.\n"); return -1; } // 监听客户端连接请求 if (listen(server_socket, 10) == -1) { printf("Failed to listen.\n"); return -1; } printf("Server is running on port %d.\n", SERVER_PORT); while (1) { // 接收客户端连接请求 client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len); if (client_socket == -1) { printf("Failed to accept.\n"); return -1; } printf("Client %s:%d connected.\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 接收客户端请求 char buffer[1024]; memset(buffer, 0, sizeof(buffer)); read(client_socket, buffer, sizeof(buffer)); // 解析HTTP请求报文 printf("Request:\n%s", buffer); // 处理请求,生成HTTP响应报文 char response[] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body><h1>Hello, World!</h1></body></html>"; // 发送HTTP响应报文给客户端 write(client_socket, response, sizeof(response)); // 关闭连接 close(client_socket); printf("Client %s:%d disconnected.\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } // 关闭Socket close(server_socket); return 0; } ``` 这个Web服务器可以处理来自客户端的HTTP请求,并返回一个简单的HTML页面。需要注意的是,这只是一个简单的示例,实际的Web服务器需要考虑更多的因素,例如并发连接、安全性、性能等。
评论 56
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值